// storebound console fill: which browser it uses. Pure parts (data dir, profile pick, file selection, cookie host // filter, DevToolsActivePort, the opt-in flag, the browser plan, Google's rejected page, the launch switches) and the // session copy on throwaway folders (cleanup on success and on error). Safety: the default run never copies Google // (or Apple) cookies; the opt-in copy of Google cookies warns about device-bound sessions. Integration (real Google // Chrome, throwaway profiles, a local fixture site): the window storebound starts has navigator.webdriver false and no // automation switch on its real command line (chrome://version); the opt-in copy carries a cookie of the source // profile and drops another host's. 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 { APPLE_COPY_NOTE, AUTOMATION_SWITCHES, browserPlan, chromeDataDir, consoleChromeArgs, CONSOLE_HOSTS, cookieApplies, copySession, devToolsEndpoint, filterCookies, GOOGLE_COPY_WARNING, GOOGLE_REJECTED, googleRejected, NEVER_COPY, openConsoleBrowser, parseUseChromeSession, pickProfile, PROVIDER_HOSTS, sessionFiles } from '../cli/console/session'; import { sandboxArgs } from '../cli/shots/browser'; 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('Google session safety (device-bound cookies)', () => { const none = { storeboundChrome: null, chrome: null, chromium: null }; const copies = () => readdirSync(tmpdir()).filter(f => f.startsWith('storebound-chrome-')).length; test('--use-chrome-session: google | apple only; absent = no copy', () => { expect(parseUseChromeSession(undefined)).toEqual([]); expect(parseUseChromeSession('google')).toEqual(['google']); expect(parseUseChromeSession('Apple, google,apple')).toEqual(['apple', 'google']); expect(() => parseUseChromeSession(true)).toThrow('--use-chrome-session'); expect(() => parseUseChromeSession('all')).toThrow('--use-chrome-session'); }); test('plan: no copy by default for either store; an opt-in copies only the form\'s own provider', () => { for (const store of ['play', 'apple'] as const) expect(browserPlan({ store, platform: 'darwin' }, true).copy).toBeNull(); expect(browserPlan({ store: 'play', platform: 'darwin', useChromeSession: ['google'] }, true).copy).toBe('google'); expect(browserPlan({ store: 'play', platform: 'darwin', useChromeSession: ['apple'] }, true).copy).toBeNull(); expect(browserPlan({ store: 'apple', platform: 'darwin', useChromeSession: ['google'] }, true).copy).toBeNull(); expect(browserPlan({ store: 'apple', platform: 'darwin', useChromeSession: ['apple'] }, true).copy).toBe('apple'); expect(browserPlan({ store: 'play', platform: 'darwin', useChromeSession: ['google'], fresh: true }, true).copy).toBeNull(); expect(browserPlan({ store: 'play', platform: 'linux', useChromeSession: ['google'] }, true).copy).toBeNull(); // an Apple copy never carries a Google cookie and the other way round expect(PROVIDER_HOSTS.apple.some(h => h.includes('google'))).toBe(false); expect(PROVIDER_HOSTS.google.some(h => h.includes('apple'))).toBe(false); }); test('the default run makes no copy of the owner\'s Chrome cookies (play and apple)', async () => { const src = await fakeChrome('chrome-default'); for (const store of ['play', 'apple'] as const) { const said: string[] = []; const before = copies(); const e = await openConsoleBrowser(scratch, { store, profileDir: tmp(`own-${store}`), say: m => said.push(m), chromeDir: src, platform: 'darwin', launchDeps: { installed: none } }).catch(x => x as Error); expect((e as Error).message).toContain('no usable browser'); // it went straight to storebound's own profile expect(said.join('\n')).not.toMatch(/copied|Chrome sign-in|WARNING/); expect(copies()).toBe(before); } }); test('the opt-in Google copy warns about device-bound sessions first; the Apple one says its note', async () => { const src = await fakeChrome('chrome-optin'); const said: string[] = []; await openConsoleBrowser(scratch, { store: 'play', useChromeSession: ['google'], profileDir: tmp('own-optin'), say: m => said.push(m), chromeDir: src, platform: 'darwin', launchDeps: { installed: none } }).catch(() => null); expect(said[0]).toBe(GOOGLE_COPY_WARNING); expect(GOOGLE_COPY_WARNING).toContain('sign you out of your real Chrome'); expect(said.join('\n')).toContain('could not use your Chrome sign-in'); // no Google Chrome here: the copy is removed again const saidA: string[] = []; await openConsoleBrowser(scratch, { store: 'apple', useChromeSession: ['apple'], profileDir: tmp('own-optin-a'), say: m => saidA.push(m), chromeDir: src, platform: 'darwin', launchDeps: { installed: none } }).catch(() => null); expect(saidA[0]).toBe(APPLE_COPY_NOTE); }); test('Google\'s rejected page is recognised (and only that page)', () => { for (const u of ['https://accounts.google.com/v3/signin/rejected?continue=x', 'https://accounts.google.com/signin/rejected', 'https://accounts.google.com/signin/v2/rejected']) expect(googleRejected(u)).toBe(true); for (const u of ['https://accounts.google.com/v3/signin/accountchooser', 'https://accounts.google.com/v3/signin/identifier', 'https://play.google.com/console/signin/rejected', 'https://example.com/v3/signin/rejected', 'not a url']) expect(googleRejected(u)).toBe(false); expect(GOOGLE_REJECTED).toBe('Google refused sign-in in this window (/signin/rejected). storebound uses a normal Chrome window; try again, or sign in at accounts.google.com first in this window.'); }); test('launch switches: no automation switch, webdriver off, session restore, a DevTools port the OS picks', () => { const a = consoleChromeArgs('/p'); for (const s of AUTOMATION_SWITCHES) expect(a.some(x => x.startsWith(s))).toBe(false); expect(a).toContain('--disable-blink-features=AutomationControlled'); expect(a).toContain('--restore-last-session'); expect(a).toContain('--remote-debugging-port=0'); expect(a).toContain('--user-data-dir=/p'); expect(a.some(x => /user-agent/i.test(x))).toBe(false); expect(consoleChromeArgs('/p', { headless: true })).toContain('--headless=new'); expect(consoleChromeArgs('/p', { sandbox: [] })).not.toContain('--no-sandbox'); expect(consoleChromeArgs('/p', { sandbox: ['--no-sandbox'] })).toContain('--no-sandbox'); }); test('--no-sandbox only for root on Linux or inside a container; never on macOS, Windows or for a normal user', () => { const no = () => false, yes = () => true; expect(sandboxArgs('linux', 0, no)).toEqual(['--no-sandbox']); expect(sandboxArgs('linux', 1000, yes)).toEqual(['--no-sandbox']); expect(sandboxArgs('linux', 1000, no)).toEqual([]); expect(sandboxArgs('darwin', 0, yes)).toEqual([]); expect(sandboxArgs('darwin', 501, no)).toEqual([]); expect(sandboxArgs('win32', undefined, yes)).toEqual([]); }); }); 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.concurrent('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', useChromeSession: ['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[0]).toBe(APPLE_COPY_NOTE); 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.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.close(), CLOSE_TIMEOUT_MS); b.cleanup(); } server.stop(true); } }, 2 * LAUNCH_TIMEOUT_MS); // bounded by the two launches; ~3 s on an idle machine }); describe.skipIf(!hasChrome)('the console window has no automation markers (real Chrome, throwaway profile, local page)', () => { test.concurrent('navigator.webdriver is false and the real command line has no --enable-automation', async () => { const server = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: () => new Response( '

