@sinequa/atomic

Testing

Testing code that uses the library by injecting fetch, storage and a logger — with no module mocks at all.

The library takes its fetch, its storage and its logger as configuration. So testing code that calls it needs no module interception: build a client with test doubles, and it behaves exactly as it does in production minus the network.

the whole idea
const client = createAtomicClient({
  app: "test",
  storagePrefix: "test:",
  auth: { fetch: fakeFetch, storage: { session: memory(), local: memory() } },
});

Why this matters more than convenience

A module mock replaces the library, so the test passes whatever the library does. Injection keeps the real code path — the headers it builds, the token it stores, the recovery it attempts — and only replaces the boundary. What the test exercises is what production runs.

A test harness

One client per test, no shared state
src/test/harness.ts
import { LogLevel, configureLogger, createAtomicClient } from "@sinequa/atomic";
import type { AtomicClient, TokenStorage } from "@sinequa/atomic";

/** An in-memory TokenStorage — what the library falls back to without a window. */
export function memoryStorage(): TokenStorage {
  const entries = new Map<string, string>();

  return {
    get: (key) => entries.get(key) ?? null,
    set: (key, value) => {
      entries.set(key, value);
    },
    remove: (key) => {
      entries.delete(key);
    },
  };
}

export type Recorded = { url: string; method: string; body?: string };

/**
 * A fetch that answers from a table of canned responses and records every call.
 * The signature is the platform's, so the library uses it unchanged.
 */
export function stubFetch(routes: Record<string, unknown>) {
  const calls: Recorded[] = [];

  const fetch: typeof globalThis.fetch = async (input, init) => {
    const url = String(input);
    calls.push({
      url,
      method: init?.method ?? "GET",
      body: typeof init?.body === "string" ? init.body : undefined,
    });

    const match = Object.keys(routes).find((path) => url.includes(path));

    if (!match) {
      // A test should fail loudly on an unexpected call, not silently 404.
      return new Response(JSON.stringify({ errorMessage: `no stub for ${url}` }), {
        status: 500,
        headers: { "content-type": "application/json" },
      });
    }

    return new Response(JSON.stringify(routes[match]), {
      status: 200,
      headers: { "content-type": "application/json" },
    });
  };

  return { fetch, calls };
}

/** A fresh client per test: its session, storage and events are its own. */
export function testClient(routes: Record<string, unknown>): {
  client: AtomicClient;
  calls: Recorded[];
} {
  const { fetch, calls } = stubFetch(routes);

  configureLogger({ level: LogLevel.NONE });

  const client = createAtomicClient({
    app: "test",
    backendUrl: "https://server.test",
    // A distinct prefix, so tests running in one process cannot see each other.
    storagePrefix: `test-${calls.length}:`,
    auth: {
      fetch,
      storage: { session: memoryStorage(), local: memoryStorage() },
      // Silence the library's own diagnostics without touching the console.
      logger: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
    },
  });

  return { client, calls };
}

What each injection point buys

Prop

Type

Testing what the library does, not just what it returns

Because the request path is real, a test can assert on things a mock would have hidden:

what actually went on the wire
const { client, calls } = testClient({ "api/v1/query": { records: [], rowCount: 0, aggregations: [] } });

await client.api.query.search({ name: "<your-query>", text: "hello" });

// The body carries app, locale, noUserOverride and noAutoAuthentication.
const body = JSON.parse(calls[0].body ?? "{}");

That is how the library's own suite verifies the auth flows — the sign-in sequence, the 401 recovery, the deliberate sign-out window — without a single module mock.

Pitfalls

What's next

On this page