Files
gstack/browse/test/cookie-import-reliability.test.ts
T
Garry Tan a84b0b5b6d v1.90.0.0 feat: make browser cookie imports explicit and safe (#2964)
* fix(browse): prepare reliable cookie import wave for validation

* ci: sequence quality and behavior for validation branch

* fix(browse): isolate Windows qualification and preserve native diagnostics

* test(browse): cover cookie workflow quality and isolate Windows user paths

* test(browse): trace native member startup and initialize fresh folders

* fix(browse): keep Windows member stdin alive through EOF

* fix(browse): latch native timeouts and compare contained Edge startup

* test(browse): verify native version metadata and actual Windows argv

* test(browse): qualify Dia import on isolated macOS CI

* fix(browse): require picker origin for session mutations

* fix(browse): bound credential reads through stream completion

* test(browse): inspect owned Windows process arguments natively

* test(evals): preserve passing coverage during cookie repair reruns

* test(browse): isolate Dia qualification in a fresh macOS account

* test(browse): pass bounded integer timeouts to native Mac probes

* test(browse): distinguish Windows profile initialization from containment

* test(browse): await descendant pipe readiness before parent exit

* test(browse): initialize and restore isolated macOS Keychain state

* test(browse): initialize Windows fixture folders before qualification

* test(ci): pin the same Node runtime across Windows checks

* test(browse): distinguish native macOS browser preflight stages

* test(browse): isolate Windows descendant console lifetime

* test(browse): preserve native receipts and identify fixture lock holders

* test(browse): prepare dependency resolution before native Mac worker startup

* test(ci): include lock and close checks in native diagnostics

* test(browse): preserve native owner probe stages and subprocess deadlines

* fix(browse): classify Chromium profile-in-use exit precisely

* test(browse): retain Mac qualification evidence through cleanup failures

* test(browse): bound Mac fixture paths and retire its owned user domain

* test(browse): accept vanished fixture entries without weakening cleanup

* test(browse): identify probe-created macOS user domains safely

* test(browse): observe Mac user domains without targeting them first

* test(browse): use passive fresh-user ownership throughout Mac qualification

* test(browse): distinguish profile and registered-home Keychain lookups

* test(browse): qualify Dia under one registered account home

* test(browse): identify Dia startup and owned process-group failures

* test(browse): classify bounded Dia startup diagnostics without leaking output

* fix(test): preserve native Mac sandboxing and reap owned browser children

* fix(browse): preserve Chromium sandboxing for native profile imports

* test(browse): inspect signed Mach-O architecture without launching Xcode tools

* test(browse): sample pending Dia startup and reap on all cleanup paths

* test(browse): compare protected Dia launches in fresh Bun and Node accounts

* test(browse): inspect isolated Mac GUI readiness without browser access

* v1.90.0.0 fix: bind cookie picker actions to their document

* test: validate cookie guards and fit nested launch fixtures

* ci: configure the bundled Chromium sandbox helper

* fix(browse): classify Playwright authentication timeouts

* test: retain bounded Windows lifecycle diagnostics

* test(cso): reuse bounded NTFS precision candidates

* test(review): handle explicit preservation choices safely

* test(browse): remove owned fixture directories with explicit primitives

* test(review): distinguish descriptive reuse from edit commitments

* test: admit only the approved unscored cookie workflow refusal

* test: keep the Office Hours judge mock export-complete

* fix: keep dependency-free CI planners independent of the model SDK

* test: observe the exact holder after a native fixture unlink failure

* fix: start seeded PTY observations at owned readiness

* test: acquire identity-bound Windows deletion admission before profile resets

* test: preserve qualified Git index bits without authorizing mutations
2026-09-25 12:06:45 -04:00

188 lines
9.1 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
import { Database } from 'bun:sqlite';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { findInstalledBrowsers, importCookies, listProfiles, cookieDomainMatches, CookieImportError, normalizeCookieDomain, withCookieReadRetry } from '../src/cookie-import-browser';
let home: string;
let oldHome: string | undefined;
let oldUserProfile: string | undefined;
let spawn: typeof Bun.spawn;
let homeMock: ReturnType<typeof spyOn>;
function profile(dir: string, name: string, cookieDomain = '.example.test') {
const target = path.join(home, dir, name);
fs.mkdirSync(target, { recursive: true });
expect(fs.realpathSync(target).startsWith(fs.realpathSync(home) + path.sep)).toBe(true);
const db = new Database(path.join(target, 'Cookies'));
db.run('CREATE TABLE cookies (host_key TEXT, name TEXT, value TEXT, encrypted_value BLOB, path TEXT, expires_utc INTEGER, is_secure INTEGER, is_httponly INTEGER, has_expires INTEGER, samesite INTEGER)');
db.run('INSERT INTO cookies VALUES (?, ?, ?, ?, ?, 0, 1, 1, 0, 1)', [cookieDomain, 'fixture', 'synthetic-value', Buffer.alloc(0), '/']);
db.close();
return target;
}
beforeEach(() => {
home = fs.mkdtempSync(path.join(os.tmpdir(), 'cookie-wave-'));
oldHome = process.env.HOME;
oldUserProfile = process.env.USERPROFILE;
process.env.HOME = home;
process.env.USERPROFILE = home;
homeMock = spyOn(os, 'homedir').mockReturnValue(home);
spawn = Bun.spawn;
Bun.spawn = ((command: string[]) => {
if (!['secret-tool', 'security'].includes(command[0])) throw new Error('Unexpected fixture subprocess');
return { stdout: new Blob(['fixture-password']).stream(), stderr: new Blob([]).stream(), exited: Promise.resolve(0), kill() {} };
}) as typeof Bun.spawn;
});
afterEach(() => {
Bun.spawn = spawn;
homeMock.mockRestore();
if (oldHome === undefined) delete process.env.HOME; else process.env.HOME = oldHome;
if (oldUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = oldUserProfile;
fs.rmSync(home, { recursive: true, force: true });
});
describe('cookie import reliability', () => {
test('discovers Dia with only a numbered macOS profile', () => {
profile('Library/Application Support/Dia/User Data', 'Profile 2');
expect(findInstalledBrowsers().map(browser => browser.name)).toContain('Dia');
expect(listProfiles('dia')[0].name).toBe('Profile 2');
});
test('prefers current Local State names and sorts profile numbers naturally', () => {
const root = '.config/chromium';
for (const name of ['Profile 10', 'Profile 2', 'Default']) {
const dir = profile(root, name);
fs.writeFileSync(path.join(dir, 'Preferences'), JSON.stringify({ profile: { name: 'Old name' } }));
}
fs.writeFileSync(path.join(home, root, 'Local State'), JSON.stringify({ profile: { info_cache: { 'Profile 2': { name: 'Current name' } } } }));
expect(listProfiles('chromium')).toEqual([
{ name: 'Default', displayName: 'Old name' },
{ name: 'Profile 2', displayName: 'Current name' },
{ name: 'Profile 10', displayName: 'Old name' },
]);
});
test('malformed metadata preserves the directory identity', () => {
const dir = profile('.config/chromium', 'Default');
fs.writeFileSync(path.join(home, '.config/chromium/Local State'), '{');
fs.writeFileSync(path.join(dir, 'Preferences'), '{');
expect(listProfiles('chromium')).toEqual([{ name: 'Default', displayName: 'Default' }]);
});
test('bare and dotted domain selection import the same stored row without widening scope', async () => {
const dir = profile('.config/chromium', 'Default');
const db = new Database(path.join(dir, 'Cookies'));
db.run("INSERT INTO cookies VALUES ('evil-example.test', 'other', 'synthetic-other', x'', '/', 0, 1, 1, 0, 1)");
db.close();
for (const domain of ['example.test', '.example.test', 'EXAMPLE.TEST.']) {
const result = await importCookies('chromium', [domain]);
expect(result.count).toBe(1);
expect(result.cookies[0].domain).toBe('.example.test');
}
});
test('reports partial decrypt reasons without error or cookie values', async () => {
const dir = profile('.config/chromium', 'Default');
const db = new Database(path.join(dir, 'Cookies'));
db.run("INSERT INTO cookies VALUES ('.example.test', 'broken', '', ?, '/', 0, 1, 1, 0, 1)", [Buffer.from('v20synthetic')]);
db.close();
const result = await importCookies('chromium', ['example.test']);
expect(result.count).toBe(1);
expect(result.failed).toBe(1);
expect(result.failureReasons).toEqual({ unsupported_encryption: 1 });
});
test('domain counts do not inherit object prototype properties', async () => {
profile('.config/chromium', 'Default', 'constructor');
const result = await importCookies('chromium', ['constructor']);
expect(result.count).toBe(1);
expect(result.domainCounts.constructor).toBe(1);
expect(JSON.stringify(result.domainCounts)).toBe('{"constructor":1}');
});
test('domain matching preserves host-only boundaries and rejects malformed input', () => {
expect(cookieDomainMatches('app.example.test', '.example.test')).toBe(true);
expect(cookieDomainMatches('app.example.test', 'example.test')).toBe(false);
expect(cookieDomainMatches('evil-example.test', '.example.test')).toBe(false);
expect(normalizeCookieDomain('.EXAMPLE.TEST.')).toBe('example.test');
expect(normalizeCookieDomain('service_name.example.test')).toBe('service_name.example.test');
expect(normalizeCookieDomain('-service.example.test')).toBe('-service.example.test');
expect(normalizeCookieDomain('::1')).toBe('[::1]');
expect(normalizeCookieDomain('[0:0:0:0:0:0:0:1]')).toBe('[::1]');
expect(cookieDomainMatches('[::1]', '[::1]')).toBe(true);
for (const domain of ['', 'https://example.test', 'example.test/path', 'user@example.test', '..example.test', 'example.test:80', '*.example.test']) {
expect(() => normalizeCookieDomain(domain)).toThrow(CookieImportError);
}
});
for (const domain of ['service_name.example.test', '[::1]', '-service.example.test']) {
test(`imports the Chromium-supported hostname ${domain}`, async () => {
profile('.config/chromium', 'Default', domain);
const result = await importCookies('chromium', [domain]);
expect(result.count).toBe(1);
expect(result.cookies[0].domain).toBe(domain);
});
}
test('does not automatically retry permission denial or corrupt databases', async () => {
for (const code of ['keychain_denied', 'keychain_timeout', 'db_corrupt']) {
let attempts = 0;
await expect(withCookieReadRetry(() => {
attempts++;
throw new CookieImportError('Safe fixture error', code, 'retry');
})).rejects.toThrow('Safe fixture error');
expect(attempts).toBe(1);
}
});
test('normalizes adapter failures without exposing database error text', async () => {
for (const [source, expected] of [['SQLITE_CORRUPT', 'db_corrupt'], ['SQLITE_READONLY', 'db_permission'], ['SQLITE_ERROR', 'db_read_error']]) {
let caught: any;
try { await withCookieReadRetry(() => { throw Object.assign(new Error('synthetic-private-db-detail'), { code: source }); }); }
catch (error) { caught = error; }
expect(caught).toBeInstanceOf(CookieImportError);
expect(caught.code).toBe(expected);
expect(caught.message).not.toContain('synthetic-private-db-detail');
}
});
test('actual Keychain denial is sanitized, never repeated, and clears its deadline', async () => {
const dir = profile('Library/Application Support/Dia/User Data', 'Default');
const db = new Database(path.join(dir, 'Cookies'));
db.run("UPDATE cookies SET value = '', encrypted_value = ?", [Buffer.from('v10synthetic-encrypted-row')]);
db.close();
let requests = 0;
Bun.spawn = ((command: string[]) => {
expect(command).toEqual(['security', 'find-generic-password', '-s', 'Dia Safe Storage', '-w']);
requests++;
return { stdout: new Blob([]).stream(), stderr: new Blob(['user canceled synthetic-private-detail']).stream(), exited: Promise.resolve(1), kill() {} };
}) as typeof Bun.spawn;
const timer = spyOn(globalThis, 'setTimeout').mockReturnValue(123 as any);
const clear = spyOn(globalThis, 'clearTimeout').mockImplementation(() => {});
try {
let error: any;
try { await withCookieReadRetry(() => importCookies('dia', ['example.test'])); } catch (caught) { error = caught; }
expect(error).toBeInstanceOf(CookieImportError);
expect(error.code).toBe('keychain_denied');
expect(error.message).not.toContain('synthetic-private-detail');
expect(requests).toBe(1);
expect(clear).toHaveBeenCalledWith(123);
} finally {
timer.mockRestore();
clear.mockRestore();
}
});
test('bounds transient database retries to three attempts', async () => {
let attempts = 0;
await expect(withCookieReadRetry(() => {
attempts++;
throw new CookieImportError('Locked', 'db_locked', 'retry');
})).rejects.toThrow('Locked');
expect(attempts).toBe(3);
});
});