storebound/tests/console-session.test.ts
CRT a5295db539 browser tests: deterministic under load
The afterAll hook of tests/console-fill.test.ts timed out (30 s) in 2 of 3
runs with 10 CPU hogs. Cause: Google Chrome (the fallback when playwright's
Chromium is not downloaded) hangs in its own shutdown after loading http
pages until its 10 s teardown watchdog fires; under load that passed 30 s.

- tests/helpers/browser.ts: one shared launch per file as a browser server
  with a 60 s launch timeout, killed at the end (throwaway profile, nothing
  to flush); a launch timeout fails instead of silently skipping; contexts
  and engine runs tracked per test and closed in afterEach (capped);
  event-driven waits for engine log lines
- console-fill: explicit test timeouts, slow tests marked, waits expected
  to succeed get a long cap (they return at once), sign-in / watching
  navTimeoutMs 1.5 s -> 5 s, persistent-profile Chromium closed in finally,
  temp account dirs removed
- site-overflow, console-session: same plumbing (site-overflow's afterAll
  had bun's 5 s default)
- console session: launchTimeoutMs option (tests only)
2026-10-12 04:54:07 +08:00

173 lines
11 KiB
TypeScript

// storebound console fill: which browser it uses. Pure parts (data dir, profile pick, file selection, cookie host
// filter, DevToolsActivePort) and the session copy on throwaway folders (cleanup on success and on error). The
// integration test sets a cookie on a local fixture site in a THROWAWAY Chrome profile, then opens the console browser
// in copy mode from that profile and sees the cookie (and not the one of another host). It never touches the real
// Chrome profile or a store site; it skips when Google Chrome is not installed or not on macOS.
import { afterAll, describe, expect, test } from 'bun:test';
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { chromeDataDir, CONSOLE_HOSTS, cookieApplies, copySession, devToolsEndpoint, filterCookies, NEVER_COPY, openConsoleBrowser, pickProfile, sessionFiles } from '../cli/console/session';
import { capped, CLOSE_TIMEOUT_MS, LAUNCH_TIMEOUT_MS } from './helpers/browser';
const scratch = mkdtempSync(join(tmpdir(), 'sb-session-test-'));
afterAll(() => rmSync(scratch, { recursive: true, force: true }));
const tmp = (name: string) => { const d = join(scratch, name); mkdirSync(d, { recursive: true }); return d; };
async function cookieDb(path: string, hosts: string[]): Promise<void> {
const { Database } = await import('bun:sqlite');
const db = new Database(path);
db.exec('CREATE TABLE cookies (host_key TEXT, name TEXT, value TEXT, encrypted_value BLOB)');
const ins = db.prepare('INSERT INTO cookies VALUES (?, ?, ?, ?)');
for (const h of hosts) ins.run(h, 'c', '', new Uint8Array([1]));
db.close();
}
/** A fake Chrome user data dir with the session files and the ones that must never be copied. */
async function fakeChrome(name: string, profile = 'Default'): Promise<string> {
const d = tmp(name);
writeFileSync(join(d, 'Local State'), JSON.stringify({ profile: { last_used: profile } }));
mkdirSync(join(d, profile), { recursive: true });
await cookieDb(join(d, profile, 'Cookies'), ['.apple.com', 'appstoreconnect.apple.com', '.google.com', 'play.google.com', 'accounts.google.com', '.youtube.com', 'mail.google.com', '.example.com', 'news.example.org']);
writeFileSync(join(d, profile, 'Cookies-journal'), '');
for (const f of ['Login Data', 'History', 'Web Data', 'Bookmarks', 'Preferences']) writeFileSync(join(d, profile, f), 'x');
mkdirSync(join(d, profile, 'Extensions'), { recursive: true });
return d;
}
describe('pure parts', () => {
test('Chrome user data dir per OS', () => {
expect(chromeDataDir('darwin', '/Users/you')).toBe('/Users/you/Library/Application Support/Google/Chrome');
expect(chromeDataDir('linux', '/home/you', {})).toBe('/home/you/.config/google-chrome');
expect(chromeDataDir('win32', 'C:\\Users\\you', { LOCALAPPDATA: 'L' })).toContain('User Data');
});
test('profile: --chrome-profile, else last used, else Default; no path tricks', () => {
expect(pickProfile(null)).toBe('Default');
expect(pickProfile('{"profile":{"last_used":"Profile 2"}}')).toBe('Profile 2');
expect(pickProfile('{"profile":{"last_used":"../x"}}')).toBe('Default');
expect(pickProfile('not json', 'Profile 1')).toBe('Profile 1');
expect(() => pickProfile(null, '../Default')).toThrow('--chrome-profile');
});
test('files: Local State and the cookie store only, never passwords, history or extensions', () => {
const all = new Set(['Local State', 'Default/Cookies', 'Default/Cookies-journal', 'Default/Login Data', 'Default/History', 'Default/Network/Cookies']);
expect(sessionFiles('Default', r => all.has(r))).toEqual([
['Local State', 'Local State'], ['Default/Cookies', 'Default/Cookies'], ['Default/Cookies-journal', 'Default/Cookies-journal'], ['Default/Network/Cookies', 'Default/Network/Cookies'],
]);
expect(sessionFiles('Profile 1', r => r === 'Profile 1/Cookies')).toEqual([['Profile 1/Cookies', 'Default/Cookies']]);
for (const f of ['Default/Login Data', 'Default/History', 'Default/Extensions', 'Default/Web Data']) expect(NEVER_COPY.test(f)).toBe(true);
expect(NEVER_COPY.test('Default/Cookies')).toBe(false);
});
test('cookie hosts: only domains the consoles get', () => {
for (const h of ['.apple.com', 'appstoreconnect.apple.com', 'idmsa.apple.com', '.google.com', 'play.google.com', 'accounts.google.com']) expect(cookieApplies(h)).toBe(true);
for (const h of ['.youtube.com', 'mail.google.com', 'developer.apple.com', '.example.com', 'apple.com.example.org', '', '.com']) expect(cookieApplies(h)).toBe(false);
expect(CONSOLE_HOSTS).toContain('play.google.com');
});
test('DevToolsActivePort -> a local endpoint', () => {
const d = tmp('devtools');
expect(devToolsEndpoint(d)).toBeNull();
writeFileSync(join(d, 'DevToolsActivePort'), '9333\n/devtools/browser/abc\n');
expect(devToolsEndpoint(d)).toBe('http://127.0.0.1:9333');
writeFileSync(join(d, 'DevToolsActivePort'), 'nope');
expect(devToolsEndpoint(d)).toBeNull();
});
});
describe('session copy', () => {
test('copies only the session files, filters the cookies, cleans up', async () => {
const src = await fakeChrome('chrome-a');
const root = tmp('copies-a');
const c = await copySession(src, { tmpRoot: root });
expect(c.profile).toBe('Default');
expect(c.files).toEqual(['Local State', 'Default/Cookies', 'Default/Cookies-journal']);
for (const f of readdirSync(join(c.dir, 'Default'))) expect(['Cookies', 'Cookies-journal']).toContain(f);
expect(readdirSync(c.dir).sort()).toEqual(['Default', 'Local State']);
for (const f of ['Login Data', 'History', 'Web Data', 'Bookmarks', 'Preferences', 'Extensions']) expect(existsSync(join(c.dir, 'Default', f))).toBe(false);
expect(c.filtered).toBe(true);
expect(c.kept).toBe(5);
const { Database } = await import('bun:sqlite');
const db = new Database(join(c.dir, 'Default', 'Cookies'), { readonly: true });
expect((db.query('SELECT host_key AS h FROM cookies ORDER BY h').all() as { h: string }[]).map(r => r.h)).toEqual(['.apple.com', '.google.com', 'accounts.google.com', 'appstoreconnect.apple.com', 'play.google.com']);
db.close();
c.cleanup(); c.cleanup();
expect(existsSync(c.dir)).toBe(false);
expect(readdirSync(root)).toEqual([]);
// the source is untouched
expect(existsSync(join(src, 'Default', 'Login Data'))).toBe(true);
});
test('a named profile lands in Default of the copy', async () => {
const src = await fakeChrome('chrome-b', 'Profile 3');
const c = await copySession(src, { tmpRoot: tmp('copies-b'), profile: 'Profile 3' });
expect(c.files).toContain('Profile 3/Cookies');
expect(existsSync(join(c.dir, 'Default', 'Cookies'))).toBe(true);
c.cleanup();
});
test('an error removes the temporary profile before it is thrown', async () => {
const src = tmp('chrome-broken');
mkdirSync(join(src, 'Default', 'Cookies'), { recursive: true }); // a folder where the file should be: the copy fails
const root = tmp('copies-c');
await expect(copySession(src, { tmpRoot: root })).rejects.toThrow();
expect(readdirSync(root)).toEqual([]);
await expect(copySession(tmp('chrome-empty'), { tmpRoot: root })).rejects.toThrow('no Chrome cookie store');
expect(readdirSync(root)).toEqual([]);
});
test('an unreadable cookie file is copied whole and the log says so (filtered: false)', async () => {
const src = tmp('chrome-notdb');
writeFileSync(join(src, 'Local State'), '{}');
mkdirSync(join(src, 'Default'));
writeFileSync(join(src, 'Default', 'Cookies'), 'not a database');
const c = await copySession(src, { tmpRoot: tmp('copies-d') });
expect(c.filtered).toBe(false);
c.cleanup();
expect(await filterCookies(join(scratch, 'none.db')).catch(() => 'err')).toBe('err');
});
});
// ---- integration: real Google Chrome, throwaway profiles, a local fixture site ----
const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const hasChrome = process.platform === 'darwin' && existsSync(CHROME);
if (!hasChrome) console.warn('console-session integration test skipped: Google Chrome is not installed (or not macOS)');
describe.skipIf(!hasChrome)('copy mode with real Chrome (throwaway profile, local site)', () => {
test('a cookie set in the source profile is there in the copy; another host\'s is filtered out; the copy is deleted on close', async () => {
let chromium: any;
try { chromium = (await import('playwright')).chromium; } catch { console.warn('skipped: playwright not installed'); return; }
const server = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: req => {
const u = new URL(req.url);
if (u.pathname === '/set') return new Response('set', { headers: { 'set-cookie': 'sb_test=1; Max-Age=3600; Path=/' } });
return new Response(`cookie: ${/sb_test=1/.test(req.headers.get('cookie') ?? '') ? 'yes' : 'no'}`);
} });
const site = `http://127.0.0.1:${server.port}`, other = `http://localhost:${server.port}`;
const src = tmp('chrome-real');
// two real Chrome launches: SLOW; launch timeouts raised like the shared browser's (tests/helpers/browser.ts), and
// whatever is still open is closed in `finally`, so a failed assertion leaves no Chrome running
let seed: any = null, b: any = null;
try {
const opts = { channel: 'chrome', headless: true, timeout: LAUNCH_TIMEOUT_MS, ignoreDefaultArgs: ['--use-mock-keychain', '--password-store=basic'] };
seed = await chromium.launchPersistentContext(src, opts);
const p0 = seed.pages()[0] ?? await seed.newPage();
await p0.goto(`${site}/set`); await p0.goto(`${other}/set`);
await seed.close();
seed = null;
const said: string[] = [];
b = await openConsoleBrowser(join(import.meta.dir, '..'), {
store: 'apple', profileDir: tmp('own-profile'), say: m => said.push(m), chromeDir: src, hosts: ['127.0.0.1'], headless: true, platform: 'darwin', launchTimeoutMs: LAUNCH_TIMEOUT_MS,
});
expect(b.mode).toBe('copy');
expect(said.join('\n')).toContain('your Chrome sign-in');
expect(said.join('\n')).not.toContain('sb_test');
expect(b.tempDir && existsSync(b.tempDir)).toBe(true);
await b.page.goto(`${site}/check`);
expect(await b.page.textContent('body')).toBe('cookie: yes');
await b.page.goto(`${other}/check`);
expect(await b.page.textContent('body')).toBe('cookie: no');
await b.context.close();
await b.closed;
expect(existsSync(b.tempDir!)).toBe(false);
} finally {
if (seed) await capped(seed.close(), CLOSE_TIMEOUT_MS);
if (b && !b.isClosed()) { await capped(b.context.close(), CLOSE_TIMEOUT_MS); b.cleanup(); }
server.stop(true);
}
}, 2 * LAUNCH_TIMEOUT_MS); // bounded by the two launches; ~3 s on an idle machine
});