import express from 'express'; import type { AddressInfo } from 'node:net'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { classifyRedditSessionError } from '../src/browser/client.js'; import { createLoginRouter } from '../src/web/routes/login.js'; const servers: Array> = []; async function request(app: express.Express, path: string, init?: RequestInit): Promise { const server = app.listen(0); servers.push(server); await new Promise((resolve) => server.once('listening', resolve)); const { port } = server.address() as AddressInfo; return fetch(`http://127.0.0.1:${port}${path}`, init); } afterEach(async () => { await Promise.all( servers.splice(0).map( (server) => new Promise((resolve) => server.close(() => resolve())), ), ); }); function appWithBrowser(state: 'authenticated' | 'login-required' | 'unreachable') { const app = express(); app.use( createLoginRouter({ services: { accountAuth: { ensureFullyAuthenticated: async () => state === 'authenticated', getSnapshot: () => ({ redditUsername: state === 'authenticated' ? 'example-account' : null }), }, browser: { isSessionValid: async () => state === 'authenticated', getAuthenticatedUsername: async () => state === 'authenticated' ? 'example-account' : null, getSessionState: () => state, }, }, 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'); expect(response.status).toBe(200); expect(await response.json()).toEqual({ ready: true, loggedIn: false, status: 'unreachable', username: null, }); }); it('reports when an interactive Reddit login is required', async () => { const response = await request(appWithBrowser('login-required'), '/login/status'); expect(await response.json()).toMatchObject({ loggedIn: false, status: 'login-required', }); }); it('does not expose the retired interactive login endpoints', async () => { const app = appWithBrowser('authenticated'); expect((await request(app, '/login/info')).status).toBe(404); expect((await request(app, '/login/start', { method: 'POST' })).status).toBe(404); }); it('removes an account and reports the account it deleted', async () => { const remove = vi.fn().mockResolvedValue({ accountId: 't2_alice', redditUsername: 'alice-bot' }); const app = express(); app.use(createLoginRouter({ services: { redditAccounts: { remove } }, logger: { error: () => undefined, info: () => undefined }, } as never)); const response = await request(app, '/accounts/t2_alice', { method: 'DELETE' }); expect(response.status).toBe(200); expect(await response.json()).toEqual({ ok: true, account: { accountId: 't2_alice', redditUsername: 'alice-bot' }, }); expect(remove).toHaveBeenCalledWith('t2_alice'); }); it('answers 404 when the account to remove does not exist', async () => { const app = express(); app.use(createLoginRouter({ services: { redditAccounts: { remove: async () => { throw new Error('Reddit account not found'); } }, }, logger: { error: () => undefined, info: () => undefined }, } as never)); const response = await request(app, '/accounts/t2_missing', { method: 'DELETE' }); expect(response.status).toBe(404); expect(await response.json()).toEqual({ ok: false, error: 'Reddit account not found' }); }); it('reports a failed removal as a server error rather than a silent success', async () => { const app = express(); app.use(createLoginRouter({ services: { redditAccounts: { remove: async () => { throw new Error('database is locked'); } }, }, logger: { error: () => undefined, info: () => undefined }, } as never)); const response = await request(app, '/accounts/t2_alice', { method: 'DELETE' }); expect(response.status).toBe(500); expect(await response.json()).toEqual({ ok: false, error: 'database is locked' }); }); it('classifies Reddit navigation failures as unreachable', () => { expect( classifyRedditSessionError( new Error('page.goto: net::ERR_HTTP_RESPONSE_CODE_FAILURE at https://www.reddit.com/'), ), ).toBe('unreachable'); expect(classifyRedditSessionError(new Error('login form was not found'))).toBe('login-required'); }); });