Skip to content

Test an App

Use tablewalk/testing from server-side tests. The helpers work with Node’s test runner, Vitest and Playwright; they do not modify your running App.

import { test } from 'node:test';
import {
createTestWorkspace, startTestApp, assertAuthorizationMatrix,
} from 'tablewalk/testing';
test('reader access', async t => {
const workspace = await createTestWorkspace();
t.after(() => workspace.close());
const app = await workspace.copy('./app', 'app');
const database = await workspace.database('business.db', db => {
db.exec('CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)');
// Run your migrations and deterministic seeds here.
});
const host = await startTestApp({ workspace, directory: app, database });
await assertAuthorizationMatrix([
{ name: 'ready', client: host.client(), path: '/readyz', status: 200 },
]);
});

A workspace owns its temporary directory and registered cleanup functions. Use workspace.own(() => service.close()) for dependencies, or await using for the workspace. Cleanup runs in reverse registration order, continues after failures, and reports them together. database always closes its SQLite handle, including when migration or seed code throws. write creates text or JSON fixtures; copy copies an authored App. Paths cannot escape the workspace.

startTestApp launches the production CLI in a separate process, waits for its startup readiness signal, and stops it during workspace cleanup. It chooses an OS-assigned port; authenticated hosts require a stable nonzero port, so startup retries a reservation collision. config, args and environment allow the same connection configuration, policy, commands and jobs used by deployment. Inherited secrets are excluded; HOME and XDG configuration belong to the test. Explicit App paths still mean what they say: change absolute database, auth and job paths in a copied App to disposable paths before starting it.

startTestSuite accepts a suite configuration object, writes it under the workspace and assigns its port. Relative paths resolve under that workspace. Its grant({ appId, subjectId, roles, expectedRevision }) uses the production shared-identity grant maintenance path. Keep suite environment mappings and explicit test secrets in its environment. The suite retains production capability checks; unsupported suite deployment features are not bypassed.

Each host.client() has its own cookie jar. signUp, signIn, signOut use the real email authentication endpoints; pass a custom endpoint path for mounted Apps. request(path, { method, json, headers }) supplies the origin and cookies, keeps redirects explicit and refuses another origin. It preserves cookie paths so independent App sessions do not overwrite each other.

Use assertAuthorizationMatrix for anonymous, reader, writer, disabled account, wrong tenant and wrong App cases. Every row names a client, path and expected status (or statuses). request supports command bodies and headers; inspect receives the response for assertions about hidden fields, row membership and refusal codes. All rows run and failures retain the case name and underlying assertion. Include a deliberate wrong expectation with assert.rejects once to prove the negative checks really detect a defect.

Seed tenant memberships through the App’s maintenance API. App grants and tenant memberships are separate authorization boundaries. Check direct API/command requests as well as navigation, revocation, logout, webhook signatures and cross-tenant identifiers. A test-only HTTP client is not a complete browser cookie implementation; use Playwright for SameSite, browser origin and navigation tests.

validateTestApp(directory) loads an authored App through the production loader and authority-lock validation. It does not replace database-bound --check-app. createTestClock(initial) provides now, date, advance and reset for your own business functions. Inject this clock into business logic; production command receipts, authorization and transactions should be tested through the HTTP host.

The repository’s examples/testing scaffold includes a Node test and a Playwright fixture that own their host lifecycle, desktop/mobile projects, semantic locators, and traces/screenshots on failure. No private tablewalk imports are needed. Extend the fixture with your App’s sign-in, writes, validation and shared suite journey. Run fast unit/API tests while editing, then browser projects before release:

Terminal window
node --import tsx --test tests/*.test.ts
npx playwright test --config playwright.config.ts

In CI install the pinned dependencies and browser once, run the tests without blind retries, report durations and retain the Playwright report and test-results folder on failure. Keep provider contract checks separate: a simulated provider cannot prove that a live provider still accepts your requests. An optional security scanner may target the disposable loopback host using test accounts; passing a scanner does not establish that your authorization policy is correct.

App test subprocesses and validation/account/grant maintenance subprocesses install a TCP guard before startup. Node fetch, http, https and TCP database clients may connect to loopback by default; an attempted other host fails the request and the test’s teardown, even if App code catches the connection error. For a deliberate contract test, pass allowedHosts to the host, or to validateTestApp(directory, { allowedHosts }). Unix sockets are denied. This is a test boundary for Node TCP clients, not an operating-system sandbox: native addons, child processes, UDP and DNS require an external network sandbox when those are part of your integration.

The same examples directory contains a real Keystone acceptance fixture. It copies the Lender and Dealer Apps into an ESM workspace, selects their shared menu, seeds a uniquely owned PostgreSQL database, and tests pending credit checks, signed callbacks, durable deduplication, the resulting underwriting score and cross-App denial. Its browser journey signs into the actual Lender App. Set TABLEWALK_TEST_PG to a local database administrator URL and follow the example’s README. host.account({ name, email, password }) provisions a shared identity account explicitly for invite-only Apps; grants remain a separate operation.