API Unit Test Helpers
Shared createMockRequestEvent + invokeApi for dispatcher unit tests — thin, not a parallel HTTP client.
On this page
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.
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— defaulttruesetslocals.__testBypass. Usefalsefor RBAC matrices.- Role matching:
hasPermissionWithRolesmatchesrole._id === user.roleor display name"Editor"foruser.role === "editor".
Reference suites
tests/unit/api/user.test.ts—invokeApitests/unit/api/ai-security.test.ts— auth / tenant edgestests/unit/api/collections.test.ts— sharedcreateMockRequestEventtests/unit/api/auth-2fa.test.ts— thin wrapper over shared event factorytests/unit/api/media-security-critical.test.ts—formData+callApiDispatchertests/unit/api/token.test.ts— shared event factorytests/unit/api/dispatcher-security-matrix.test.ts— fail-closed RBAC, multi-tenant, hot namespacestests/unit/api/namespace-ownership.test.ts— every catch-all namespace has a test ownertests/unit/api/graphql-security.test.ts— depth/aliases + dispatcher 401 gatetests/unit/graphql/multi-tenancy-isolation.test.ts— resolver tenant isolation
Migrating remaining tests/unit/api/*
Do opportunistically when touching a file:
- Replace local
createMockEventwithcreateMockRequestEvent/invokeApi - Prefer real
apiHandlerwhen asserting HTTP status codes - Keep file-specific
vi.mockfor db/services - Update
NAMESPACE_OWNERSonly 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