/** * Unit tests for DiagnosticsService.collectDiagnostics: the shape of the * report, secret redaction (allowlist), Docker probe handling, and core-table * detection. Shared by the /api/diagnostics route and the diagnostics/validate * CLI commands, so these guarantees protect all three. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb'; let tmpDir: string; let collectDiagnostics: typeof import('../services/DiagnosticsService').collectDiagnostics; let DatabaseService: typeof import('../services/DatabaseService').DatabaseService; beforeAll(async () => { tmpDir = await setupTestDb(); ({ collectDiagnostics } = await import('../services/DiagnosticsService')); ({ DatabaseService } = await import('../services/DatabaseService')); }); afterAll(() => { cleanupTestDb(tmpDir); }); describe('collectDiagnostics', () => { it('reports a healthy baseline database with at least one admin', async () => { const report = await collectDiagnostics(); expect(report.database.ok).toBe(true); expect(report.database.integrity).toBe('ok'); expect(report.database.missingTables).toEqual([]); expect(report.auth.adminCount).toBeGreaterThanOrEqual(1); expect(report.encryptionKey).toEqual({ present: true, valid: true }); }); it('reports docker unreachable when no probe is supplied', async () => { const report = await collectDiagnostics(); expect(report.docker.reachable).toBe(false); }); it('reflects a passed docker probe', async () => { const reachable = await collectDiagnostics({ checkDocker: async () => true }); expect(reachable.docker.reachable).toBe(true); const down = await collectDiagnostics({ checkDocker: async () => { throw new Error('socket closed'); }, }); expect(down.docker.reachable).toBe(false); expect(down.docker.error).toBe('socket closed'); }); it('never surfaces secret settings in the config block', async () => { const db = DatabaseService.getInstance(); db.updateGlobalSetting('auth_jwt_secret', 'top-secret-signing-key'); db.updateGlobalSetting('cloud_backup_secret_key', 'enc:deadbeef'); db.updateGlobalSetting('host_cpu_limit', '80'); const report = await collectDiagnostics(); expect(report.config.auth_jwt_secret).toBeUndefined(); expect(report.config.cloud_backup_secret_key).toBeUndefined(); // The allowlisted, non-secret value is present. expect(report.config.host_cpu_limit).toBe('80'); // No emitted value is one of the seeded secrets, regardless of key name. expect(Object.values(report.config)).not.toContain('top-secret-signing-key'); expect(Object.values(report.config)).not.toContain('enc:deadbeef'); }); // Destructive: these table drops must run last in this file. it('degrades instead of throwing when a read table is missing', async () => { // sso_config is read by collectDiagnostics; dropping it must not throw, // it must flag the table and fall back to an empty provider list. DatabaseService.getInstance().getDb().exec('DROP TABLE sso_config'); const report = await collectDiagnostics(); expect(report.database.missingTables).toContain('sso_config'); expect(report.database.ok).toBe(false); expect(report.auth.ssoProviders).toEqual([]); }); it('flags a missing core table', async () => { DatabaseService.getInstance().getDb().exec('DROP TABLE audit_log'); const report = await collectDiagnostics(); expect(report.database.missingTables).toContain('audit_log'); expect(report.database.ok).toBe(false); }); });