import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest"; import type { FastifyInstance } from "fastify"; import { createTestApp, cleanDb, signupUser } from "./helpers.js"; let app: FastifyInstance; beforeAll(async () => { app = await createTestApp(); }); afterAll(async () => { await app.close(); }); beforeEach(async () => { await cleanDb(); }); async function setupUser() { const signup = await signupUser(app, "user@example.com", "Org"); const { accessToken, accounts } = signup.json(); return { accessToken, accountId: accounts[0].id as string }; } function headers(accessToken: string) { return { authorization: `Bearer ${accessToken}` }; } describe("GET /accounts/:accountId/projects", () => { it("returns empty list when no projects exist", async () => { const { accessToken, accountId } = await setupUser(); const res = await app.inject({ method: "GET", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), }); expect(res.statusCode).toBe(200); expect(res.json()).toEqual([]); }); it("returns projects for the account", async () => { const { accessToken, accountId } = await setupUser(); await app.inject({ method: "POST", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), payload: { name: "Project A" }, }); await app.inject({ method: "POST", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), payload: { name: "Project B" }, }); const res = await app.inject({ method: "GET", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), }); expect(res.statusCode).toBe(200); const body = res.json(); expect(body).toHaveLength(2); expect(body.map((p: { name: string }) => p.name).sort()).toEqual(["Project A", "Project B"]); }); it("does not return projects from another account", async () => { const { accessToken, accountId } = await setupUser(); // Create a project in the first account await app.inject({ method: "POST", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), payload: { name: "Project A" }, }); // Create a second account const accountRes = await app.inject({ method: "POST", url: "/accounts", headers: { authorization: `Bearer ${accessToken}` }, payload: { name: "Org 2" }, }); const secondAccountId = accountRes.json().id; // List projects in the second account — should be empty const res = await app.inject({ method: "GET", url: `/accounts/${secondAccountId}/projects`, headers: headers(accessToken), }); expect(res.statusCode).toBe(200); expect(res.json()).toEqual([]); }); it("returns 401 without auth", async () => { const res = await app.inject({ method: "GET", url: "/accounts/00000000-0000-0000-0000-000000000000/projects" }); expect(res.statusCode).toBe(401); }); }); describe("POST /accounts/:accountId/projects", () => { it("creates a project", async () => { const { accessToken, accountId } = await setupUser(); const res = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), payload: { name: "New Project" }, }); expect(res.statusCode).toBe(201); const body = res.json(); expect(body.name).toBe("New Project"); expect(body.accountId).toBe(accountId); expect(body.id).toBeDefined(); }); it("returns 400 if name is missing", async () => { const { accessToken, accountId } = await setupUser(); const res = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), payload: {}, }); expect(res.statusCode).toBe(400); }); }); describe("PATCH /accounts/:accountId/projects/:id", () => { it("updates a project name", async () => { const { accessToken, accountId } = await setupUser(); const create = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), payload: { name: "Old Name" }, }); const projectId = create.json().id; const res = await app.inject({ method: "PATCH", url: `/accounts/${accountId}/projects/${projectId}`, headers: headers(accessToken), payload: { name: "New Name" }, }); expect(res.statusCode).toBe(200); expect(res.json().name).toBe("New Name"); expect(res.json().id).toBe(projectId); }); it("returns 404 for non-existent project", async () => { const { accessToken, accountId } = await setupUser(); const res = await app.inject({ method: "PATCH", url: `/accounts/${accountId}/projects/00000000-0000-0000-0000-000000000000`, headers: headers(accessToken), payload: { name: "Nope" }, }); expect(res.statusCode).toBe(404); }); it("returns 404 when updating a project from another account", async () => { const { accessToken, accountId } = await setupUser(); const create = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), payload: { name: "Project" }, }); const projectId = create.json().id; // Create second account const accountRes = await app.inject({ method: "POST", url: "/accounts", headers: { authorization: `Bearer ${accessToken}` }, payload: { name: "Org 2" }, }); const secondAccountId = accountRes.json().id; // Try to update from second account const res = await app.inject({ method: "PATCH", url: `/accounts/${secondAccountId}/projects/${projectId}`, headers: headers(accessToken), payload: { name: "Hacked" }, }); expect(res.statusCode).toBe(404); }); }); describe("DELETE /accounts/:accountId/projects/:id", () => { it("deletes a project", async () => { const { accessToken, accountId } = await setupUser(); const create = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), payload: { name: "To Delete" }, }); const projectId = create.json().id; const res = await app.inject({ method: "DELETE", url: `/accounts/${accountId}/projects/${projectId}`, headers: headers(accessToken), }); expect(res.statusCode).toBe(204); // Verify it's gone const list = await app.inject({ method: "GET", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), }); expect(list.json()).toEqual([]); }); it("returns 404 for non-existent project", async () => { const { accessToken, accountId } = await setupUser(); const res = await app.inject({ method: "DELETE", url: `/accounts/${accountId}/projects/00000000-0000-0000-0000-000000000000`, headers: headers(accessToken), }); expect(res.statusCode).toBe(404); }); it("returns 404 when deleting a project from another account", async () => { const { accessToken, accountId } = await setupUser(); const create = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects`, headers: headers(accessToken), payload: { name: "Project" }, }); const projectId = create.json().id; // Create second account const accountRes = await app.inject({ method: "POST", url: "/accounts", headers: { authorization: `Bearer ${accessToken}` }, payload: { name: "Org 2" }, }); const secondAccountId = accountRes.json().id; // Try to delete from second account const res = await app.inject({ method: "DELETE", url: `/accounts/${secondAccountId}/projects/${projectId}`, headers: headers(accessToken), }); expect(res.statusCode).toBe(404); }); });