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): 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}`); } async function post(app: express.Express, path: string): 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}`, { method: 'POST' }); } afterEach(async () => { await Promise.all( servers.splice(0).map( (server) => new Promise((resolve) => server.close(() => resolve())), ), ); delete process.env.NOVNC_PUBLIC_URL; }); function appWithBrowser(state: 'authenticated' | 'login-required' | 'unreachable') { const app = express(); app.use( createLoginRouter({ services: { accountAuth: { ensureFullyAuthenticated: async () => state === 'authenticated', }, browser: { isSessionValid: async () => state === 'authenticated', getSessionState: () => state, }, }, logger: { error: () => undefined }, } as never), ); 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 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', }); }); 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 report authenticated when only the Reddit browser session is valid', async () => { const response = await request(appWithPartialAuthentication(), '/login/status'); 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', }); }); 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'); }); 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(); }); });