# API Server Backend API server for the publisher dashboard. ## Development ```bash # Start development server bun run dev # Type check bun run typecheck # Lint bun run lint ``` ## Testing ### Running Tests ```bash # Run e2e tests (requires PostgreSQL) bun run test:e2e # Run unit tests bun run test:unit ``` ### E2E Test Setup E2E tests use a real PostgreSQL database. The test infrastructure handles: 1. **Database creation** - Creates the test database if it doesn't exist 2. **Migrations** - Runs dbmate migrations before tests 3. **Cleanup** - Truncates tables between test files #### Environment Set `TEST_DATABASE_URL` in your environment (devenv.nix sets this automatically): ```bash TEST_DATABASE_URL=postgres://reviq:reviq@localhost/reviq-dashboard_test ``` ### Writing E2E Tests Create test files in `src/__tests__/e2e/`. E2E tests should call router handlers directly using the `call` function from `@orpc/server`. #### Basic Setup ```typescript import { describe, test, expect, beforeAll, afterAll } from "bun:test"; import type { Kysely } from "kysely"; import type { Database } from "@reviq/db-schema"; import { call } from "@orpc/server"; import { router } from "../../router.js"; import type { AuthenticatedContext } from "../../context.js"; import { createTestDb, createTestUser, destroyTestDb, runMigrations, truncateAllTables, } from "../helpers/test-db.js"; let db: Kysely; beforeAll(async () => { await runMigrations(); db = createTestDb(); }); afterAll(async () => { await destroyTestDb(db); }); describe("my feature", () => { beforeAll(async () => { await truncateAllTables(db); }); test("does something", async () => { const user = await createTestUser(db, { email: "test@example.com" }); expect(user.id).toBeGreaterThan(0); }); }); ``` #### Calling Router Handlers Use `call()` from `@orpc/server` to invoke router handlers directly with the appropriate context: ```typescript import { call } from "@orpc/server"; import { router } from "../../router.js"; import type { AuthenticatedContext } from "../../context.js"; // Create a context object for authenticated endpoints function createAuthContext(userId: number, email: string): AuthenticatedContext { return { db, origin: "http://localhost:3000", allowedOrigins: ["http://localhost:3000"], rpName: "Test App", user: { id: userId, email, displayName: null, emailVerifiedAt: null, isSuperuser: false, }, session: { id: 1, trustedMode: false, createdAt: new Date(), }, }; } test("lists passkeys via router", async () => { const user = await createTestUser(db, { email: "test@example.com" }); const ctx = createAuthContext(user.id, user.email); // Call router handler directly const passkeys = await call(router.me.passkeys.list, undefined, { context: ctx, }); expect(passkeys).toHaveLength(0); }); test("renames passkey via router", async () => { const user = await createTestUser(db, { email: "test@example.com" }); const ctx = createAuthContext(user.id, user.email); // Call with input await call( router.me.passkeys.rename, { passkeyId: 1, name: "My Key" }, { context: ctx } ); }); test("handles errors from router", async () => { const user = await createTestUser(db, { email: "test@example.com" }); const ctx = createAuthContext(user.id, user.email); // Expect router to throw await expect( call(router.me.passkeys.delete, { passkeyId: 999 }, { context: ctx }) ).rejects.toThrow(); }); ``` #### Context Types Different endpoints require different context types: | Context Type | Use Case | |-------------|----------| | `APIContext` | Public endpoints (no auth required) | | `AuthenticatedContext` | Protected endpoints (requires user session) | | `LoginRequestContext` | Login flow endpoints | See `src/context.ts` for the full interface definitions. ### Test Helpers #### `test-db.ts` | Function | Description | |----------|-------------| | `createTestDb()` | Creates a Kysely connection to the test database | | `runMigrations()` | Runs dbmate migrations (creates DB if needed) | | `truncateAllTables(db)` | Truncates all tables with CASCADE | | `createTestUser(db, overrides?)` | Creates a test user with optional overrides | | `destroyTestDb(db)` | Closes the database connection | #### `test-constants.ts` Test constants for RP configuration and known values: ```typescript import { TEST_RP, KNOWN_AAGUIDS } from "../helpers/test-constants.js"; ``` #### VirtualAuthenticator For WebAuthn testing, generates real cryptographic credentials. Available from the `@reviq/virtual-authenticator` package: ```typescript import { VirtualAuthenticator } from "@reviq/virtual-authenticator"; const authenticator = new VirtualAuthenticator({ origin: "http://localhost:3000", }); // Registration const regResponse = await authenticator.createCredential(regOptions); // Authentication const authResponse = await authenticator.getAssertion(authOptions); ``` ### Test Isolation - Tests run serially (`--no-parallel`) to avoid database conflicts - Each test file should call `truncateAllTables()` in `beforeAll` - Use unique emails/identifiers per test to avoid collisions within a file