eyrun-api/tests/projects.test.ts
2026-02-08 13:39:33 +01:00

280 lines
7.8 KiB
TypeScript

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