@fabricorg/experiments-test-harness
v0.0.2
Published
Hermetic test harness: pglite + Better Auth + migrations for integration tests.
Downloads
52
Readme
@fabricorg/experiments-test-harness
Hermetic test bootstrapping for Fabric Experiments.
What this harness covers
- Schema migrations — applies the Postgres-compatible SQL migrations in
infra/neon/migrations/*.sqlagainst an in-process Postgres-shaped runtime (@electric-sql/pglite). The directory keeps its historical name, but the target is standard Postgres; Neon is one hosted option. PlatformRuntime— action invocation againstMemoryStore. Swap in a Postgres-backed store once Better Auth's schema is materialized.- Fast bootstrap —
createHarness({ skipAuth: true })returns in ≤ 200 ms and is the right path for migration + runtime + workflow unit tests.
What this harness does NOT cover
Better Auth's session, OAuth, magic-link, organization, or apiKey flows are not exercised against pglite. Better Auth uses Kysely under the hood, and a Kysely-on-pglite dialect is intentionally not on the M5+ roadmap. Maintaining a custom dialect would be ongoing tax with no real upside vs. spinning up a real Postgres for the auth-flow tests.
Fabric Experiments targets standard Postgres for hosted-control-plane state. Neon is the recommended hosted/serverless Postgres provider for Fabric-managed deployments and per-PR branches, but RDS Postgres, Cloud SQL Postgres, Supabase Postgres, Crunchy/Postgres operators, service-container Postgres, and local Testcontainers Postgres are valid targets as long as the migrations pass.
The Better Auth integration tests therefore run against:
- Local dev: Testcontainers Postgres (recipe below).
- Generic CI: a vanilla
postgresservice container. - Hosted CI option: a per-PR Neon branch via
neondatabase/create-branch-action.
Testcontainers recipe (local dev)
packages/test-harness/test/setup.testcontainers.ts:
import { GenericContainer, type StartedTestContainer } from 'testcontainers';
import { Pool } from 'pg';
let container: StartedTestContainer;
let pool: Pool;
export async function setupPostgres(): Promise<{ pool: Pool; url: string }> {
container = await new GenericContainer('postgres:16-alpine')
.withEnvironment({
POSTGRES_PASSWORD: 'test',
POSTGRES_USER: 'test',
POSTGRES_DB: 'fxtest',
})
.withExposedPorts(5432)
.start();
const url = `postgres://test:test@${container.getHost()}:${container.getMappedPort(5432)}/fxtest`;
pool = new Pool({ connectionString: url });
// Apply fx migrations (Better Auth applies its own schema via initial query).
for (const file of fxMigrationFiles()) {
await pool.query(readFileSync(file, 'utf8'));
}
return { pool, url };
}
export async function teardownPostgres(): Promise<void> {
await pool?.end();
await container?.stop();
}Then in your auth integration test:
import { createAuth } from '@fabricorg/experiments-auth';
import { setupPostgres, teardownPostgres } from '../setup.testcontainers';
let auth: ReturnType<typeof createAuth>;
beforeAll(async () => {
const { pool } = await setupPostgres();
auth = createAuth({
baseURL: 'http://test',
secret: 'test'.repeat(8),
database: { dialect: 'pg', pool } as never,
sendEmail: async () => {},
trustedOrigins: ['http://test'],
});
});
afterAll(teardownPostgres);
it('signs up + verifies + creates org', async () => {
// ... use auth.api.signUpEmail / verifyEmail / createOrganization
});CI recipe (vanilla Postgres service container)
The production CI job (integration-test in .github/workflows/ci.yml) uses a
vanilla postgres:16-alpine service container:
# .github/workflows/ci.yml (excerpt)
jobs:
integration-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: fabric_experiments
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/fabric_experiments
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.10.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm -r run build
- name: Apply migrations
run: node scripts/apply-migrations.mjs
- name: Run Better Auth integration tests against Postgres
run: pnpm --filter @fabricorg/experiments-api run test:integrationCI recipe (hosted Postgres via per-PR Neon branch)
# .github/workflows/ci.yml (excerpt)
- uses: neondatabase/create-branch-action@v5
id: create-branch
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
parent: main
branch_name: pr-${{ github.event.number }}
api_key: ${{ secrets.NEON_API_KEY }}
- name: Run integration tests against Neon branch
env:
DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
run: pnpm --filter @fabricorg/experiments-api test:integration
- if: always()
uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch: pr-${{ github.event.number }}
api_key: ${{ secrets.NEON_API_KEY }}Use the service-container path when you want provider-neutral Postgres coverage. Use the Neon branch path when you want hosted/serverless Postgres coverage close to Fabric-managed deployments. In both cases, the contract is standard Postgres: migrations must pass and Better Auth must exercise real session/API-key tables.
API surface
import { createHarness } from '@fabricorg/experiments-test-harness';
// Migration + runtime tests:
const h = await createHarness({ skipAuth: true });
// h.db — pglite instance
// h.runtime — PlatformRuntime backed by MemoryStore
// h.signInFake({ orgId, role }) → Principal
// await h.close()createHarness({ skipAuth: false }) is reserved for the M5 milestone when
Better Auth ships a real adapter we can drive in-process. Until then, the
Better Auth side of integration testing lives behind Testcontainers.
