diff --git a/src/services/chat/matrix/token-refresher.ts b/src/services/chat/matrix/token-refresher.ts index e4c272e..2a61c07 100644 --- a/src/services/chat/matrix/token-refresher.ts +++ b/src/services/chat/matrix/token-refresher.ts @@ -2,7 +2,10 @@ import type { BrowserContext } from 'playwright'; import type { Logger } from '../../../utils/logger.js'; import type { TokenRefresher } from '../service.js'; import type { MatrixCredentials } from './types.js'; -import { extractMatrixCredentials } from './token-extractor.js'; +import { + extractMatrixCredentials, + MatrixBrowserLoginRequiredError, +} from './token-extractor.js'; export interface MatrixSessionBrowser { getContext(): BrowserContext | null; @@ -50,12 +53,10 @@ export class MatrixTokenRefresher implements TokenRefresher { private async performRefresh(): Promise { const sessionValid = await this.browser.isSessionValid(); + let sessionRestored = false; if (!sessionValid) { - this.logger.warn('[matrix] Reddit browser session expired; attempting login recovery'); - if (!(await this.browser.login())) { - throw new MatrixRemoteLoginRequiredError(); - } - this.logger.info('[matrix] Reddit browser session restored'); + await this.restoreSession(); + sessionRestored = true; } const context = this.browser.getContext(); @@ -63,7 +64,34 @@ export class MatrixTokenRefresher implements TokenRefresher { throw new Error('Matrix token refresh requires an initialized browser context'); } - const credentials = await this.extract(context, this.logger); + let credentials: MatrixCredentials; + try { + credentials = await this.extract(context, this.logger); + } catch (error) { + if (!(error instanceof MatrixBrowserLoginRequiredError)) throw error; + if (sessionRestored) throw new MatrixRemoteLoginRequiredError(); + + this.logger.warn( + '[matrix] Reddit API session looked valid but chat requires login; attempting recovery', + ); + await this.restoreSession(); + try { + credentials = await this.extract(context, this.logger); + } catch (retryError) { + if (retryError instanceof MatrixBrowserLoginRequiredError) { + throw new MatrixRemoteLoginRequiredError(); + } + throw retryError; + } + } this.applyCredentials(credentials); } + + private async restoreSession(): Promise { + this.logger.warn('[matrix] Reddit browser session expired; attempting login recovery'); + if (!(await this.browser.login())) { + throw new MatrixRemoteLoginRequiredError(); + } + this.logger.info('[matrix] Reddit browser session restored'); + } } diff --git a/tests/matrix-token-refresher.test.ts b/tests/matrix-token-refresher.test.ts index 91426f7..c6b4eb9 100644 --- a/tests/matrix-token-refresher.test.ts +++ b/tests/matrix-token-refresher.test.ts @@ -5,6 +5,7 @@ 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 = { @@ -75,4 +76,23 @@ describe('MatrixTokenRefresher', () => { 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); + }); });