import { describe, expect, it, vi } from 'vitest'; import type { BrowserContext } from 'playwright'; import { createMockLogger } from './mockLogger.js'; import { MatrixRemoteLoginRequiredError, MatrixTokenRefresher, } from '../src/services/chat/matrix/token-refresher.js'; import { MatrixBrowserLoginRequiredError } from '../src/services/chat/matrix/token-extractor.js'; import type { MatrixCredentials } from '../src/services/chat/matrix/types.js'; const credentials: MatrixCredentials = { homeserver: 'https://matrix.redditspace.com', accessToken: 'fresh-token', userId: '@t2_bot:reddit.com', capturedAt: 123, }; function setup(options: { sessionValid?: boolean; loginSucceeds?: boolean } = {}) { const context = {} as BrowserContext; const browser = { getContext: vi.fn(() => context), isSessionValid: vi.fn(async () => options.sessionValid ?? true), login: vi.fn(async () => options.loginSucceeds ?? true), }; const extract = vi.fn(async () => credentials); const applyCredentials = vi.fn(); const logger = createMockLogger(); const refresher = new MatrixTokenRefresher( browser, logger, applyCredentials, extract, ); return { refresher, browser, context, extract, applyCredentials, logger }; } describe('MatrixTokenRefresher', () => { it('repairs an expired Reddit browser session before capturing a Matrix token', async () => { const { refresher, browser, context, extract, applyCredentials } = setup({ sessionValid: false, loginSucceeds: true, }); await refresher.refresh(); expect(browser.login).toHaveBeenCalledOnce(); expect(extract).toHaveBeenCalledWith(context, expect.anything()); expect(applyCredentials).toHaveBeenCalledWith(credentials); }); it('fails fast with remote-login guidance when the Reddit session cannot be restored', async () => { const { refresher, browser, extract } = setup({ sessionValid: false, loginSucceeds: false, }); await expect(refresher.refresh()).rejects.toBeInstanceOf(MatrixRemoteLoginRequiredError); await expect(refresher.refresh()).rejects.toThrow(/dashboard.*noVNC/i); expect(browser.login).toHaveBeenCalledTimes(2); expect(extract).not.toHaveBeenCalled(); }); it('coalesces concurrent refreshes so only one browser capture runs', async () => { const { refresher, extract, applyCredentials } = setup(); let release: ((value: MatrixCredentials) => void) | undefined; extract.mockImplementation( () => new Promise((resolve) => { release = resolve; }), ); const first = refresher.refresh(); const second = refresher.refresh(); await vi.waitFor(() => expect(extract).toHaveBeenCalledOnce()); release?.(credentials); await Promise.all([first, second]); expect(applyCredentials).toHaveBeenCalledOnce(); }); it('repairs and retries when chat redirects to login despite a valid Reddit API session', async () => { const { refresher, browser, context, extract, applyCredentials } = setup({ sessionValid: true, loginSucceeds: true, }); extract .mockRejectedValueOnce( new MatrixBrowserLoginRequiredError('https://www.reddit.com/login/?dest=/chat/'), ) .mockResolvedValueOnce(credentials); await refresher.refresh(); expect(browser.login).toHaveBeenCalledOnce(); expect(extract).toHaveBeenCalledTimes(2); expect(extract).toHaveBeenNthCalledWith(2, context, expect.anything()); expect(applyCredentials).toHaveBeenCalledWith(credentials); }); });