From c04b3df7229a0f8ba2755075a42123af594c6239 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Fri, 8 May 2026 16:09:18 +0200 Subject: [PATCH] Add Fastify app contract tests --- src/server/app.test.ts | 94 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/server/app.test.ts diff --git a/src/server/app.test.ts b/src/server/app.test.ts new file mode 100644 index 0000000..51a5971 --- /dev/null +++ b/src/server/app.test.ts @@ -0,0 +1,94 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import type { FastifyInstance } from "fastify"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { buildApp } from "./app.js"; +import { ProjectService } from "./projects/projectService.js"; +import { ProjectStore } from "./storage/projectStore.js"; +import { WorkspaceService } from "./workspaces/workspaceService.js"; +import type { Project, Workspace } from "./types.js"; + +let app: FastifyInstance; +let tempDir: string; +let projectDir: string; + +beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "pi-web-app-test-")); + projectDir = join(tempDir, "project"); + app = await buildApp({ + projects: new ProjectService(new ProjectStore(join(tempDir, "projects.json"))), + workspaces: new WorkspaceService(), + clientDist: false, + logger: false, + }); +}); + +afterEach(async () => { + await app.close(); + await rm(tempDir, { recursive: true, force: true }); +}); + +describe("buildApp", () => { + it("adds, lists, and closes projects through the HTTP contract", async () => { + const addResponse = await app.inject({ + method: "POST", + url: "/api/projects", + payload: { name: "Example", path: projectDir, create: true }, + }); + + expect(addResponse.statusCode).toBe(200); + const project = addResponse.json(); + expect(project).toMatchObject({ name: "Example", path: projectDir }); + expect(project.id).not.toBe(""); + + const listResponse = await app.inject({ method: "GET", url: "/api/projects" }); + expect(listResponse.statusCode).toBe(200); + expect(listResponse.json()).toEqual([project]); + + const closeResponse = await app.inject({ method: "DELETE", url: `/api/projects/${project.id}` }); + expect(closeResponse.statusCode).toBe(200); + expect(closeResponse.json()).toEqual({ closed: true }); + + const emptyListResponse = await app.inject({ method: "GET", url: "/api/projects" }); + expect(emptyListResponse.json()).toEqual([]); + }); + + it("returns stable errors for invalid project requests", async () => { + const addResponse = await app.inject({ + method: "POST", + url: "/api/projects", + payload: { name: "Missing", path: join(tempDir, "missing") }, + }); + + expect(addResponse.statusCode).toBe(400); + expect(addResponse.json()).toHaveProperty("error"); + + const closeResponse = await app.inject({ method: "DELETE", url: "/api/projects/does-not-exist" }); + expect(closeResponse.statusCode).toBe(404); + expect(closeResponse.json()).toEqual({ error: "Project not found" }); + }); + + it("lists a non-git project as a single workspace", async () => { + const addResponse = await app.inject({ + method: "POST", + url: "/api/projects", + payload: { name: "Plain", path: projectDir, create: true }, + }); + const project = addResponse.json(); + + const workspacesResponse = await app.inject({ method: "GET", url: `/api/projects/${project.id}/workspaces` }); + + expect(workspacesResponse.statusCode).toBe(200); + expect(workspacesResponse.json()).toEqual([ + expect.objectContaining({ + projectId: project.id, + path: projectDir, + label: "Plain", + isMain: true, + isGitRepo: false, + isGitWorktree: false, + }), + ]); + }); +});