JavaScript testing
The @calmlens/sdk/testing subpath provides an offline in-memory mock environment with MockCalmLensClient, InMemoryMockDatabaseStorage, InMemoryMockImageStorage, mock API key generation, webhook delivery builders and generateMediaTestCode.
The main @calmlens/sdk entry point never imports testing utilities. Keep /testing imports inside test files so mock storage and generators are not bundled into production builds.
Basic setup
import { MockCalmLensClient, createMockApiKey } from "@calmlens/sdk/testing";
const client = new MockCalmLensClient({
apiKey: createMockApiKey(),
projectId: "123e4567-e89b-12d3-a456-426614174000",
});createMockApiKey() mints mock keys locally without contacting CalmLens.
Mock client options (mockOptions)
Configure execution behavior, deterministic clocks, or custom storage drivers via mockOptions:
const client = new MockCalmLensClient({
apiKey: createMockApiKey(),
projectId: "123e4567-e89b-12d3-a456-426614174000",
mockOptions: {
// Generate deterministic IDs for snapshot testing
createId: () => "00000000-0000-0000-0000-000000000001",
// Fix timestamps for reproducible assertions
now: () => 1_700_000_000_000,
// Status assigned to asynchronous uploads (defaults to "pending")
queuedStatus: "pending",
// Status assigned to synchronous moderate calls (defaults to "approved")
analyzedStatus: "approved",
// Base URL for mock signed URLs
signedUrlBaseUrl: "mock://calmlens",
},
});The mock accepts the same options as CalmLensClient (baseUrl, cdnBaseUrl, timeoutMs and so on) and rejects invalid ones the same way. cdn.getAssetUrl uses cdnBaseUrl, which defaults to mock://calmlens/v1.
Behaviour that matches the API
The mock reproduces the API where tests usually depend on it:
- Errors carry the API's
code, HTTPstatus, arequestIdand responseheaders, soerror.isNotFound()and status checks work as they do against CalmLens. assets.listappliesordering,folderIdand simplefilterexpressions: comparisons (==,!=,<,<=,>,>=,=in=,=out=) joined by;(and) or,(or). A filter it can't evaluate throwssdk/invalid-argumentinstead of returning the wrong assets.assets.iteratepages through the results.assets.searchmatches the query against names, IDs, external IDs, descriptions and tags.- A second asset with the same
externalIdfails withasset/duplicate-external-id. - Multipart parts must arrive in order (
upload/invalid-part-numberwithdetails.expectedPartNumber), the queue mode chosen at the start applies to the final part, and parts after it fail withupload/not-open. - Reusing an
idempotencyKeyreturns the first result; reusing it for a different request fails withidempotency/key-reused.
Events
events.list starts empty. Add events with client.enqueueEvent, then read them with a cursor as you would from the API:
const { cursor } = await client.events.list(); // a cursor for "now"
client.enqueueEvent({ type: "asset.classified", assetId: asset.id });
const page = await client.events.list({ after: cursor });
expect(page.events.map((event) => event.type)).toEqual(["asset.classified"]);Testing webhook handlers
toWebhookEventPayload turns an event into a delivery body, and createWebhookHeaders signs it the way CalmLens does, so your handler's parseWebhookEvent or verifyWebhookSignature call runs unchanged:
import { createWebhookHeaders, toWebhookEventPayload } from "@calmlens/sdk/testing";
const event = client.enqueueEvent({ type: "asset.classified", assetId: asset.id });
const body = JSON.stringify(toWebhookEventPayload(event));
const headers = await createWebhookHeaders({ secret, body, deliveryId: "delivery-1" });
const response = await handler(new Request("http://localhost/webhooks", { method: "POST", body, headers }));Recorded calls
Every call is recorded in client.calls as { operation, options }: the method path, such as "text.moderate" or "assets.retrieve", and the options object it received. client.callsTo(operation) returns the calls to one method:
await client.text.moderate({ text: "hello", externalId: "comment-1" });
expect(client.callsTo("text.moderate")).toEqual([
{ operation: "text.moderate", options: { text: "hello", externalId: "comment-1" } },
]);Simulating network failures and timeouts
client.failNextRequest() makes the next call throw the CalmLensError that CalmLensClient throws when a request cannot complete (code: "sdk/transport-error", with the network error as cause). Pass "timeout" to throw sdk/timeout instead, or any Error to throw it as is. { operation } limits the failure to one method. Each queued failure is used once:
client.failNextRequest("timeout", { operation: "text.moderate" });
await expect(client.text.moderate({ text: "hello" })).rejects.toMatchObject({
code: "sdk/timeout",
});
await expect(client.text.moderate({ text: "hello" })).resolves.toMatchObject({
status: "approved",
});Inspecting in-memory storage
MockCalmLensClient exposes its internal storage instances as client.databaseStorage and client.imageStorage. Use these in assertions to verify saved records without extra API calls:
// Inspect database records directly
const asset = await client.databaseStorage.getAsset(createdAsset.id);
const allAssets = await client.databaseStorage.listAssets(projectId);
const folder = await client.databaseStorage.getFolder(createdFolder.id);
const allFolders = await client.databaseStorage.listFolders(projectId);
// Inspect raw binary upload content and multipart parts
const storedContent = await client.imageStorage.getContent(createdAsset.id);
console.log(storedContent?.type); // "image/png"
console.log(storedContent?.parts.get(1)); // Uint8Array of bytesCustomising test storage
You can inject custom storage implementations implementing MockDatabaseStorage or MockImageStorage to track method calls or simulate database failures:
import {
MockCalmLensClient,
InMemoryMockDatabaseStorage,
InMemoryMockImageStorage,
createMockApiKey,
} from "@calmlens/sdk/testing";
const databaseStorage = new InMemoryMockDatabaseStorage();
const imageStorage = new InMemoryMockImageStorage();
const client = new MockCalmLensClient({
apiKey: createMockApiKey(),
projectId: "123e4567-e89b-12d3-a456-426614174000",
mockOptions: { databaseStorage, imageStorage },
});Simulating CDN serving and access control
MockCalmLensClient.cdn.serve fetches raw bytes directly from in-memory storage and checks access control without network calls. Like the CDN, it returns Content-Type, Content-Length, ETag and X-Request-Id headers, and a JSON error body with 404 or 403:
// Public assets can be served directly
const publicAsset = await client.assets.create({
name: "public.png",
content: { base64: pngBase64, type: "image/png" },
visibility: "public",
});
const response = await client.cdn.serve({ assetIdentifier: publicAsset.id });
expect(response.status).toBe(200);
// A key without assets:download cannot read private assets directly
const privateAsset = await client.assets.create({
name: "secret.png",
content: { base64: pngBase64, type: "image/png" },
visibility: "private",
});
const privateResponse = await client.cdn.serve({ assetIdentifier: privateAsset.id });
expect(privateResponse.status).toBe(403);