// 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 { 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 { 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 });