- Resolve merge conflicts in auth.test.ts, me.test.ts, db/schema.sql - Merge new loginRequestMiddleware tests into auth.test.ts describeE2E wrapper - Merge new authMiddleware tests into me.test.ts describeE2E wrapper - Add me.apiTokens and me.invites tests in separate describeE2E block - Migrate admin.test.ts to use describeE2E and @reviq/test-helpers - Migrate orgs.test.ts to use describeE2E and @reviq/test-helpers All e2e tests now properly use the describeE2E helper which enables SKIP_DB_TESTS environment variable support. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
API Server
Backend API server for the publisher dashboard.
Development
# Start development server
bun run dev
# Type check
bun run typecheck
# Lint
bun run lint
Testing
Running Tests
# 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:
- Database creation - Creates the test database if it doesn't exist
- Migrations - Runs dbmate migrations before tests
- Cleanup - Truncates tables between test files
Environment
Set TEST_DATABASE_URL in your environment (devenv.nix sets this automatically):
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
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<Database>;
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:
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:
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:
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()inbeforeAll - Use unique emails/identifiers per test to avoid collisions within a file