experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: strict context + dialogs IPC coverage

+382
+255
backend/electron/tile-context-strict.test.ts
··· 1 + /** 2 + * Unit tests for strict `tile:context:*` capability enforcement. 3 + * 4 + * Covers the pure-helper `checkContextAllowed` used by the 5 + * `tile:context:*` IPC handlers in `tile-ipc.ts`. See 6 + * `docs/tile-preload-trimming-plan.md` §2.5 for the capability 7 + * design and §6 for the test strategy. 8 + * 9 + * Imports from the pure module (`tile-context-enforcement`) so the 10 + * test does not pull in `tile-ipc`'s Electron dependencies. 11 + */ 12 + 13 + import { describe, it } from 'node:test'; 14 + import * as assert from 'node:assert'; 15 + 16 + import { checkContextAllowed } from './tile-context-enforcement.js'; 17 + import { resolveCapabilities } from './tile-manifest.js'; 18 + import type { CapabilityGrant, TileCapabilities } from './tile-manifest.js'; 19 + 20 + // ─── Helpers ───────────────────────────────────────────────────────── 21 + 22 + function makeGrant(caps: TileCapabilities, builtin = false): CapabilityGrant { 23 + return resolveCapabilities('test-tile', caps, builtin); 24 + } 25 + 26 + // ─── Token / Grant Presence ────────────────────────────────────────── 27 + 28 + describe('checkContextAllowed — grant presence', () => { 29 + it('rejects when grant is null (invalid token)', () => { 30 + const result = checkContextAllowed(null, 'get', 'foo'); 31 + assert.strictEqual(result.ok, false); 32 + if (!result.ok) { 33 + assert.match(result.error, /invalid token/i); 34 + } 35 + }); 36 + 37 + it('rejects when grant has no context capability', () => { 38 + const grant = makeGrant({ pubsub: { scopes: ['self'] } }); 39 + const result = checkContextAllowed(grant, 'get', 'foo'); 40 + assert.strictEqual(result.ok, false); 41 + if (!result.ok) { 42 + assert.match(result.error, /context capability not granted/i); 43 + } 44 + }); 45 + 46 + it('rejects when context capability is explicitly false in grant', () => { 47 + const grant: CapabilityGrant = { 48 + tileId: 'test-tile', 49 + capabilities: { context: false as unknown as boolean }, 50 + denied: [], 51 + }; 52 + const result = checkContextAllowed(grant, 'get', 'foo'); 53 + assert.strictEqual(result.ok, false); 54 + }); 55 + }); 56 + 57 + // ─── context: true (unrestricted) ──────────────────────────────────── 58 + 59 + describe('checkContextAllowed — context: true', () => { 60 + it('allows any read key when context is true', () => { 61 + const grant = makeGrant({ context: true }); 62 + for (const key of ['foo', 'mode', 'bar.baz']) { 63 + assert.strictEqual(checkContextAllowed(grant, 'get', key).ok, true); 64 + assert.strictEqual(checkContextAllowed(grant, 'history', key).ok, true); 65 + } 66 + }); 67 + 68 + it('allows any non-mode write key when context is true', () => { 69 + // context: true sugars to {} (no `modes` flag), so writing the 70 + // reserved `mode` key still requires explicit modes: true. 71 + const grant = makeGrant({ context: true }); 72 + assert.strictEqual(checkContextAllowed(grant, 'set', 'foo').ok, true); 73 + assert.strictEqual(checkContextAllowed(grant, 'set', 'whatever').ok, true); 74 + assert.strictEqual(checkContextAllowed(grant, 'set', 'mode').ok, false); 75 + }); 76 + 77 + it('allows snapshot when context is true', () => { 78 + const grant = makeGrant({ context: true }); 79 + assert.strictEqual(checkContextAllowed(grant, 'snapshot').ok, true); 80 + }); 81 + 82 + it('rejects windowsWithValue when queryWindows not set (context: true)', () => { 83 + // context: true does NOT imply queryWindows. 84 + const grant = makeGrant({ context: true }); 85 + assert.strictEqual(checkContextAllowed(grant, 'windowsWithValue').ok, false); 86 + assert.strictEqual(checkContextAllowed(grant, 'windowsInSpace').ok, false); 87 + }); 88 + }); 89 + 90 + // ─── context.read allowlist ────────────────────────────────────────── 91 + 92 + describe('checkContextAllowed — read allowlist', () => { 93 + it('allows read key in allowlist', () => { 94 + const grant = makeGrant({ context: { read: ['mode', 'space'] } }); 95 + assert.strictEqual(checkContextAllowed(grant, 'get', 'mode').ok, true); 96 + assert.strictEqual(checkContextAllowed(grant, 'history', 'space').ok, true); 97 + }); 98 + 99 + it('rejects read key not in allowlist', () => { 100 + const grant = makeGrant({ context: { read: ['mode'] } }); 101 + const result = checkContextAllowed(grant, 'get', 'secrets'); 102 + assert.strictEqual(result.ok, false); 103 + if (!result.ok) { 104 + assert.match(result.error, /not in read allowlist/i); 105 + assert.match(result.error, /secrets/); 106 + } 107 + }); 108 + 109 + it('rejects every key when read allowlist is empty', () => { 110 + const grant = makeGrant({ context: { read: [] } }); 111 + for (const key of ['mode', 'foo']) { 112 + assert.strictEqual(checkContextAllowed(grant, 'get', key).ok, false); 113 + } 114 + }); 115 + 116 + it('omitting read allows any key', () => { 117 + const grant = makeGrant({ context: { write: ['x'] } }); 118 + assert.strictEqual(checkContextAllowed(grant, 'get', 'anything').ok, true); 119 + assert.strictEqual(checkContextAllowed(grant, 'history', 'anything').ok, true); 120 + }); 121 + 122 + it('rejects read without a key when allowlist is present', () => { 123 + const grant = makeGrant({ context: { read: ['mode'] } }); 124 + const result = checkContextAllowed(grant, 'get'); 125 + assert.strictEqual(result.ok, false); 126 + }); 127 + }); 128 + 129 + // ─── context.write allowlist ───────────────────────────────────────── 130 + 131 + describe('checkContextAllowed — write allowlist', () => { 132 + it('allows write key in allowlist', () => { 133 + const grant = makeGrant({ context: { write: ['entities'] } }); 134 + assert.strictEqual(checkContextAllowed(grant, 'set', 'entities').ok, true); 135 + }); 136 + 137 + it('rejects write key not in allowlist', () => { 138 + const grant = makeGrant({ context: { write: ['entities'] } }); 139 + const result = checkContextAllowed(grant, 'set', 'secrets'); 140 + assert.strictEqual(result.ok, false); 141 + if (!result.ok) { 142 + assert.match(result.error, /not in write allowlist/i); 143 + } 144 + }); 145 + 146 + it('rejects every write when write allowlist is empty', () => { 147 + const grant = makeGrant({ context: { write: [] } }); 148 + assert.strictEqual(checkContextAllowed(grant, 'set', 'x').ok, false); 149 + }); 150 + 151 + it('rejects set without a key', () => { 152 + const grant = makeGrant({ context: true }); 153 + const result = checkContextAllowed(grant, 'set'); 154 + assert.strictEqual(result.ok, false); 155 + }); 156 + }); 157 + 158 + // ─── mode key special case ─────────────────────────────────────────── 159 + 160 + describe('checkContextAllowed — mode key requires modes flag', () => { 161 + it('rejects writing mode when modes flag is false', () => { 162 + const grant = makeGrant({ context: { write: ['mode'] } }); 163 + const result = checkContextAllowed(grant, 'set', 'mode'); 164 + assert.strictEqual(result.ok, false); 165 + if (!result.ok) { 166 + assert.match(result.error, /modes capability required/i); 167 + } 168 + }); 169 + 170 + it('allows writing mode when both write allowlist and modes flag set', () => { 171 + const grant = makeGrant({ context: { write: ['mode'], modes: true } }); 172 + assert.strictEqual(checkContextAllowed(grant, 'set', 'mode').ok, true); 173 + }); 174 + 175 + it('allows writing mode when context is unrestricted and modes: true', () => { 176 + const grant = makeGrant({ context: { modes: true } }); 177 + assert.strictEqual(checkContextAllowed(grant, 'set', 'mode').ok, true); 178 + }); 179 + 180 + it('reading mode does not require modes flag', () => { 181 + const grant = makeGrant({ context: { read: ['mode'] } }); 182 + assert.strictEqual(checkContextAllowed(grant, 'get', 'mode').ok, true); 183 + assert.strictEqual(checkContextAllowed(grant, 'history', 'mode').ok, true); 184 + }); 185 + }); 186 + 187 + // ─── queryWindows ──────────────────────────────────────────────────── 188 + 189 + describe('checkContextAllowed — queryWindows', () => { 190 + it('allows windowsWithValue with queryWindows: true', () => { 191 + const grant = makeGrant({ context: { queryWindows: true } }); 192 + assert.strictEqual(checkContextAllowed(grant, 'windowsWithValue').ok, true); 193 + assert.strictEqual(checkContextAllowed(grant, 'windowsInSpace').ok, true); 194 + }); 195 + 196 + it('rejects windowsWithValue without queryWindows', () => { 197 + const grant = makeGrant({ context: { read: ['mode'] } }); 198 + const result = checkContextAllowed(grant, 'windowsWithValue'); 199 + assert.strictEqual(result.ok, false); 200 + if (!result.ok) { 201 + assert.match(result.error, /queryWindows capability required/i); 202 + } 203 + }); 204 + 205 + it('rejects windowsInSpace without queryWindows', () => { 206 + const grant = makeGrant({ context: { read: ['mode'] } }); 207 + assert.strictEqual(checkContextAllowed(grant, 'windowsInSpace').ok, false); 208 + }); 209 + }); 210 + 211 + // ─── snapshot ──────────────────────────────────────────────────────── 212 + 213 + describe('checkContextAllowed — snapshot', () => { 214 + it('allows snapshot whenever context capability is present', () => { 215 + assert.strictEqual(checkContextAllowed(makeGrant({ context: true }), 'snapshot').ok, true); 216 + assert.strictEqual(checkContextAllowed(makeGrant({ context: {} }), 'snapshot').ok, true); 217 + assert.strictEqual(checkContextAllowed(makeGrant({ context: { read: [] } }), 'snapshot').ok, true); 218 + }); 219 + 220 + it('rejects snapshot without context capability', () => { 221 + const grant = makeGrant({ pubsub: { scopes: ['self'] } }); 222 + assert.strictEqual(checkContextAllowed(grant, 'snapshot').ok, false); 223 + }); 224 + }); 225 + 226 + // ─── Grant-resolution interop ──────────────────────────────────────── 227 + 228 + describe('resolveCapabilities — context shape preservation', () => { 229 + it('preserves context: true across resolution', () => { 230 + const grant = resolveCapabilities('t', { context: true }, false); 231 + assert.strictEqual(grant.capabilities.context, true); 232 + }); 233 + 234 + it('preserves context object with lists and flags across resolution', () => { 235 + const grant = resolveCapabilities( 236 + 't', 237 + { context: { read: ['mode'], write: ['mode'], modes: true, queryWindows: true } }, 238 + false, 239 + ); 240 + const cc = grant.capabilities.context; 241 + assert.ok(cc && typeof cc === 'object', 'context should remain an object'); 242 + const obj = cc as { read?: string[]; write?: string[]; modes?: boolean; queryWindows?: boolean }; 243 + assert.deepStrictEqual(obj.read, ['mode']); 244 + assert.deepStrictEqual(obj.write, ['mode']); 245 + assert.strictEqual(obj.modes, true); 246 + assert.strictEqual(obj.queryWindows, true); 247 + }); 248 + 249 + it('end-to-end: resolved grant enforces write allowlist at IPC enforcement point', () => { 250 + const caps: TileCapabilities = { context: { write: ['entities'] } }; 251 + const grant = resolveCapabilities('my-tile', caps, false); 252 + assert.strictEqual(checkContextAllowed(grant, 'set', 'entities').ok, true); 253 + assert.strictEqual(checkContextAllowed(grant, 'set', 'rogue').ok, false); 254 + }); 255 + });
+127
backend/electron/tile-dialogs-strict.test.ts
··· 1 + /** 2 + * Unit tests for strict `tile:dialogs:*` capability enforcement. 3 + * 4 + * Covers the pure-helper `checkDialogAllowed` used by the 5 + * `tile:dialogs:save` / `tile:dialogs:open` IPC handlers in 6 + * `tile-ipc.ts`. See `docs/tile-preload-trimming-plan.md` §2.4. 7 + * 8 + * Imports from the pure module (`tile-dialogs-enforcement`) so the 9 + * test does not pull in `tile-ipc`'s Electron dependencies. 10 + */ 11 + 12 + import { describe, it } from 'node:test'; 13 + import * as assert from 'node:assert'; 14 + 15 + import { checkDialogAllowed } from './tile-dialogs-enforcement.js'; 16 + import { resolveCapabilities } from './tile-manifest.js'; 17 + import type { CapabilityGrant, TileCapabilities } from './tile-manifest.js'; 18 + 19 + // ─── Helpers ───────────────────────────────────────────────────────── 20 + 21 + function makeGrant(caps: TileCapabilities, builtin = false): CapabilityGrant { 22 + return resolveCapabilities('test-tile', caps, builtin); 23 + } 24 + 25 + // ─── Token / Grant Presence ────────────────────────────────────────── 26 + 27 + describe('checkDialogAllowed — grant presence', () => { 28 + it('rejects when grant is null (invalid token)', () => { 29 + const result = checkDialogAllowed(null, 'save'); 30 + assert.strictEqual(result.ok, false); 31 + if (!result.ok) { 32 + assert.match(result.error, /invalid token/i); 33 + } 34 + }); 35 + 36 + it('rejects when grant has no dialogs capability', () => { 37 + const grant = makeGrant({ pubsub: { scopes: ['self'] } }); 38 + const result = checkDialogAllowed(grant, 'save'); 39 + assert.strictEqual(result.ok, false); 40 + if (!result.ok) { 41 + assert.match(result.error, /dialogs capability not granted/i); 42 + } 43 + }); 44 + 45 + it('rejects when dialogs capability is explicitly false in grant', () => { 46 + const grant: CapabilityGrant = { 47 + tileId: 'test-tile', 48 + capabilities: { dialogs: false as unknown as boolean }, 49 + denied: [], 50 + }; 51 + assert.strictEqual(checkDialogAllowed(grant, 'save').ok, false); 52 + }); 53 + }); 54 + 55 + // ─── dialogs: true (unrestricted) ──────────────────────────────────── 56 + 57 + describe('checkDialogAllowed — dialogs: true', () => { 58 + it('allows save when dialogs is true', () => { 59 + const grant = makeGrant({ dialogs: true }); 60 + assert.strictEqual(checkDialogAllowed(grant, 'save').ok, true); 61 + }); 62 + 63 + it('allows open when dialogs is true', () => { 64 + const grant = makeGrant({ dialogs: true }); 65 + assert.strictEqual(checkDialogAllowed(grant, 'open').ok, true); 66 + }); 67 + }); 68 + 69 + // ─── dialogs.types allowlist ───────────────────────────────────────── 70 + 71 + describe('checkDialogAllowed — types allowlist', () => { 72 + it('allows a type that appears in the types allowlist', () => { 73 + const grant = makeGrant({ dialogs: { types: ['save'] } }); 74 + assert.strictEqual(checkDialogAllowed(grant, 'save').ok, true); 75 + }); 76 + 77 + it('rejects a type not in the types allowlist', () => { 78 + const grant = makeGrant({ dialogs: { types: ['save'] } }); 79 + const result = checkDialogAllowed(grant, 'open'); 80 + assert.strictEqual(result.ok, false); 81 + if (!result.ok) { 82 + assert.match(result.error, /not in capability types allowlist/i); 83 + assert.match(result.error, /open/); 84 + } 85 + }); 86 + 87 + it('rejects every type when allowlist is empty', () => { 88 + const grant = makeGrant({ dialogs: { types: [] } }); 89 + assert.strictEqual(checkDialogAllowed(grant, 'save').ok, false); 90 + assert.strictEqual(checkDialogAllowed(grant, 'open').ok, false); 91 + }); 92 + 93 + it('allows both types when both are in the allowlist', () => { 94 + const grant = makeGrant({ dialogs: { types: ['save', 'open'] } }); 95 + assert.strictEqual(checkDialogAllowed(grant, 'save').ok, true); 96 + assert.strictEqual(checkDialogAllowed(grant, 'open').ok, true); 97 + }); 98 + 99 + it('allows any type when object form omits types', () => { 100 + const grant = makeGrant({ dialogs: {} }); 101 + assert.strictEqual(checkDialogAllowed(grant, 'save').ok, true); 102 + assert.strictEqual(checkDialogAllowed(grant, 'open').ok, true); 103 + }); 104 + }); 105 + 106 + // ─── Grant-resolution interop ──────────────────────────────────────── 107 + 108 + describe('resolveCapabilities — dialogs shape preservation', () => { 109 + it('preserves dialogs: true across resolution', () => { 110 + const grant = resolveCapabilities('t', { dialogs: true }, false); 111 + assert.strictEqual(grant.capabilities.dialogs, true); 112 + }); 113 + 114 + it('preserves dialogs object (with types) across resolution', () => { 115 + const grant = resolveCapabilities('t', { dialogs: { types: ['save'] } }, false); 116 + const dc = grant.capabilities.dialogs; 117 + assert.ok(dc && typeof dc === 'object', 'dialogs should remain an object'); 118 + assert.deepStrictEqual((dc as { types: string[] }).types, ['save']); 119 + }); 120 + 121 + it('end-to-end: resolved grant enforces types allowlist at IPC enforcement point', () => { 122 + const caps: TileCapabilities = { dialogs: { types: ['save'] } }; 123 + const grant = resolveCapabilities('my-tile', caps, false); 124 + assert.strictEqual(checkDialogAllowed(grant, 'save').ok, true); 125 + assert.strictEqual(checkDialogAllowed(grant, 'open').ok, false); 126 + }); 127 + });