280 lines
7.8 KiB
TypeScript
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);
|
|
});
|
|
});
|