import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest"; import type { FastifyInstance } from "fastify"; import { createTestApp, cleanDb, signupUser } from "./helpers.js"; import { db } from "../src/db/index.js"; import { packages } from "../src/db/schema.js"; let app: FastifyInstance; beforeAll(async () => { app = await createTestApp(); }); afterAll(async () => { await app.close(); }); beforeEach(async () => { await cleanDb(); }); async function setupUserWithProject() { const signup = await signupUser(app, "user@example.com", "Org"); const { accessToken, accounts } = signup.json(); const accountId = accounts[0].id as string; const projectRes = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects`, headers: { authorization: `Bearer ${accessToken}` }, payload: { name: "My Project" }, }); const projectId = projectRes.json().id as string; return { accessToken, accountId, projectId }; } function headers(accessToken: string) { return { authorization: `Bearer ${accessToken}` }; } async function seedSmallPackage() { const [pkg] = await db .insert(packages) .values({ slug: "small", name: "Small", vcpu: 1, ram: 1024, disk: 20, priceMonthly: "5.00" }) .returning(); return pkg; } describe("GET /accounts/:accountId/projects/:projectId/wms", () => { it("returns empty list when no WMs exist", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const res = await app.inject({ method: "GET", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), }); expect(res.statusCode).toBe(200); expect(res.json()).toEqual([]); }); it("returns WMs for the project", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); await app.inject({ method: "POST", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "WM 1", vcpu: 1, ram: 512, disk: 10 }, }); await app.inject({ method: "POST", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "WM 2", vcpu: 2, ram: 1024, disk: 20 }, }); const res = await app.inject({ method: "GET", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), }); expect(res.statusCode).toBe(200); expect(res.json()).toHaveLength(2); }); it("returns 404 for a project in another account", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); // 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; const res = await app.inject({ method: "GET", url: `/accounts/${secondAccountId}/projects/${projectId}/wms`, headers: headers(accessToken), }); expect(res.statusCode).toBe(404); }); }); describe("POST /accounts/:accountId/projects/:projectId/wms", () => { it("creates a WM from a package", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const pkg = await seedSmallPackage(); const res = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "My WM", packageId: pkg.id }, }); expect(res.statusCode).toBe(201); const body = res.json(); expect(body.name).toBe("My WM"); expect(body.packageId).toBe(pkg.id); expect(body.vcpu).toBe(1); expect(body.ram).toBe(1024); expect(body.disk).toBe(20); }); it("creates a WM with custom settings", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const res = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "Custom WM", vcpu: 4, ram: 8192, disk: 100 }, }); expect(res.statusCode).toBe(201); const body = res.json(); expect(body.packageId).toBeNull(); expect(body.vcpu).toBe(4); expect(body.ram).toBe(8192); expect(body.disk).toBe(100); }); it("returns 400 if neither packageId nor full custom settings provided", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const res = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "Bad WM", vcpu: 2 }, }); expect(res.statusCode).toBe(400); expect(res.json().error).toMatch(/packageId/); }); it("returns 404 for non-existent package", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const res = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "WM", packageId: "00000000-0000-0000-0000-000000000000" }, }); expect(res.statusCode).toBe(404); expect(res.json().error).toMatch(/Package/); }); it("returns 404 for a project in another account", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const accountRes = await app.inject({ method: "POST", url: "/accounts", headers: { authorization: `Bearer ${accessToken}` }, payload: { name: "Org 2" }, }); const secondAccountId = accountRes.json().id; const res = await app.inject({ method: "POST", url: `/accounts/${secondAccountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "WM", vcpu: 1, ram: 512, disk: 10 }, }); expect(res.statusCode).toBe(404); }); }); describe("PATCH /accounts/:accountId/projects/:projectId/wms/:id", () => { it("updates WM name", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const create = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "Old Name", vcpu: 1, ram: 512, disk: 10 }, }); const wmId = create.json().id; const res = await app.inject({ method: "PATCH", url: `/accounts/${accountId}/projects/${projectId}/wms/${wmId}`, headers: headers(accessToken), payload: { name: "New Name" }, }); expect(res.statusCode).toBe(200); expect(res.json().name).toBe("New Name"); expect(res.json().vcpu).toBe(1); }); it("updates individual settings (e.g. only disk)", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const pkg = await seedSmallPackage(); const create = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "WM", packageId: pkg.id }, }); const wmId = create.json().id; const res = await app.inject({ method: "PATCH", url: `/accounts/${accountId}/projects/${projectId}/wms/${wmId}`, headers: headers(accessToken), payload: { disk: 40 }, }); expect(res.statusCode).toBe(200); expect(res.json().disk).toBe(40); expect(res.json().vcpu).toBe(1); expect(res.json().ram).toBe(1024); }); it("returns 404 for non-existent WM", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const res = await app.inject({ method: "PATCH", url: `/accounts/${accountId}/projects/${projectId}/wms/00000000-0000-0000-0000-000000000000`, headers: headers(accessToken), payload: { name: "Nope" }, }); expect(res.statusCode).toBe(404); }); it("returns 404 for project in another account", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const create = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "WM", vcpu: 1, ram: 512, disk: 10 }, }); const wmId = create.json().id; const accountRes = await app.inject({ method: "POST", url: "/accounts", headers: { authorization: `Bearer ${accessToken}` }, payload: { name: "Org 2" }, }); const secondAccountId = accountRes.json().id; const res = await app.inject({ method: "PATCH", url: `/accounts/${secondAccountId}/projects/${projectId}/wms/${wmId}`, headers: headers(accessToken), payload: { name: "Hacked" }, }); expect(res.statusCode).toBe(404); }); }); describe("DELETE /accounts/:accountId/projects/:projectId/wms/:id", () => { it("deletes a WM", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const create = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "To Delete", vcpu: 1, ram: 512, disk: 10 }, }); const wmId = create.json().id; const res = await app.inject({ method: "DELETE", url: `/accounts/${accountId}/projects/${projectId}/wms/${wmId}`, headers: headers(accessToken), }); expect(res.statusCode).toBe(204); // Verify it's gone const list = await app.inject({ method: "GET", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), }); expect(list.json()).toEqual([]); }); it("returns 404 for non-existent WM", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const res = await app.inject({ method: "DELETE", url: `/accounts/${accountId}/projects/${projectId}/wms/00000000-0000-0000-0000-000000000000`, headers: headers(accessToken), }); expect(res.statusCode).toBe(404); }); it("returns 404 for project in another account", async () => { const { accessToken, accountId, projectId } = await setupUserWithProject(); const create = await app.inject({ method: "POST", url: `/accounts/${accountId}/projects/${projectId}/wms`, headers: headers(accessToken), payload: { name: "WM", vcpu: 1, ram: 512, disk: 10 }, }); const wmId = create.json().id; const accountRes = await app.inject({ method: "POST", url: "/accounts", headers: { authorization: `Bearer ${accessToken}` }, payload: { name: "Org 2" }, }); const secondAccountId = accountRes.json().id; const res = await app.inject({ method: "DELETE", url: `/accounts/${secondAccountId}/projects/${projectId}/wms/${wmId}`, headers: headers(accessToken), }); expect(res.statusCode).toBe(404); }); });