/** * Project Folders API Tests - CRUD for project folders + move project to folder */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import type { FastifyInstance } from 'fastify'; import { SqliteAdapter } from '../src/database/SqliteAdapter.js'; import { createServer } from '../src/server.js'; describe('Project Folders API', () => { let app: FastifyInstance; let db: SqliteAdapter; let authToken: string; let createdFolderId: string; let childFolderId: string; let createdProjectId: string; beforeAll(async () => { db = new SqliteAdapter(':memory:'); await db.init(); app = await createServer({ db, port: 0 }); await app.ready(); const regRes = await app.inject({ method: 'POST', url: '/api/auth/register', payload: { email: 'folder-test@example.com', password: 'Password123!', name: 'Folder Test' }, }); const regBody = JSON.parse(regRes.body); authToken = regBody.session.token; }); afterAll(async () => { await app.close(); db.close(); }); it('should list empty folders initially', async () => { const res = await app.inject({ method: 'GET', url: '/api/project-folders', headers: { authorization: `Bearer ${authToken}` }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(Array.isArray(body)).toBe(true); expect(body.length).toBe(0); }); it('should create a folder', async () => { const res = await app.inject({ method: 'POST', url: '/api/project-folders', headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Test Folder' }, }); expect(res.statusCode).toBe(201); const body = JSON.parse(res.body); expect(body.name).toBe('Test Folder'); expect(body.parent_id).toBeNull(); expect(body.id).toBeDefined(); createdFolderId = body.id; }); it('should create a child folder', async () => { const res = await app.inject({ method: 'POST', url: '/api/project-folders', headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Child Folder', parent_id: createdFolderId }, }); expect(res.statusCode).toBe(201); const body = JSON.parse(res.body); expect(body.name).toBe('Child Folder'); expect(body.parent_id).toBe(createdFolderId); childFolderId = body.id; }); it('should list folders including the created ones', async () => { const res = await app.inject({ method: 'GET', url: '/api/project-folders', headers: { authorization: `Bearer ${authToken}` }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.length).toBe(2); expect(body.some((f: { id: string }) => f.id === createdFolderId)).toBe(true); expect(body.some((f: { id: string }) => f.id === childFolderId)).toBe(true); }); it('should get a single folder', async () => { const res = await app.inject({ method: 'GET', url: `/api/project-folders/${createdFolderId}`, headers: { authorization: `Bearer ${authToken}` }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.id).toBe(createdFolderId); expect(body.name).toBe('Test Folder'); }); it('should rename a folder', async () => { const res = await app.inject({ method: 'PUT', url: `/api/project-folders/${createdFolderId}`, headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Renamed Folder' }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.name).toBe('Renamed Folder'); }); it('should reject folder creation without name', async () => { const res = await app.inject({ method: 'POST', url: '/api/project-folders', headers: { authorization: `Bearer ${authToken}` }, payload: {}, }); expect(res.statusCode).toBe(400); }); it('should reject rename without name', async () => { const res = await app.inject({ method: 'PUT', url: `/api/project-folders/${createdFolderId}`, headers: { authorization: `Bearer ${authToken}` }, payload: {}, }); expect(res.statusCode).toBe(400); }); it('should reject operations on non-existent folder', async () => { const getRes = await app.inject({ method: 'GET', url: '/api/project-folders/nonexistent-folder', headers: { authorization: `Bearer ${authToken}` }, }); expect(getRes.statusCode).toBe(404); const delRes = await app.inject({ method: 'DELETE', url: '/api/project-folders/nonexistent-folder', headers: { authorization: `Bearer ${authToken}` }, }); expect(delRes.statusCode).toBe(404); }); it('should reject unauthenticated requests', async () => { const res = await app.inject({ method: 'GET', url: '/api/project-folders', }); expect(res.statusCode).toBe(401); }); // ─── Project + Folder integration ──────────────────── it('should create a project with folder_id', async () => { const res = await app.inject({ method: 'POST', url: '/api/projects', headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'Folder Project', folder_id: createdFolderId }, }); expect(res.statusCode).toBe(201); const body = JSON.parse(res.body); expect(body.name).toBe('Folder Project'); expect(body.folder_id).toBe(createdFolderId); createdProjectId = body.id; }); it('should list projects filtered by folder', async () => { const res = await app.inject({ method: 'GET', url: `/api/projects?folderId=${createdFolderId}`, headers: { authorization: `Bearer ${authToken}` }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.length).toBe(1); expect(body[0].id).toBe(createdProjectId); }); it('should list unfoldered projects with folderId=null', async () => { // First create a project without folder await app.inject({ method: 'POST', url: '/api/projects', headers: { authorization: `Bearer ${authToken}` }, payload: { name: 'No Folder Project' }, }); const res = await app.inject({ method: 'GET', url: '/api/projects?folderId=null', headers: { authorization: `Bearer ${authToken}` }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.every((p: { folder_id: string | null }) => p.folder_id === null)).toBe(true); }); it('should move project to a different folder', async () => { const res = await app.inject({ method: 'PUT', url: `/api/projects/${createdProjectId}/folder`, headers: { authorization: `Bearer ${authToken}` }, payload: { folder_id: childFolderId }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.folder_id).toBe(childFolderId); }); it('should move project to root (folder_id = null)', async () => { const res = await app.inject({ method: 'PUT', url: `/api/projects/${createdProjectId}/folder`, headers: { authorization: `Bearer ${authToken}` }, payload: { folder_id: null }, }); expect(res.statusCode).toBe(200); const body = JSON.parse(res.body); expect(body.folder_id).toBeNull(); }); it('should reject moving project to non-existent folder', async () => { const res = await app.inject({ method: 'PUT', url: `/api/projects/${createdProjectId}/folder`, headers: { authorization: `Bearer ${authToken}` }, payload: { folder_id: 'nonexistent-folder' }, }); expect(res.statusCode).toBe(404); }); it('should delete folder and move projects to root', async () => { // Move project back to folder first await app.inject({ method: 'PUT', url: `/api/projects/${createdProjectId}/folder`, headers: { authorization: `Bearer ${authToken}` }, payload: { folder_id: childFolderId }, }); // Delete the child folder const delRes = await app.inject({ method: 'DELETE', url: `/api/project-folders/${childFolderId}`, headers: { authorization: `Bearer ${authToken}` }, }); expect(delRes.statusCode).toBe(204); // Verify project is now in root const projRes = await app.inject({ method: 'GET', url: `/api/projects/${createdProjectId}`, headers: { authorization: `Bearer ${authToken}` }, }); expect(projRes.statusCode).toBe(200); const projBody = JSON.parse(projRes.body); expect(projBody.folder_id).toBeNull(); }); it('should delete remaining folders', async () => { const res = await app.inject({ method: 'DELETE', url: `/api/project-folders/${createdFolderId}`, headers: { authorization: `Bearer ${authToken}` }, }); expect(res.statusCode).toBe(204); // Verify no folders left const listRes = await app.inject({ method: 'GET', url: '/api/project-folders', headers: { authorization: `Bearer ${authToken}` }, }); const listBody = JSON.parse(listRes.body); expect(listBody.length).toBe(0); }); });