Skip to content

Documentation

API Unit Test Helpers

Shared createMockRequestEvent + invokeApi for dispatcher unit tests — thin, not a parallel HTTP client.

7/15/2026
5 min read Edit on GitHub

SveltyCMS unit tests that exercise the catch-all API dispatcher should use the shared helpers in tests/unit/utils/mock-event.ts instead of inventing a new createMockEvent per file.

Note

An experimental full createApiTester client was prototyped and removed. Measured unit→unit timings were ~parity with ad-hoc mocks; first-case cost is dominated by transforming api/[...path]/+server, not by the event wrapper. A heavier client did not buy enough speed or fidelity to justify the surface area. Prefer these thin helpers + black-box HTTP for real integration.


What to use

Helper File Role
createMockRequestEvent(opts) tests/unit/utils/mock-event.ts Build a consistent RequestEvent
callApiDispatcher(method, event) same Call production GET/POST/… from +server
invokeApi(method, opts) same One-liner: build event + dispatch
expectApi(method, opts, status) same invokeApi + status (+ JSON body)
invokeGraphql(query, vars?, opts?) same Thin POST /api/graphql
mockFormData(entries) same Multipart bag for media/DAM unit tests
runRbacMatrix(rows) tests/unit/utils/rbac-matrix.ts Table-driven RBAC statuses
Fixtures @tests/harness USERS, ROLES, PRIMARY_TENANT

Options that matter for security tests

import { invokeApi, expectApi, invokeGraphql, mockFormData } from "../utils/mock-event";
import { runRbacMatrix } from "../utils/rbac-matrix";

// Unauthenticated
await expectApi("POST", { path: "ai/chat", body: { userMessage: "x" }, user: null }, 401);

// Missing tenant (multi-tenant)
await expectApi(
  "GET",
  { path: "collections", user: admin, tenantId: null, bypass: true },
  [400, 403],
);

// GraphQL (still unit-layer; real GraphQL contracts live in integration)
await invokeGraphql("{ __typename }", {}, { user: null, bypass: false });

// Media multipart
const res = await invokeApi("POST", {
  path: "media/process",
  formData: mockFormData({ processType: "save", files: [file] }),
  user: admin,
});

// RBAC table (bypass false = real ENDPOINT_PERMISSIONS)
await runRbacMatrix([
  {
    name: "editor denied write",
    method: "POST",
    path: "collections/posts",
    body: { title: "x" },
    user: editor,
    roles: [{ _id: "editor", name: "Editor", isAdmin: false, permissions: [] }],
    expectedStatus: 403,
  },
]);
  • user: null — unauthenticated (do not omit; omit defaults to an admin-like user).
  • tenantId: null — explicit missing tenant (uses "tenantId" in options, not ??).
  • bypass — default true sets locals.__testBypass. Use false for RBAC matrices.
  • Role matching: hasPermissionWithRoles matches role._id === user.role or display name "Editor" for user.role === "editor".

Reference suites

  • tests/unit/api/user.test.tsinvokeApi
  • tests/unit/api/ai-security.test.ts — auth / tenant edges
  • tests/unit/api/collections.test.ts — shared createMockRequestEvent
  • tests/unit/api/auth-2fa.test.ts — thin wrapper over shared event factory
  • tests/unit/api/media-security-critical.test.tsformData + callApiDispatcher
  • tests/unit/api/token.test.ts — shared event factory
  • tests/unit/api/dispatcher-security-matrix.test.ts — fail-closed RBAC, multi-tenant, hot namespaces
  • tests/unit/api/namespace-ownership.test.ts — every catch-all namespace has a test owner
  • tests/unit/api/graphql-security.test.ts — depth/aliases + dispatcher 401 gate
  • tests/unit/graphql/multi-tenancy-isolation.test.ts — resolver tenant isolation

Migrating remaining tests/unit/api/*

Do opportunistically when touching a file:

  1. Replace local createMockEvent with createMockRequestEvent / invokeApi
  2. Prefer real apiHandler when asserting HTTP status codes
  3. Keep file-specific vi.mock for db/services
  4. Update NAMESPACE_OWNERS only when adding a new API namespace

Security matrix (unit)

dispatcher-security-matrix.test.ts exercises the real catch-all +server (apiHandler not unwrapped), including table-driven rows via runRbacMatrix.

Case Expected
Unknown namespace (admin) 404
SCIM as non-admin (bypass: false) 403
Unauthenticated collections 401
Multi-tenant collections without tenantId 4xx (not 200)
Editor without collections:write 403 on POST
Editor with collections:write not 401/403
Editor media DELETE / settings POST without perms 403

Patterns by layer (thin helpers only at unit)

Concern Unit (helpers) Integration E2E
RBAC / fail-closed runRbacMatrix + expectApi Cookie as editor/admin on real server UI affordances (hide Publish)
Collections CRUD Handler mocks + invokeApi for auth edges HTTP CRUD + multi-DB Builder → entry smoke
Media / DAM mockFormData + permission rows Upload/delete HTTP + permissions Gallery smoke only
GraphQL Rules + invokeGraphql 401 gate; resolver tenant isolation graphql.test.ts HTTP 401/200 Avoid GraphQL in Playwright
Widgets / Valibot Widget unit packages Entry create rejects invalid payloads Optional UI validation message

Do not use unit helpers inside multi-DB benchmarks or Playwright. CI multi-DB uses real HTTP.

When to use which layer

Goal Tool
Dispatcher / handler unit logic invokeApi / expectApi / runRbacMatrix
Full hooks + cookies + multi-DB Black-box API testing
Browser UX Playwright E2E

Related


Last Updated: 2026-07-15

testingapivitestunit
Was this page helpful?