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, HTTP status, a requestId and response headers, so error.isNotFound() and status checks work as they do against CalmLens.
  • assets.list applies ordering, folderId and simple filter expressions: comparisons (==, !=, <, <=, >, >=, =in=, =out=) joined by ; (and) or , (or). A filter it can't evaluate throws sdk/invalid-argument instead of returning the wrong assets. assets.iterate pages through the results.
  • assets.search matches the query against names, IDs, external IDs, descriptions and tags.
  • A second asset with the same externalId fails with asset/duplicate-external-id.
  • Multipart parts must arrive in order (upload/invalid-part-number with details.expectedPartNumber), the queue mode chosen at the start applies to the final part, and parts after it fail with upload/not-open.
  • Reusing an idempotencyKey returns the first result; reusing it for a different request fails with idempotency/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 bytes

Customising 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);

Signing in

Signing out

Cookies on CalmLens

We use essential cookies to keep you signed in and the site secure. With your permission, we'll also use functional cookies to remember preferences such as your layout and documentation language, and to offer Google One Tap sign-in. We don't use analytics or advertising cookies. Manage cookies