/** * Integration tests for the full server startup sequence. * * Uses startServer() with a test config (tmpDir, port 0, networking off, * firehose off, OAuth off) + mock PDS to validate the complete HTTP surface. */ import { describe, it, expect, afterEach } from "vitest"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Config } from "./config.js"; import { startServer, type ServerHandle } from "./start.js"; import { createTestRepo, startMockPds, createMockDidResolver, type MockPds, } from "./replication/test-helpers.js"; const TEST_DID = "did:plc:testuser1"; function testConfig(dataDir: string, replicateDids: string[] = []): Config { return { DID: "did:plc:localnode", HANDLE: "local.test", PDS_HOSTNAME: "local.test", AUTH_TOKEN: "test-auth-token", SIGNING_KEY: "0000000000000000000000000000000000000000000000000000000000000001", SIGNING_KEY_PUBLIC: "zQ3shP2mWsZYWgvZM9GJ3EvMfRXQJwuTh6BdXLvJB9gFhT3Lr", JWT_SECRET: "test-jwt-secret", PASSWORD_HASH: "$2a$10$test", DATA_DIR: dataDir, PORT: 0, // OS-assigned random port IPFS_ENABLED: true, IPFS_NETWORKING: false, REPLICATE_DIDS: replicateDids, FIREHOSE_URL: "wss://localhost/xrpc/com.atproto.sync.subscribeRepos", FIREHOSE_ENABLED: false, RATE_LIMIT_ENABLED: false, RATE_LIMIT_READ_PER_MIN: 300, RATE_LIMIT_SYNC_PER_MIN: 30, RATE_LIMIT_SESSION_PER_MIN: 10, RATE_LIMIT_WRITE_PER_MIN: 200, RATE_LIMIT_CHALLENGE_PER_MIN: 20, RATE_LIMIT_MAX_CONNECTIONS: 100, RATE_LIMIT_FIREHOSE_PER_IP: 3, OAUTH_ENABLED: false, PUBLIC_URL: "http://localhost:3000", }; } describe("server startup integration", () => { let tmpDir: string; let handle: ServerHandle | undefined; let mockPds: MockPds | undefined; afterEach(async () => { if (handle) { await handle.close(); handle = undefined; } if (mockPds) { await mockPds.close(); mockPds = undefined; } if (tmpDir) { rmSync(tmpDir, { recursive: true, force: true }); } }); it("health check returns 200", async () => { tmpDir = mkdtempSync(join(tmpdir(), "server-startup-")); const config = testConfig(tmpDir); handle = await startServer(config); const res = await fetch(`${handle.url}/xrpc/_health`); expect(res.status).toBe(200); const body = (await res.json()) as { status: string; version: string }; expect(body.status).toBe("ok"); expect(body.version).toBeTruthy(); }); it("app HTML returns 200 with expected content", async () => { tmpDir = mkdtempSync(join(tmpdir(), "server-startup-")); const config = testConfig(tmpDir); handle = await startServer(config); const res = await fetch(`${handle.url}/`); expect(res.status).toBe(200); const html = await res.text(); expect(html).toContain("P2PDS"); }); it("admin getOverview returns 200 with version and replication state", async () => { tmpDir = mkdtempSync(join(tmpdir(), "server-startup-")); const config = testConfig(tmpDir); handle = await startServer(config); const res = await fetch(`${handle.url}/xrpc/org.p2pds.app.getOverview`, { headers: { Authorization: `Bearer ${config.AUTH_TOKEN}` }, }); expect(res.status).toBe(200); const body = (await res.json()) as { version: string; replication: unknown }; expect(body.version).toBeTruthy(); expect(body.replication).toBeDefined(); }); it("add DID, sync, and verify in overview", async () => { tmpDir = mkdtempSync(join(tmpdir(), "server-startup-")); // Create a mock PDS with a test repo const carBytes = await createTestRepo(TEST_DID, [ { collection: "app.bsky.feed.post", rkey: "abc123", record: { text: "hello", createdAt: new Date().toISOString() } }, ]); mockPds = await startMockPds([{ did: TEST_DID, carBytes }]); const mockResolver = createMockDidResolver({ [TEST_DID]: mockPds.url }); const config = testConfig(tmpDir); handle = await startServer(config, { didResolver: mockResolver }); // Add the DID via admin API const addRes = await fetch(`${handle.url}/xrpc/org.p2pds.app.addDid`, { method: "POST", headers: { Authorization: `Bearer ${config.AUTH_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ did: TEST_DID }), }); expect(addRes.status).toBe(200); // Trigger sync const syncRes = await fetch(`${handle.url}/xrpc/org.p2pds.replication.syncNow`, { method: "POST", headers: { Authorization: `Bearer ${config.AUTH_TOKEN}` }, }); expect(syncRes.status).toBe(200); // Wait a bit for async sync to complete await new Promise((r) => setTimeout(r, 2000)); // Check overview — the DID should appear in replication state const overviewRes = await fetch(`${handle.url}/xrpc/org.p2pds.app.getOverview`, { headers: { Authorization: `Bearer ${config.AUTH_TOKEN}` }, }); expect(overviewRes.status).toBe(200); const overview = (await overviewRes.json()) as { replication: { trackedDids: string[] } }; expect(overview.replication.trackedDids.length).toBeGreaterThanOrEqual(1); }, 15_000); it("graceful shutdown completes cleanly", async () => { tmpDir = mkdtempSync(join(tmpdir(), "server-startup-")); const config = testConfig(tmpDir); handle = await startServer(config); // Verify server is running const res = await fetch(`${handle.url}/xrpc/_health`); expect(res.status).toBe(200); // Close and verify it doesn't throw await handle.close(); handle = undefined; // Prevent double-close in afterEach // Verify server is no longer responding await expect( fetch(`http://localhost:${0}/xrpc/_health`).then((r) => r.status), ).rejects.toThrow(); }); });