import { describe, expect, it, vi } from 'vitest'; import type { Logger } from '../src/utils/logger.js'; import type { MatrixCredentials } from '../src/services/chat/matrix/types.js'; import { BrowserMatrixTokenRefresher, type BrowserMatrixTokenRefresherDeps, type BrowserMatrixTokenRefresherOptions, type TokenCaptureRequest, } from '../src/services/chat/matrix/browser-token-refresher.js'; import { MatrixRemoteLoginRequiredError } from '../src/services/chat/matrix/token-refresher.js'; function createLogger(): Logger { return { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as unknown as Logger; } function fakeRequest(url: string, headers: Record): TokenCaptureRequest { return { url: () => url, headers: () => headers }; } interface Harness { refresher: BrowserMatrixTokenRefresher; emit(request: TokenCaptureRequest): void; handlers(): number; pagesOpened(): number; pageClosed(): boolean; applied: MatrixCredentials[]; persisted: MatrixCredentials[]; verify: ReturnType; } function createHarness(input: { /** Requests emitted while the chat page loads. */ requestsOnGoto?: TokenCaptureRequest[]; verifiedUserId?: string | Error; options?: BrowserMatrixTokenRefresherOptions; deps?: Partial; } = {}): Harness { const handlers = new Set<(request: TokenCaptureRequest) => void>(); const applied: MatrixCredentials[] = []; const persisted: MatrixCredentials[] = []; let pagesOpened = 0; let pageClosed = false; const verify = vi.fn((credentials: MatrixCredentials) => { void credentials; const outcome = input.verifiedUserId ?? '@t2_abc:reddit.com'; return outcome instanceof Error ? Promise.reject(outcome) : Promise.resolve(outcome); }); const refresher = new BrowserMatrixTokenRefresher({ context: { on: (_event, handler) => handlers.add(handler), off: (_event, handler) => handlers.delete(handler), newPage: async () => { pagesOpened += 1; return { goto: async () => { for (const request of input.requestsOnGoto ?? []) { for (const handler of handlers) handler(request); } }, close: async () => { pageClosed = true; }, }; }, }, expectedUserId: '@t2_abc:reddit.com', verifyCredentials: verify, applyCredentials: (credentials) => applied.push(credentials), persistCredentials: (credentials) => persisted.push(credentials), logger: createLogger(), ...input.deps, }, { captureTimeoutMs: 50, failureCooldownMs: 60_000, ...input.options }); return { refresher, emit: (request) => { for (const handler of handlers) handler(request); }, handlers: () => handlers.size, pagesOpened: () => pagesOpened, pageClosed: () => pageClosed, applied, persisted, verify, }; } const bearerRequest = fakeRequest( 'https://matrix.redditspace.com/_matrix/client/v3/sync', { authorization: 'Bearer fresh-token' }, ); describe('BrowserMatrixTokenRefresher', () => { it('captures the bearer from Matrix traffic, verifies, applies and persists it', async () => { const now = () => 1_000; const harness = createHarness({ requestsOnGoto: [bearerRequest], options: { now } }); await harness.refresher.refresh(); const expected: MatrixCredentials = { homeserver: 'https://matrix.redditspace.com', accessToken: 'fresh-token', userId: '@t2_abc:reddit.com', capturedAt: 1_000, }; expect(harness.verify).toHaveBeenCalledWith(expected); expect(harness.applied).toEqual([expected]); expect(harness.persisted).toEqual([expected]); expect(harness.pageClosed()).toBe(true); expect(harness.handlers()).toBe(0); }); it('ignores non-Matrix and non-bearer requests', async () => { const harness = createHarness({ requestsOnGoto: [ fakeRequest('https://chat.reddit.com/api', { authorization: 'Bearer decoy' }), fakeRequest('https://matrix.redditspace.com/versions', {}), fakeRequest('http://matrix.reddit.com/insecure', { authorization: 'Bearer decoy' }), fakeRequest('not a url', { authorization: 'Bearer decoy' }), bearerRequest, ], }); await harness.refresher.refresh(); expect(harness.applied[0]?.accessToken).toBe('fresh-token'); }); it('requires a fresh dump when no bearer appears before the timeout', async () => { const harness = createHarness({ requestsOnGoto: [] }); await expect(harness.refresher.refresh()).rejects.toBeInstanceOf(MatrixRemoteLoginRequiredError); expect(harness.applied).toEqual([]); expect(harness.persisted).toEqual([]); expect(harness.pageClosed()).toBe(true); }); it('rejects a token minted for a different Matrix user without applying it', async () => { const harness = createHarness({ requestsOnGoto: [bearerRequest], verifiedUserId: '@t2_other:reddit.com', }); await expect(harness.refresher.refresh()).rejects.toThrow(/minted a Matrix token for @t2_other/); expect(harness.applied).toEqual([]); expect(harness.persisted).toEqual([]); }); it('propagates verification failures without persisting', async () => { const harness = createHarness({ requestsOnGoto: [bearerRequest], verifiedUserId: new Error('whoami exploded'), }); await expect(harness.refresher.refresh()).rejects.toThrow('whoami exploded'); expect(harness.persisted).toEqual([]); }); it('deduplicates concurrent refreshes onto one browser pass', async () => { const harness = createHarness({ requestsOnGoto: [bearerRequest] }); await Promise.all([harness.refresher.refresh(), harness.refresher.refresh()]); expect(harness.pagesOpened()).toBe(1); expect(harness.applied).toHaveLength(1); }); it('backs off after a failure and retries once the cooldown elapses', async () => { let clock = 0; const requests: TokenCaptureRequest[] = []; const harness = createHarness({ requestsOnGoto: requests, options: { now: () => clock, failureCooldownMs: 60_000, captureTimeoutMs: 10 }, }); await expect(harness.refresher.refresh()).rejects.toBeInstanceOf(MatrixRemoteLoginRequiredError); expect(harness.pagesOpened()).toBe(1); // Inside the cooldown the browser is not reopened. clock = 30_000; await expect(harness.refresher.refresh()).rejects.toBeInstanceOf(MatrixRemoteLoginRequiredError); expect(harness.pagesOpened()).toBe(1); clock = 60_001; requests.push(bearerRequest); await harness.refresher.refresh(); expect(harness.pagesOpened()).toBe(2); expect(harness.applied).toHaveLength(1); }); });