Files
publisher-dashboard/apps/api-server
igm 73ef3df01f Add pre-configured procedures and use them throughout codebase
- Add authedProcedure, superuserProcedure, loginRequestProcedure,
  orgMemberProcedure in base.ts
- Create procedures/me/_base.ts with meRoute = authedProcedure.me
- Update all me procedures to use meRoute.X.handler()
- Update auth/logout and auth/resend-verification to use authedProcedure
- Update all admin procedures to use superuserProcedure
- Update all orgs procedures to use authedProcedure

This reduces boilerplate and makes middleware usage consistent.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 17:57:15 +08:00
..

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:

  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):

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() in beforeAll
  • Use unique emails/identifiers per test to avoid collisions within a file