import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { WARN_BEFORE_EXPIRY_MS, formatClampedTtlWarning, formatConnectionDetails, minutesUntil, toConnectionJson, toExpiryWarningDelay, validateDatabaseProxyArgs, type DatabaseAccessSession, } from './database-proxy.js'; const NOW = Date.parse('2026-10-12T12:00:00.000Z'); const inMinutes = (minutes: number): string => new Date(NOW + minutes * 60_000).toISOString(); const session = (expiresAt: string): DatabaseAccessSession => ({ databaseAccessSessionId: '8191584c-dd96-4803-bfd8-5f644b32293c', username: 'dbaccess_8191584cdd964803bfd85f644b32293c', password: 'a-single-use-password', databaseName: 'h_xyrlwpb8pyoqthz9_test_service_a', expiresAt, databaseProxyUrl: 'ws://localhost:12500/connect', }); const validArgs = { serviceId: 'test-service-a', idServiceAuthBaseURL: 'http://localhost:10500', mosaicHostingClientId: 'client', mosaicHostingClientSecret: 'secret', hostingServiceBaseURL: 'http://localhost:11300', }; describe('minutesUntil', () => { it('rounds down to whole minutes', () => { expect(minutesUntil(inMinutes(59.6), NOW)).toBe(59); }); it('never reports a negative remainder for a session already over', () => { expect(minutesUntil(inMinutes(-10), NOW)).toBe(0); }); }); describe('formatClampedTtlWarning', () => { it('says nothing when the granted lifetime is what was asked for', () => { expect(formatClampedTtlWarning(60, inMinutes(60), NOW)).toBeUndefined(); }); it('tolerates the minute the round trip costs', () => { expect(formatClampedTtlWarning(60, inMinutes(59.2), NOW)).toBeUndefined(); }); it('says nothing when no lifetime was asked for', () => { expect( formatClampedTtlWarning(undefined, inMinutes(10), NOW), ).toBeUndefined(); }); it('reports both numbers when Hosting clamped the lifetime', () => { expect(formatClampedTtlWarning(60, inMinutes(10), NOW)).toBe( 'Requested 60 minutes, granted 10 - database access for this service expires then.', ); }); }); describe('toExpiryWarningDelay', () => { it('schedules the warning ahead of the expiry it warns about', () => { expect(toExpiryWarningDelay(inMinutes(60), NOW)).toBe( 60 * 60_000 - WARN_BEFORE_EXPIRY_MS, ); }); it('skips the warning when it could only arrive after the expiry', () => { expect(toExpiryWarningDelay(inMinutes(4), NOW)).toBeUndefined(); }); }); describe('formatConnectionDetails', () => { it('lists everything a database client needs', () => { const details = formatConnectionDetails(session(inMinutes(60)), 5433, NOW); expect(details).toContain('Host 127.0.0.1'); expect(details).toContain('Port 5433'); expect(details).toContain('Database h_xyrlwpb8pyoqthz9_test_service_a'); expect(details).toContain( 'User dbaccess_8191584cdd964803bfd85f644b32293c', ); expect(details).toContain('Password a-single-use-password'); expect(details).toContain('(60 minutes)'); }); }); describe('toConnectionJson', () => { it('names the local proxy, not the database host the proxy reaches', () => { expect(toConnectionJson(session(inMinutes(60)), 5433)).toMatchObject({ host: '127.0.0.1', port: 5433, user: 'dbaccess_8191584cdd964803bfd85f644b32293c', }); }); }); describe('validateDatabaseProxyArgs', () => { // The fallbacks read process.env, so the assertions below would otherwise // depend on whatever the shell running the tests happens to export. const ENV_FALLBACKS = [ 'SERVICE_ID', 'ID_SERVICE_AUTH_BASE_URL', 'MOSAIC_HOSTING_CLIENT_ID', 'MOSAIC_HOSTING_CLIENT_SECRET', 'HOSTING_SERVICE_BASE_URL', ]; let saved: Record; beforeEach(() => { saved = Object.fromEntries(ENV_FALLBACKS.map((k) => [k, process.env[k]])); for (const key of ENV_FALLBACKS) { delete process.env[key]; } }); afterEach(() => { for (const [key, value] of Object.entries(saved)) { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } } }); it('accepts a fully specified invocation', () => { const [, errors] = validateDatabaseProxyArgs(validArgs); expect(errors).toEqual([]); }); it('takes the service id from the environment when the flag is absent', () => { process.env.SERVICE_ID = 'media-service'; const [resolved, errors] = validateDatabaseProxyArgs({ ...validArgs, serviceId: undefined, }); delete process.env.SERVICE_ID; expect(resolved.serviceId).toBe('media-service'); expect(errors).toEqual([]); }); it('names every missing connection argument', () => { const [, errors] = validateDatabaseProxyArgs({ serviceId: 'test-service-a', }); expect(errors).toEqual([ '[idServiceAuthBaseURL] is required.', '[mosaicHostingClientId] is required.', '[mosaicHostingClientSecret] is required.', '[hostingServiceBaseURL] is required.', ]); }); it('rejects a lifetime that is not a whole positive number of minutes', () => { const [, errors] = validateDatabaseProxyArgs({ ...validArgs, ttlMinutes: 0, }); expect(errors).toContain( '[ttlMinutes] must be a whole number of 1 or more.', ); }); it('rejects a local proxy port outside the range a socket can bind', () => { const [, errors] = validateDatabaseProxyArgs({ ...validArgs, localProxyPort: 70000, }); expect(errors).toContain( '[localProxyPort] must be a whole number between 1 and 65535.', ); }); });