mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-27 23:21:53 +02:00
* 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
156 lines
8.7 KiB
TypeScript
156 lines
8.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test';
|
|
import { generatePickerCode, handleCookiePickerRoute, hasActivePicker } from '../src/cookie-picker-routes';
|
|
import * as importer from '../src/cookie-import-browser';
|
|
import * as operation from '../src/cookie-import-operation';
|
|
import * as auth from '../src/cookie-auth-verification';
|
|
|
|
const origin = 'http://127.0.0.1:9470';
|
|
let bm: any;
|
|
let context: any;
|
|
let reads: ReturnType<typeof spyOn>[];
|
|
let reset: ReturnType<typeof spyOn>;
|
|
let verify: ReturnType<typeof spyOn>;
|
|
let previousSelector: string | undefined;
|
|
let previousIdentity: string | undefined;
|
|
|
|
function page(url: string) {
|
|
return { url: () => url, isClosed: () => false, context: () => context } as any;
|
|
}
|
|
|
|
async function request(path: string, cookie?: string, instance?: string, body?: unknown, bearer?: string) {
|
|
const url = new URL(origin + '/cookie-picker' + path);
|
|
return handleCookiePickerRoute(url, new Request(url, {
|
|
method: body === undefined ? 'GET' : 'POST',
|
|
headers: { Origin: origin, 'Content-Type': 'application/json',
|
|
...(cookie ? { Cookie: cookie } : {}),
|
|
...(instance ? { 'X-Gstack-Picker-Instance': instance } : {}),
|
|
...(bearer ? { Authorization: 'Bearer ' + bearer } : {}) },
|
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
}), bm, 'fixture-bearer');
|
|
}
|
|
|
|
async function open(target: any) {
|
|
const code = generatePickerCode({ target: { page: target, url: target.url() } });
|
|
const exchanged = await request('?code=' + code);
|
|
expect(exchanged.status).toBe(302);
|
|
const cookie = exchanged.headers.get('set-cookie')!.split(';')[0];
|
|
const response = await request('', cookie);
|
|
expect(response.status).toBe(200);
|
|
const html = await response.text();
|
|
const config = JSON.parse(html.match(/<script id="picker-config" type="application\/json">(.*?)<\/script>/s)![1]);
|
|
return { cookie, config, html, code };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
previousSelector = process.env.GSTACK_COOKIE_AUTH_SELECTOR;
|
|
previousIdentity = process.env.GSTACK_COOKIE_AUTH_EXPECTED_IDENTITY;
|
|
process.env.GSTACK_COOKIE_AUTH_SELECTOR = '#fixture-identity';
|
|
process.env.GSTACK_COOKIE_AUTH_EXPECTED_IDENTITY = 'Synthetic account';
|
|
context = { addCookies: mock(async () => {}), clearCookies: mock(async () => {}),
|
|
browser: () => ({ browserType: () => ({ name: () => 'chromium' }) }) };
|
|
bm = { getActiveSession: () => ({ getPage: () => page('https://example.test/current') }), trackCookieImportDomains: mock(() => {}) };
|
|
reads = [
|
|
spyOn(importer, 'findInstalledBrowsers').mockReturnValue([{ name: 'Chromium', aliases: ['chromium'] }] as any),
|
|
spyOn(operation, 'getCookieProfiles').mockResolvedValue({ profiles: [{ name: 'Default', displayName: 'Synthetic' }], recommendedProfile: 'Default' }),
|
|
spyOn(importer, 'listDomains').mockReturnValue({ browser: 'Chromium', domains: [{ domain: '.example.test', count: 1 }] }),
|
|
spyOn(importer, 'importCookies').mockResolvedValue({ cookies: [{ name: 'fixture', value: 'synthetic', domain: '.example.test', path: '/',
|
|
expires: -1, secure: true, httpOnly: true, sameSite: 'Lax' }], count: 1, failed: 0, domainCounts: { '.example.test': 1 } }),
|
|
];
|
|
reset = spyOn(auth, 'clearCookieTargetStorage').mockResolvedValue(undefined);
|
|
verify = spyOn(auth, 'verifyCookieAuthentication').mockResolvedValue({ verified: true, reason: 'verified' });
|
|
});
|
|
|
|
afterEach(() => {
|
|
mock.restore();
|
|
if (previousSelector === undefined) delete process.env.GSTACK_COOKIE_AUTH_SELECTOR;
|
|
else process.env.GSTACK_COOKIE_AUTH_SELECTOR = previousSelector;
|
|
if (previousIdentity === undefined) delete process.env.GSTACK_COOKIE_AUTH_EXPECTED_IDENTITY;
|
|
else process.env.GSTACK_COOKIE_AUTH_EXPECTED_IDENTITY = previousIdentity;
|
|
const now = Date.now;
|
|
Date.now = () => now() + 3_900_001;
|
|
try { hasActivePicker(); } finally { Date.now = now; }
|
|
});
|
|
|
|
describe('cookie picker document binding', () => {
|
|
test('renders independent instance identifiers without exposing authentication credentials', async () => {
|
|
const first = await open(page('https://example.test/a'));
|
|
const second = await open(page('https://example.test/b'));
|
|
for (const picker of [first, second]) {
|
|
expect(picker.config.pickerInstance).toMatch(/^[0-9a-f-]{36}$/);
|
|
expect(picker.html).not.toContain(picker.cookie.slice('gstack_picker='.length));
|
|
expect(picker.html).not.toContain(picker.code);
|
|
expect(picker.html).not.toContain('fixture-bearer');
|
|
}
|
|
expect(first.config.pickerInstance).not.toBe(second.config.pickerInstance);
|
|
});
|
|
|
|
test('missing and forged instance headers fail before discovery or import', async () => {
|
|
const picker = await open(page('https://example.test/a'));
|
|
for (const instance of [undefined, 'forged', picker.cookie.slice('gstack_picker='.length)]) {
|
|
for (const [path, payload] of [['/browsers'], ['/import', { browser: 'Chromium', profile: 'Default', domains: ['example.test'], clearStorage: true, verifyAuth: true }]] as const) {
|
|
const response = await request(path, picker.cookie, instance, payload);
|
|
expect(response.status).toBe(403);
|
|
expect((await response.json()).code).toBe('picker_changed');
|
|
}
|
|
}
|
|
for (const read of reads) expect(read).not.toHaveBeenCalled();
|
|
expect(reset).not.toHaveBeenCalled();
|
|
expect(verify).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('a rendered instance identifier never authorizes an unauthenticated request', async () => {
|
|
const picker = await open(page('https://example.test/a'));
|
|
for (const cookie of [undefined, 'gstack_picker=' + picker.config.pickerInstance]) {
|
|
const response = await request('/browsers', cookie, picker.config.pickerInstance);
|
|
expect(response.status).toBe(401);
|
|
expect(await response.json()).toEqual({ error: 'Unauthorized' });
|
|
}
|
|
for (const read of reads) expect(read).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('bearer authorization ignores an unrelated cookie and stale picker identifier', async () => {
|
|
const first = await open(page('https://example.test/a'));
|
|
const second = await open(page('https://example.test/b'));
|
|
const current = page('https://example.test/current');
|
|
bm.getActiveSession = () => ({ getPage: () => current });
|
|
const response = await request('/import', second.cookie, first.config.pickerInstance,
|
|
{ browser: 'Chromium', profile: 'Default', domains: ['example.test'], clearStorage: true, verifyAuth: true }, 'fixture-bearer');
|
|
expect(response.status).toBe(200);
|
|
expect(reset).toHaveBeenCalledWith(current, 'https://example.test');
|
|
expect(verify).toHaveBeenCalledWith(current, { identitySelector: '#fixture-identity', expectedIdentity: 'Synthetic account' }, 'https://example.test');
|
|
});
|
|
|
|
for (const [scenario, firstUrl, secondUrl] of [
|
|
['same-host different-port origins', 'https://example.test:8443/a', 'https://example.test:9443/b'],
|
|
['same-origin different tabs', 'https://example.test/a', 'https://example.test/b'],
|
|
]) {
|
|
test(`rejects an old window before reads or mutations for ${scenario}`, async () => {
|
|
const first = await open(page(firstUrl));
|
|
const target = page(secondUrl);
|
|
const second = await open(target);
|
|
const body = { browser: 'Chromium', profile: 'Default', domains: ['example.test'], clearStorage: true, verifyAuth: true };
|
|
const calls: Array<[string, unknown?]> = [['/browsers'], ['/profiles?browser=Chromium'], ['/domains?browser=Chromium&profile=Default'],
|
|
['/imported'], ['/import', body], ['/remove', { domains: ['example.test'] }]];
|
|
for (const [path, payload] of calls) {
|
|
const response = await request(path, second.cookie, first.config.pickerInstance, payload);
|
|
expect(response.status).toBe(403);
|
|
expect(await response.json()).toMatchObject({ code: 'picker_changed', error: expect.stringContaining('Reopen') });
|
|
}
|
|
for (const read of reads) expect(read).not.toHaveBeenCalled();
|
|
expect(reset).not.toHaveBeenCalled();
|
|
expect(verify).not.toHaveBeenCalled();
|
|
expect(context.addCookies).not.toHaveBeenCalled();
|
|
expect(context.clearCookies).not.toHaveBeenCalled();
|
|
expect(first.config.targetOrigin).toBe(new URL(firstUrl).origin);
|
|
for (const [path, payload] of calls) {
|
|
expect((await request(path, second.cookie, second.config.pickerInstance, payload)).status).toBe(200);
|
|
}
|
|
for (const read of reads) expect(read).toHaveBeenCalledTimes(1);
|
|
expect(reset).toHaveBeenCalledWith(target, new URL(secondUrl).origin);
|
|
expect(verify).toHaveBeenCalledWith(target, { identitySelector: '#fixture-identity', expectedIdentity: 'Synthetic account' }, new URL(secondUrl).origin);
|
|
expect(context.addCookies).toHaveBeenCalledTimes(1);
|
|
expect(context.clearCookies).toHaveBeenCalledTimes(1);
|
|
});
|
|
}
|
|
});
|