import { existsSync, readdirSync, readFileSync } from 'node:fs'; import { dirname, join, resolve, sep } from 'node:path'; /** * Guard for the handler contract (Review Focus 2): a domain event handler * runs inside its delivery transaction and may be repeated after a crash, * so it only writes to the database. Network work belongs to an outbox of * its own (calendar sync, WebhooksService.enqueueDeliveries) that a worker * loop drains. A static source scan: * - every @OnDomainEvent handler lives in a `*.handlers.ts` file, and * - a handler file imports no network client: no http/https/fetch library, * no `fetch(` call, nothing from graph-client or webhook-delivery, and no * src module that itself makes network calls (a fetch wrapper). */ const SRC = join(__dirname, '..'); const DECORATOR_FILE = join( SRC, 'domain-events', 'on-domain-event.decorator.ts', ); interface SourceFile { path: string; source: string; } const NETWORK_PACKAGES = new Set([ 'http', 'https', 'http2', 'net', 'tls', 'dgram', 'undici', 'axios', 'node-fetch', 'cross-fetch', 'got', 'ky', 'superagent', ]); const NETWORK_MODULE_NAME = /graph-client|webhook-delivery/; const FETCH_CALL = /\bfetch\s*\(/; const IMPORT_SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\s*\(\s*)['"]([^'"]+)['"]/g; function importsOf(source: string): string[] { return [...source.matchAll(IMPORT_SPECIFIER)].map((match) => match[1]); } function isNetworkPackage(specifier: string): boolean { return NETWORK_PACKAGES.has(specifier.replace(/^node:/, '')); } /** A module that talks to the network itself. */ function makesNetworkCalls(file: SourceFile): boolean { return ( FETCH_CALL.test(file.source) || importsOf(file.source).some(isNetworkPackage) ); } function resolveImport(from: string, specifier: string): string | undefined { if (!specifier.startsWith('.')) { return undefined; } const base = resolve(dirname(from), specifier); return [`${base}.ts`, join(base, 'index.ts')].find(existsSync) ?? base; } /** Files declaring @OnDomainEvent handlers outside a `*.handlers.ts` file. */ function misplacedHandlerFiles(files: readonly SourceFile[]): string[] { return files .filter( (file) => file.path !== DECORATOR_FILE && !file.path.endsWith('.spec.ts') && file.source.includes('@OnDomainEvent(') && !file.path.endsWith('.handlers.ts'), ) .map((file) => file.path); } /** What a handler file must not use; empty when it keeps the contract. */ function networkViolations( handlerFile: SourceFile, networkModules: ReadonlySet, ): string[] { const violations = importsOf(handlerFile.source).filter((specifier) => { const resolved = resolveImport(handlerFile.path, specifier); return ( isNetworkPackage(specifier) || NETWORK_MODULE_NAME.test(specifier) || (resolved !== undefined && networkModules.has(resolved)) ); }); return FETCH_CALL.test(handlerFile.source) ? [...violations, 'fetch('] : violations; } function sourceFiles(): SourceFile[] { return readdirSync(SRC, { recursive: true, encoding: 'utf8' }) .filter((file) => file.endsWith('.ts')) .map((file) => join(SRC, file)) .map((path) => ({ path, source: readFileSync(path, 'utf8') })); } function networkModulesOf(files: readonly SourceFile[]): Set { return new Set( files .filter((file) => !file.path.endsWith('.spec.ts')) .filter(makesNetworkCalls) .map((file) => file.path), ); } describe('domain event handler contract', () => { const files = sourceFiles(); const networkModules = networkModulesOf(files); it('every @OnDomainEvent handler lives in a *.handlers.ts file', () => { expect(misplacedHandlerFiles(files)).toEqual([]); }); it('handler files never import network clients', () => { for (const file of files.filter((f) => f.path.endsWith('.handlers.ts'))) { expect({ file: file.path, violations: networkViolations(file, networkModules), }).toEqual({ file: file.path, violations: [], }); } }); it('knows the network modules of the code base (fetch wrappers included)', () => { const relative = [...networkModules].map((path) => path .slice(SRC.length + 1) .split(sep) .join('/'), ); expect(relative).toEqual( expect.arrayContaining([ 'calendar-sync/graph-client.service.ts', 'webhooks/webhook-delivery.ts', 'weather/weather.service.ts', 'common/password/password-policy.ts', ]), ); expect(relative).not.toContain('webhooks/webhooks.service.ts'); }); describe('with planted violations', () => { const handlerPath = join(SRC, 'colonies', 'colonies.handlers.ts'); it('flags a handler declared outside a *.handlers.ts file', () => { const planted = { path: join(SRC, 'colonies', 'colonies.service.ts'), source: "@OnDomainEvent('TaskDue', 'colonies.due')\n", }; expect(misplacedHandlerFiles([planted])).toEqual([planted.path]); expect( misplacedHandlerFiles([ { path: handlerPath, source: planted.source }, { path: `${planted.path.slice(0, -3)}.spec.ts`, source: planted.source, }, { path: DECORATOR_FILE, source: planted.source }, ]), ).toEqual([]); }); it.each([ "import https from 'https';", "import { request } from 'http';", "import { get } from 'node:https';", "const http = require('node:http');", "import { fetch as undiciFetch } from 'undici';", "import axios from 'axios';", 'const response = await fetch(url);', "import { GraphClientService } from '../calendar-sync/graph-client.service';", "import { postWebhook } from '../webhooks/webhook-delivery';", "import { WebhookDeliveryWorker } from '../webhooks/webhook-delivery.worker';", "import { WeatherService } from '../weather/weather.service';", "const { checkPassword } = await import('../common/password/password-policy');", ])('flags %s', (line) => { expect( networkViolations({ path: handlerPath, source: line }, networkModules), ).not.toEqual([]); }); it.each([ "import { WebhooksService } from '../webhooks/webhooks.service';", "import { Prisma } from '../../generated/prisma';", "import type { DomainEventEnvelope } from '../domain-events/domain-event-types';", 'const prefetched = await tx.task.findMany();', "const scheme = 'https';", ])('lets a database-only line pass: %s', (line) => { expect( networkViolations({ path: handlerPath, source: line }, networkModules), ).toEqual([]); }); }); });