Add central notification channels, rules, queue and delivery logs, fleet organization groups and tags, device discovery, the device action catalog, instance module reads, the log centre, connection settings and system maintenance as native /api/v1 routes backed by the existing secret store, audit trail and pinned upstream transport.
216 lines
8.4 KiB
TypeScript
216 lines
8.4 KiB
TypeScript
import { EventEmitter } from 'node:events';
|
|
import { lstat, mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises';
|
|
import { createServer } from 'node:http';
|
|
import { tmpdir } from 'node:os';
|
|
import { join, resolve } from 'node:path';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { createRuntimeConfig, GATEWAY_AUTH_HEADER } from './runtime-config.js';
|
|
import { buildApp } from './app.js';
|
|
import { createCanaryGateway, type CanaryGateway } from './canary-gateway.js';
|
|
import { createProductionReadiness } from './production-readiness.js';
|
|
import { defaultKeychainMetadataCheck } from './production-readiness.js';
|
|
import { readCanaryRuntimeOptions } from './canary-runtime.js';
|
|
import { installBoundedShutdown } from './shutdown.js';
|
|
import { secureTokenEqual } from './app.js';
|
|
import { openDatabase } from './infrastructure/database/database.js';
|
|
import { migrateDatabase } from './infrastructure/database/migrations.js';
|
|
|
|
const cleanup: Array<() => Promise<void>> = [];
|
|
afterEach(async () => {
|
|
await Promise.all(cleanup.splice(0).map((fn) => fn()));
|
|
});
|
|
const token = 'runtime-gateway-token-with-at-least-32-bytes';
|
|
|
|
describe('production runtime foundation', () => {
|
|
it('creates one deeply immutable fixed-listener config and validates before side effects', () => {
|
|
const root = resolve('/tmp/multi-simadmin-data');
|
|
const config = createRuntimeConfig({
|
|
MULTI_SIMADMIN_DATA_ROOT: root,
|
|
MULTI_SIMADMIN_DATABASE_PATH: join(root, 'db.sqlite'),
|
|
MULTI_SIMADMIN_GATEWAY_TOKEN: token,
|
|
MULTI_SIMADMIN_WEB_DIST: root,
|
|
API_HOST: '127.0.0.1',
|
|
API_PORT: '8790',
|
|
});
|
|
expect(config).toEqual({
|
|
api: { host: '127.0.0.1', port: 8790 },
|
|
dataRoot: root,
|
|
databasePath: join(root, 'db.sqlite'),
|
|
gatewayToken: token,
|
|
webDist: root,
|
|
});
|
|
expect(Object.isFrozen(config)).toBe(true);
|
|
expect(Object.isFrozen(config.api)).toBe(true);
|
|
expect(() =>
|
|
createRuntimeConfig({
|
|
MULTI_SIMADMIN_DATA_ROOT: root,
|
|
MULTI_SIMADMIN_DATABASE_PATH: '/tmp/escape.sqlite',
|
|
MULTI_SIMADMIN_GATEWAY_TOKEN: token,
|
|
MULTI_SIMADMIN_WEB_DIST: root,
|
|
}),
|
|
).toThrow(/data root/i);
|
|
expect(() =>
|
|
createRuntimeConfig({
|
|
MULTI_SIMADMIN_DATA_ROOT: root,
|
|
MULTI_SIMADMIN_DATABASE_PATH: join(root, 'db.sqlite'),
|
|
MULTI_SIMADMIN_GATEWAY_TOKEN: 'weak',
|
|
}),
|
|
).toThrow(/token/i);
|
|
});
|
|
|
|
it('rejects a database path whose existing symlink ancestor escapes before creating a DB', async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), 'runtime-containment-'));
|
|
cleanup.push(() => rm(directory, { recursive: true, force: true }));
|
|
const root = join(directory, 'data');
|
|
const outside = join(directory, 'outside');
|
|
await mkdir(root);
|
|
await mkdir(outside);
|
|
await symlink(outside, join(root, 'escape'));
|
|
const databasePath = join(root, 'escape', 'db.sqlite');
|
|
|
|
expect(() =>
|
|
createRuntimeConfig({
|
|
MULTI_SIMADMIN_DATA_ROOT: root,
|
|
MULTI_SIMADMIN_DATABASE_PATH: databasePath,
|
|
MULTI_SIMADMIN_GATEWAY_TOKEN: token,
|
|
MULTI_SIMADMIN_WEB_DIST: root,
|
|
}),
|
|
).toThrow(/data root|symlink/i);
|
|
await expect(lstat(join(outside, 'db.sqlite'))).rejects.toMatchObject({ code: 'ENOENT' });
|
|
});
|
|
|
|
it('reads and validates the canary gateway token from its environment', () => {
|
|
expect(
|
|
readCanaryRuntimeOptions({
|
|
MULTI_SIMADMIN_GATEWAY_TOKEN: token,
|
|
CANARY_DIST_DIR: '/tmp/dist',
|
|
CANARY_PORT: '8789',
|
|
CANARY_UPSTREAM_PORT: '8790',
|
|
}),
|
|
).toMatchObject({
|
|
gatewayToken: token,
|
|
port: 8789,
|
|
upstreamPort: 8790,
|
|
});
|
|
expect(() => readCanaryRuntimeOptions({ CANARY_DIST_DIR: '/tmp/dist' })).toThrow(/token/i);
|
|
expect(() =>
|
|
readCanaryRuntimeOptions({
|
|
CANARY_DIST_DIR: '/tmp/dist',
|
|
MULTI_SIMADMIN_GATEWAY_TOKEN: 'weak',
|
|
}),
|
|
).toThrow(/token/i);
|
|
});
|
|
|
|
it('always compares fixed-size token digests with timingSafeEqual', () => {
|
|
const compare = vi.fn(() => false);
|
|
expect(secureTokenEqual('x', token, compare)).toBe(false);
|
|
expect(compare).toHaveBeenCalledOnce();
|
|
const [actual, expected] = compare.mock.calls[0] as unknown as [Buffer, Buffer];
|
|
expect(actual).toHaveLength(32);
|
|
expect(expected).toHaveLength(32);
|
|
});
|
|
|
|
it('forces pluggable termination when graceful shutdown exceeds its deadline', async () => {
|
|
vi.useFakeTimers();
|
|
const target = new EventEmitter();
|
|
const terminate = vi.fn();
|
|
const stop = installBoundedShutdown(
|
|
target,
|
|
{ close: () => new Promise(() => undefined) },
|
|
{ timeoutMs: 25, terminate },
|
|
);
|
|
const stopping = stop();
|
|
await vi.advanceTimersByTimeAsync(25);
|
|
await stopping;
|
|
expect(terminate).toHaveBeenCalledOnce();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('default Keychain readiness checks only Darwin security executable metadata', async () => {
|
|
const stat = vi.fn(async () => ({ isFile: () => true, mode: 0o100755 }));
|
|
await expect(defaultKeychainMetadataCheck('darwin', stat)).resolves.toBe(true);
|
|
expect(stat).toHaveBeenCalledExactlyOnceWith('/usr/bin/security');
|
|
stat.mockClear();
|
|
await expect(defaultKeychainMetadataCheck('linux', stat)).resolves.toBe(false);
|
|
expect(stat).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('authenticates internal requests without reflecting credentials', async () => {
|
|
const app = buildApp({ gatewayToken: token });
|
|
cleanup.push(() => app.close());
|
|
expect((await app.inject({ url: '/healthz' })).statusCode).toBe(401);
|
|
expect(
|
|
(
|
|
await app.inject({
|
|
url: '/healthz',
|
|
headers: { [GATEWAY_AUTH_HEADER]: 'wrong-but-same-length-xxxxxxxxxxxxxxxxx' },
|
|
})
|
|
).statusCode,
|
|
).toBe(401);
|
|
const ok = await app.inject({ url: '/healthz', headers: { [GATEWAY_AUTH_HEADER]: token } });
|
|
expect(ok.statusCode).toBe(200);
|
|
expect(ok.payload).not.toContain(token);
|
|
});
|
|
|
|
it('readiness checks a live migrated DB and auth configuration without reading secrets', async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), 'runtime-ready-'));
|
|
cleanup.push(() => rm(directory, { recursive: true, force: true }));
|
|
const db = openDatabase(join(directory, 'db.sqlite'));
|
|
migrateDatabase(db);
|
|
const keychainMetadataCheck = vi.fn(async () => true);
|
|
const readiness = createProductionReadiness({
|
|
db,
|
|
gatewayToken: token,
|
|
keychainMetadataCheck,
|
|
assetsCheck: async () => true,
|
|
recoveryMarkersCheck: async () => 0,
|
|
upstreamGatewayCheck: async () => true,
|
|
});
|
|
expect(await readiness()).toEqual({
|
|
ready: true,
|
|
checks: {
|
|
database: 'ready',
|
|
migrations: 'ready',
|
|
gatewayAuth: 'ready',
|
|
keychain: 'ready',
|
|
pendingCredentials: 'ready',
|
|
assets: 'ready',
|
|
recoveryMarkers: 'ready',
|
|
upstreamGateway: 'ready',
|
|
},
|
|
});
|
|
expect(keychainMetadataCheck).toHaveBeenCalledOnce();
|
|
db.close();
|
|
expect((await readiness()).ready).toBe(false);
|
|
});
|
|
|
|
it('gateway replaces forged auth and only falls back for extensionless routes', async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), 'runtime-gateway-'));
|
|
await mkdir(join(directory, 'assets'));
|
|
await writeFile(join(directory, 'index.html'), '<main>SPA</main>');
|
|
cleanup.push(() => rm(directory, { recursive: true, force: true }));
|
|
let seen: string | string[] | undefined;
|
|
const upstream = createServer((request, response) => {
|
|
seen = request.headers[GATEWAY_AUTH_HEADER];
|
|
response.end('ok');
|
|
});
|
|
await new Promise<void>((done) => upstream.listen(0, '127.0.0.1', done));
|
|
cleanup.push(() => new Promise<void>((done) => upstream.close(() => done())));
|
|
const address = upstream.address();
|
|
if (!address || typeof address === 'string') throw new Error('address');
|
|
const gateway: CanaryGateway = createCanaryGateway({
|
|
distDir: directory,
|
|
port: 0,
|
|
upstreamPort: address.port,
|
|
gatewayToken: token,
|
|
});
|
|
await gateway.start();
|
|
cleanup.push(() => gateway.stop());
|
|
await fetch(`${gateway.origin}/healthz`, { headers: { [GATEWAY_AUTH_HEADER]: 'forged' } });
|
|
expect(seen).toBe(token);
|
|
expect((await fetch(`${gateway.origin}/assets/missing.js`)).status).toBe(404);
|
|
expect((await fetch(`${gateway.origin}/missing.css`)).status).toBe(404);
|
|
expect((await fetch(`${gateway.origin}/instances/one`)).status).toBe(200);
|
|
});
|
|
});
|