import { describe, expect, it } from 'vitest'; import { Code, ConnectError } from '@connectrpc/connect'; import { maintenanceRetryMs, parseRetryAfterMs } from './maintenance'; // A raw ConnectError as connect-web builds it from a non-Connect HTTP 503: the response // headers are preserved as `.metadata` (verified against @connectrpc/connect-web). const err503 = (headers: Record): ConnectError => new ConnectError('HTTP 503', Code.Unavailable, new Headers(headers)); describe('maintenanceRetryMs', () => { it('detects the marker and parses Retry-After', () => { expect(maintenanceRetryMs(err503({ 'x-scrabble-maintenance': '1', 'retry-after': '120' }))).toBe(120_000); }); it('matches the header case-insensitively, defaulting the delay when Retry-After is absent', () => { expect(maintenanceRetryMs(err503({ 'X-Scrabble-Maintenance': '1' }))).toBe(15_000); }); it('returns null for a 503 without the marker (a plain transient failure)', () => { expect(maintenanceRetryMs(err503({ 'retry-after': '30' }))).toBeNull(); }); it('returns null for non-Connect errors', () => { expect(maintenanceRetryMs(new Error('boom'))).toBeNull(); expect(maintenanceRetryMs(undefined)).toBeNull(); expect(maintenanceRetryMs('nope')).toBeNull(); }); }); describe('parseRetryAfterMs', () => { it('clamps into the 3s–120s band', () => { expect(parseRetryAfterMs('120')).toBe(120_000); expect(parseRetryAfterMs('1')).toBe(3_000); // floor expect(parseRetryAfterMs('9999')).toBe(120_000); // ceiling }); it('falls back to the default on absent / garbage / non-positive', () => { expect(parseRetryAfterMs(null)).toBe(15_000); expect(parseRetryAfterMs(undefined)).toBe(15_000); expect(parseRetryAfterMs('soon')).toBe(15_000); expect(parseRetryAfterMs('0')).toBe(15_000); expect(parseRetryAfterMs('-5')).toBe(15_000); }); });