import { beforeEach, describe, expect, test } from 'bun:test'; import type { DbClient } from '../db/client'; import { cleanupTestDatabase, setupTestDatabase } from '../test-utils/database'; import { listCapabilityBindings, recordCapabilityBinding, withBindingRecord, } from './capability-bindings'; describe('withBindingRecord', () => { test('reports once however many times the consumer calls', () => { let calls = 0; const iface = withBindingRecord({ a: () => 1, b: () => 2 }, () => { calls++; }); iface.a(); iface.a(); iface.b(); expect(calls).toBe(1); }); test('reports nothing when no method is called', () => { let calls = 0; withBindingRecord({ publishStaticSite: () => 1 }, () => { calls++; }); expect(calls).toBe(0); }); test('passes arguments and the return value through untouched', () => { const iface = withBindingRecord({ add: (a: number, b: number) => a + b }, () => {}); expect(iface.add(2, 3)).toBe(5); }); test('leaves a synchronous method synchronous', () => { // Unlike `wrapWithLogging`, which awaits and therefore makes every method // async. A capability's calling convention must not change because celilo // decided to watch it. const iface = withBindingRecord({ now: () => 'value' }, () => {}); expect(iface.now()).toBe('value'); }); test('passes non-function properties through, including the provider stamp', () => { const iface = withBindingRecord({ providerModuleId: 'caddy', go: () => 1 }, () => {}); expect(iface.providerModuleId).toBe('caddy'); }); }); describe('recordCapabilityBinding', () => { let db: DbClient; beforeEach(async () => { if (db) await cleanupTestDatabase(db); db = await setupTestDatabase(); db.$client.run( `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('consumer', 'consumer', '1.0.0', '/tmp/consumer', '{}')`, ); }); test('re-asserting the same binding keeps one row', () => { recordCapabilityBinding(db, 'consumer', 'external_web', 'cpanel-host'); recordCapabilityBinding(db, 'consumer', 'external_web', 'cpanel-host'); expect(listCapabilityBindings(db, 'consumer')).toHaveLength(1); }); test('a provider swap rewrites the row in place', () => { recordCapabilityBinding(db, 'consumer', 'public_web', 'caddy'); recordCapabilityBinding(db, 'consumer', 'public_web', 'caddy-internal'); const bindings = listCapabilityBindings(db, 'consumer'); expect(bindings).toHaveLength(1); expect(bindings[0].providerModuleId).toBe('caddy-internal'); }); test('boundAt is last-seen, not first-seen', () => { // A binding last exercised forty deploys ago has to be a queryable fact // rather than a silent lie, so the timestamp moves on every invocation. recordCapabilityBinding(db, 'consumer', 'idp', 'authentik'); db.$client.run( `UPDATE capability_bindings SET bound_at = 1000 WHERE consumer_module_id = 'consumer'`, ); recordCapabilityBinding(db, 'consumer', 'idp', 'authentik'); expect(listCapabilityBindings(db, 'consumer')[0].boundAt.getTime()).toBeGreaterThan( 1000 * 1000, ); }); test('bindings are scoped to the consumer that made them', () => { db.$client.run( `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('other', 'other', '1.0.0', '/tmp/other', '{}')`, ); recordCapabilityBinding(db, 'consumer', 'source_forge', 'forgejo'); recordCapabilityBinding(db, 'other', 'source_forge', 'forgejo'); expect(listCapabilityBindings(db, 'consumer')).toHaveLength(1); expect(listCapabilityBindings(db, 'other')).toHaveLength(1); }); });