Use native Chrome for Reddit login handoff

This commit is contained in:
jjbrunton committed 2026-08-08 16:16:11 +01:00
1 parent 2c533fa52a
commit dbb1842ef4
13 files changed
+324 -60

No files matched your search

+3 -6
View File
@@ -57,7 +57,7 @@ describe('BrowserClient external Chrome', () => {
expect(runtime.launchPersistentContext).toHaveBeenCalledOnce();
});
it('opens the login page for a human without submitting credentials', async () => {
it('does not control external Chrome during native human login', async () => {
process.env.CHROME_CDP_URL = 'http://127.0.0.1:9222';
const page = {
goto: vi.fn().mockResolvedValue(undefined),
@@ -79,11 +79,8 @@ describe('BrowserClient external Chrome', () => {
await client.initialize();
await expect(client.login()).resolves.toBe(false);
expect(page.bringToFront).toHaveBeenCalledOnce();
expect(page.goto).toHaveBeenCalledWith(
'https://www.reddit.com/login',
{ waitUntil: 'domcontentloaded' },
);
expect(page.bringToFront).not.toHaveBeenCalled();
expect(page.goto).not.toHaveBeenCalled();
expect(page.fill).not.toHaveBeenCalled();
expect(client.getSessionState()).toBe('login-required');
});
+32 -1
View File
@@ -1,6 +1,6 @@
import express from 'express';
import type { AddressInfo } from 'node:net';
import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { classifyRedditSessionError } from '../src/browser/client.js';
import { createLoginRouter } from '../src/web/routes/login.js';
@@ -14,6 +14,14 @@ async function request(app: express.Express, path: string): Promise<Response> {
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' });
}
afterEach(async () => {
await Promise.all(
servers.splice(0).map(
@@ -105,4 +113,27 @@ 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();
});
});
+36
View File
@@ -0,0 +1,36 @@
import type { AddressInfo } from 'node:net';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createNativeLoginGate } from '../src/native-login-gate.js';
const servers: Array<ReturnType<typeof createNativeLoginGate>> = [];
afterEach(async () => {
await Promise.all(servers.splice(0).map((server) => new Promise<void>((resolve) => {
server.close(() => resolve());
})));
});
describe('native Reddit login gate', () => {
it('serves VNC guidance and completes only after the explicit handoff', async () => {
const onComplete = vi.fn();
const server = createNativeLoginGate({
publicVncUrl: 'https://red-vnc.example/vnc.html',
onComplete,
}).listen(0);
servers.push(server);
await new Promise<void>((resolve) => server.once('listening', resolve));
const { port } = server.address() as AddressInfo;
const page = await fetch(`http://127.0.0.1:${port}/`);
const html = await page.text();
expect(html).toContain('Chrome is running without automation');
expect(html).toContain('https://red-vnc.example/vnc.html');
expect(html).toContain("fetch('/complete', { method: 'POST' })");
expect(onComplete).not.toHaveBeenCalled();
const completed = await fetch(`http://127.0.0.1:${port}/complete`, { method: 'POST' });
expect(completed.status).toBe(202);
expect(await completed.json()).toEqual({ ok: true, restarting: true });
expect(onComplete).toHaveBeenCalledOnce();
});
});
+9 -3
View File
@@ -23,17 +23,23 @@ describe('VNC container entrypoint', () => {
expect(script.indexOf('SingletonLock')).toBeLessThan(script.indexOf('launching bot'));
});
it('launches regular Google Chrome before the bot and exposes its local CDP endpoint', () => {
it('supports a native login phase with no DevTools before the controlled browser starts', () => {
const script = fs.readFileSync(path.resolve('docker/entrypoint.sh'), 'utf8');
expect(script).toContain('google-chrome');
expect(script).toContain('--user-data-dir="${BROWSER_SESSION_DIR}"');
expect(script).toContain('start_native_login_chrome');
expect(script).toContain('https://www.reddit.com/login/');
expect(script).toContain('node dist/native-login-gate.js');
expect(script).toContain('NATIVE_LOGIN_EXIT_CODE');
expect(script).toContain('--remote-debugging-port="${CHROME_CDP_PORT}"');
expect(script).toContain('export CHROME_CDP_URL=');
expect(script).not.toContain('--enable-automation');
expect(script.indexOf('starting Google Chrome')).toBeLessThan(
script.indexOf('launching bot'),
const nativeFunction = script.slice(
script.indexOf('start_native_login_chrome()'),
script.indexOf('start_controlled_chrome()'),
);
expect(nativeFunction).not.toContain('--remote-debugging');
});
it('installs the stable Google Chrome package rather than Chrome for Testing', () => {