feat: replace browser login with imported Reddit sessions
This commit is contained in:
1 parent
2b0545afcf
commit
c483c7b8be
76 files changed
+2048
-2828
No files matched your search
+85
-68
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user