286 lines
9.1 KiB
TypeScript
286 lines
9.1 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|