| 1 | import assert from "node:assert/strict"; |
| 2 | import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; |
| 3 | import { tmpdir } from "node:os"; |
| 4 | import { join } from "node:path"; |
| 5 | import { test } from "node:test"; |
| 6 | import { |
| 7 | applyBrowserSessionPolicy, |
| 8 | clearBrowserCache, |
| 9 | clearBrowserData, |
| 10 | loadBrowserControlBootstrap, |
| 11 | BrowserControlStore, |
| 12 | type BrowserSession, |
| 13 | } from "./browserControl.js"; |
| 14 | |
| 15 | const home = () => mkdtempSync(join(tmpdir(), "reasonix-browser-control-")); |
| 16 | |
| 17 | type FakeSession = BrowserSession & { |
| 18 | certificateProcs: (((request: unknown, callback: (result: number) => void) => void) | null)[]; |
| 19 | cleared: { cache: number; storage: ({ storages?: string[] } | undefined)[] }; |
| 20 | }; |
| 21 | |
| 22 | function fakeSession(): FakeSession { |
| 23 | return { |
| 24 | certificateProcs: [], |
| 25 | cleared: { cache: 0, storage: [] }, |
| 26 | setCertificateVerifyProc(proc) { |
| 27 | this.certificateProcs.push(proc); |
| 28 | }, |
| 29 | async clearCache() { |
| 30 | this.cleared.cache++; |
| 31 | }, |
| 32 | async clearStorageData(options) { |
| 33 | this.cleared.storage.push(options); |
| 34 | }, |
| 35 | }; |
| 36 | } |
| 37 | |
| 38 | test("defaults keep browser control on and certificate verification strict", () => { |
| 39 | const bootstrap = loadBrowserControlBootstrap(home()); |
| 40 | assert.equal(bootstrap.state.controlEnabled, true); |
| 41 | assert.equal(bootstrap.state.ignoreCertificateErrors, false); |
| 42 | assert.equal(bootstrap.state.writable, true); |
| 43 | assert.equal(existsSync(bootstrap.configPath), false); |
| 44 | }); |
| 45 | |
| 46 | test("persists both switches and rereads them", async () => { |
| 47 | const root = home(); |
| 48 | const bootstrap = loadBrowserControlBootstrap(root); |
| 49 | const store = new BrowserControlStore(bootstrap.configPath, bootstrap); |
| 50 | await store.setControlEnabled(false); |
| 51 | await store.setIgnoreCertificateErrors(true); |
| 52 | assert.deepEqual(JSON.parse(readFileSync(bootstrap.configPath, "utf8")), { |
| 53 | version: 1, |
| 54 | controlEnabled: false, |
| 55 | ignoreCertificateErrors: true, |
| 56 | }); |
| 57 | const reread = new BrowserControlStore(bootstrap.configPath, loadBrowserControlBootstrap(root)); |
| 58 | assert.equal(reread.current.controlEnabled, false); |
| 59 | assert.equal(reread.current.ignoreCertificateErrors, true); |
| 60 | }); |
| 61 | |
| 62 | test("rejects non-boolean patches and keeps the file untouched", async () => { |
| 63 | const root = home(); |
| 64 | const bootstrap = loadBrowserControlBootstrap(root); |
| 65 | const store = new BrowserControlStore(bootstrap.configPath, bootstrap); |
| 66 | await assert.rejects(() => store.setControlEnabled("yes" as unknown as boolean)); |
| 67 | assert.equal(existsSync(bootstrap.configPath), false); |
| 68 | }); |
| 69 | |
| 70 | test("an invalid file is backed up and replaced by a valid one", async () => { |
| 71 | const root = home(); |
| 72 | const path = join(root, "browser-control.json"); |
| 73 | writeFileSync(path, JSON.stringify({ version: 1, controlEnabled: "on", ignoreCertificateErrors: false })); |
| 74 | const bootstrap = loadBrowserControlBootstrap(root); |
| 75 | assert.equal(bootstrap.state.warning, "invalid-config"); |
| 76 | const store = new BrowserControlStore(path, bootstrap); |
| 77 | await store.setControlEnabled(false); |
| 78 | assert.ok(readdirSync(root).some((name) => name.startsWith("browser-control.json.invalid"))); |
| 79 | assert.equal(JSON.parse(readFileSync(path, "utf8")).controlEnabled, false); |
| 80 | assert.equal(store.current.warning, null); |
| 81 | }); |
| 82 | |
| 83 | test("a newer file version is read-only", async () => { |
| 84 | const root = home(); |
| 85 | const path = join(root, "browser-control.json"); |
| 86 | writeFileSync(path, JSON.stringify({ version: 9, controlEnabled: true, ignoreCertificateErrors: false })); |
| 87 | const bootstrap = loadBrowserControlBootstrap(root); |
| 88 | assert.equal(bootstrap.state.warning, "unsupported-version"); |
| 89 | assert.equal(bootstrap.state.writable, false); |
| 90 | await assert.rejects(() => new BrowserControlStore(path, bootstrap).setControlEnabled(false)); |
| 91 | }); |
| 92 | |
| 93 | test("the certificate policy is scoped to the browser session it is applied to", () => { |
| 94 | const relaxed = fakeSession(); |
| 95 | applyBrowserSessionPolicy(relaxed, true); |
| 96 | const proc = relaxed.certificateProcs[0]; |
| 97 | assert.equal(typeof proc, "function"); |
| 98 | let result = -1; |
| 99 | proc?.({}, (value) => { |
| 100 | result = value; |
| 101 | }); |
| 102 | assert.equal(result, 0); |
| 103 | |
| 104 | const strict = fakeSession(); |
| 105 | applyBrowserSessionPolicy(strict, false); |
| 106 | assert.deepEqual(strict.certificateProcs, [null]); |
| 107 | }); |
| 108 | |
| 109 | test("cache clearing keeps sign-in state while clearing everything drops it", async () => { |
| 110 | const cacheOnly = fakeSession(); |
| 111 | await clearBrowserCache(cacheOnly); |
| 112 | assert.equal(cacheOnly.cleared.cache, 1); |
| 113 | assert.deepEqual(cacheOnly.cleared.storage, [{ storages: ["shadercache", "cachestorage", "serviceworkers"] }]); |
| 114 | |
| 115 | const everything = fakeSession(); |
| 116 | await clearBrowserData(everything); |
| 117 | assert.equal(everything.cleared.cache, 1); |
| 118 | assert.deepEqual(everything.cleared.storage, [undefined]); |
| 119 | }); |
| 120 |