feat: replace browser login with imported Reddit sessions

This commit is contained in:
jjbrunton committed 2026-08-08 16:48:59 +01:00
1 parent 2b0545afcf
commit c483c7b8be
76 files changed
+2048 -2828

No files matched your search

+85 -68
View File
@@ -6,20 +6,12 @@ import { createLoginRouter } from '../src/web/routes/login.js';
const servers: Array<ReturnType<express.Express['listen']>> = [];
async function request(app: express.Express, path: string): Promise<Response> {
async function request(app: express.Express, path: string, init?: RequestInit): Promise<Response> {
const server = app.listen(0);
servers.push(server);
await new Promise<void>((resolve) => server.once('listening', resolve));
const { port } = server.address() as AddressInfo;
return fetch(`http://127.0.0.1:${port}${path}`);
}
async function post(app: express.Express, path: string): Promise<Response> {
const server = app.listen(0);
servers.push(server);
await new Promise<void>((resolve) => server.once('listening', resolve));
const { port } = server.address() as AddressInfo;
return fetch(`http://127.0.0.1:${port}${path}`, { method: 'POST' });
return fetch(`http://127.0.0.1:${port}${path}`, init);
}
afterEach(async () => {
@@ -28,7 +20,6 @@ afterEach(async () => {
(server) => new Promise<void>((resolve) => server.close(() => resolve())),
),
);
delete process.env.NOVNC_PUBLIC_URL;
});
function appWithBrowser(state: 'authenticated' | 'login-required' | 'unreachable') {
@@ -36,11 +27,9 @@ function appWithBrowser(state: 'authenticated' | 'login-required' | 'unreachable
app.use(
createLoginRouter({
services: {
accountAuth: {
ensureFullyAuthenticated: async () => state === 'authenticated',
},
browser: {
isSessionValid: async () => state === 'authenticated',
getAuthenticatedUsername: async () => state === 'authenticated' ? 'example-account' : null,
getSessionState: () => state,
},
},
@@ -50,21 +39,84 @@ function appWithBrowser(state: 'authenticated' | 'login-required' | 'unreachable
return app;
}
function appWithPartialAuthentication() {
const app = express();
app.use(createLoginRouter({
services: {
accountAuth: { ensureFullyAuthenticated: async () => false },
browser: {
getSessionState: () => 'authenticated',
},
},
logger: { error: () => undefined },
} as never));
return app;
}
describe('Reddit session status', () => {
it('reports the verified Reddit account name for an authenticated session', async () => {
const response = await request(appWithBrowser('authenticated'), '/login/status');
expect(await response.json()).toEqual({
ready: true,
loggedIn: true,
status: 'authenticated',
username: 'example-account',
});
});
it('imports Reddit cookies, omits unrelated domains, and verifies the new session', async () => {
const importSession = vi.fn().mockResolvedValue(undefined);
const app = express();
app.use(express.json());
app.use(createLoginRouter({
services: {
sessionImporter: { import: importSession },
},
logger: { error: () => undefined, info: () => undefined },
} as never));
const response = await request(app, '/login/session-import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cookies: [
{ name: 'reddit_session', value: 'secret', domain: '.reddit.com', path: '/', secure: true, httpOnly: true, sameSite: 'lax' },
{ name: 'tracking', value: 'ignore-me', domain: '.example.com', path: '/' },
],
matrix: {
homeserver: 'https://matrix.redditspace.com',
accessToken: 'matrix-secret',
userId: '@t2_bot:reddit.com',
capturedAt: 1_700_000_000_000,
},
}),
});
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ ok: true, status: 'authenticated' });
expect(importSession).toHaveBeenCalledWith(
[expect.objectContaining({ name: 'reddit_session', domain: '.reddit.com' })],
expect.objectContaining({ accessToken: 'matrix-secret' }),
);
});
it('does not claim success when imported cookies do not authenticate', async () => {
const app = express();
app.use(express.json());
app.use(createLoginRouter({
services: {
sessionImporter: {
import: async () => { throw new Error('Imported cookies did not authenticate with Reddit'); },
},
},
logger: { error: () => undefined, info: () => undefined },
} as never));
const response = await request(app, '/login/session-import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cookies: [{ name: 'reddit_session', value: 'secret', domain: '.reddit.com', path: '/' }],
matrix: {
homeserver: 'https://matrix.redditspace.com',
accessToken: 'matrix-secret',
userId: '@t2_bot:reddit.com',
capturedAt: 1_700_000_000_000,
},
}),
});
expect(response.status).toBe(401);
expect(await response.json()).toEqual({ ok: false, error: 'Imported cookies did not authenticate with Reddit' });
});
it('reports when Reddit is unreachable instead of presenting it as logged out', async () => {
const response = await request(appWithBrowser('unreachable'), '/login/status');
@@ -73,6 +125,7 @@ describe('Reddit session status', () => {
ready: true,
loggedIn: false,
status: 'unreachable',
username: null,
});
});
@@ -85,24 +138,11 @@ describe('Reddit session status', () => {
});
});
it('does not report authenticated when only the Reddit browser session is valid', async () => {
const response = await request(appWithPartialAuthentication(), '/login/status');
it('does not expose the retired interactive login endpoints', async () => {
const app = appWithBrowser('authenticated');
expect(await response.json()).toEqual({
ready: true,
loggedIn: false,
status: 'login-required',
});
});
it('returns the configured public noVNC URL', async () => {
process.env.NOVNC_PUBLIC_URL = 'https://red-vnc.apps.jbrunton.co.uk';
const response = await request(appWithBrowser('authenticated'), '/login/info');
expect(await response.json()).toMatchObject({
publicUrl: 'https://red-vnc.apps.jbrunton.co.uk',
});
expect((await request(app, '/login/info')).status).toBe(404);
expect((await request(app, '/login/start', { method: 'POST' })).status).toBe(404);
});
it('classifies Reddit navigation failures as unreachable', () => {
@@ -113,27 +153,4 @@ describe('Reddit session status', () => {
).toBe('unreachable');
expect(classifyRedditSessionError(new Error('login form was not found'))).toBe('login-required');
});
it('starts a native Chrome login handoff without controlling the attached page', async () => {
const requestNativeLogin = vi.fn();
const getPage = vi.fn();
const invalidate = vi.fn();
const app = express();
app.use(createLoginRouter({
services: {
accountAuth: { invalidate },
browser: { getPage },
nativeLogin: { request: requestNativeLogin },
},
logger: { info: vi.fn(), error: vi.fn() },
} as never));
const response = await post(app, '/login/start');
expect(response.status).toBe(202);
expect(await response.json()).toEqual({ ok: true, restarting: true });
expect(invalidate).toHaveBeenCalledWith('Native Reddit login requested');
expect(requestNativeLogin).toHaveBeenCalledOnce();
expect(getPage).not.toHaveBeenCalled();
});
});