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

361 lines
11 KiB
TypeScript

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