Improve remote Matrix token refresh

This commit is contained in:
jjbrunton committed 2026-08-08 14:30:40 +01:00
1 parent b9df8459e3
commit bffda95251
6 files changed
+226 -14

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
import { describe, expect, it, vi } from 'vitest';
import type { BrowserContext, Frame } from 'playwright';
import { createMockLogger } from './mockLogger.js';
import {
extractMatrixCredentials,
MatrixBrowserLoginRequiredError,
} from '../src/services/chat/matrix/token-extractor.js';
describe('extractMatrixCredentials', () => {
it('fails as soon as Reddit redirects the chat page to login', async () => {
const handlers = new Map<string, (value: never) => void>();
const frame = { url: () => 'https://www.reddit.com/login/?dest=https://www.reddit.com/chat/' } as Frame;
const page = {
on: vi.fn((event: string, handler: (value: never) => void) => {
handlers.set(event, handler);
}),
mainFrame: vi.fn(() => frame),
goto: vi.fn(async () => {
handlers.get('framenavigated')?.(frame as never);
return null;
}),
url: vi.fn(() => frame.url()),
screenshot: vi.fn(async () => undefined),
close: vi.fn(async () => undefined),
};
const context = { newPage: vi.fn(async () => page) } as unknown as BrowserContext;
await expect(
extractMatrixCredentials(context, createMockLogger(), { timeoutMs: 5_000 }),
).rejects.toBeInstanceOf(MatrixBrowserLoginRequiredError);
expect(page.screenshot).not.toHaveBeenCalled();
});
});
+78
View File
@@ -0,0 +1,78 @@
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 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<MatrixCredentials>((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();
});
});