', { headers: { 'content-type': 'text/html' } }) }); let b: any = null; try { b = await openConsoleBrowser(join(import.meta.dir, '..'), { store: 'play', profileDir: tmp('own-webdriver'), say: () => {}, headless: true, platform: 'darwin', chromeDir: tmp('no-chrome-here'), launchTimeoutMs: LAUNCH_TIMEOUT_MS, }); expect(b.mode).toBe('profile'); for (const s of AUTOMATION_SWITCHES) expect(b.args.some((x: string) => x.startsWith(s))).toBe(false); await b.page.goto(`http://127.0.0.1:${server.port}/`); const r = JSON.parse(await b.page.textContent('#r')); expect(r.webdriver).toBe(false); expect(r.ua).toContain('Chrome/'); await b.page.goto('chrome://version'); const cmd: string = await b.page.textContent('#command_line'); expect(cmd).toContain('--user-data-dir='); expect(cmd).not.toContain('--enable-automation'); expect(cmd).not.toContain('--remote-debugging-pipe'); expect(cmd).toContain('--disable-blink-features=AutomationControlled'); await b.close(); await b.closed; expect(b.isClosed()).toBe(true); } finally { if (b && !b.isClosed()) await capped(b.close(), CLOSE_TIMEOUT_MS); server.stop(true); } }, 2 * LAUNCH_TIMEOUT_MS); });