Files

286 lines
9.1 KiB
TypeScript
Raw Permalink Normal View History

/**
* 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);
});
});