screentinker/server/test/agency-zone-grants.test.js
ScreenTinker 289d54f4fa feat(api): zone-grant confinement for agency tokens - FK-anchored (#73)
Placement-as-grant, replacing the inferred auto-place idea. api_token_target_zones is an
ADDITIVE second table (does NOT touch the proven api_token_targets), structurally anchored:
a composite FK to api_token_targets(token_id, playlist_id) makes a zone grant orphan-
impossible and cascade away when the playlist grant is revoked - "narrow" is structural, not
conventional. zone_id FK -> layout_zones cascades on zone/layout delete.

Confinement (lib/agency-targets.resolveGrantedZone, called in the item-add): grants exist ->
the item MUST land in a granted zone (a body zone_id picks among grants, never escapes them);
none -> whole-playlist/full-screen as before. The item-add stamps the granted zone_id.

Bite-tested (6, all proven incl. neutralize->red on the confinement): granted YES; non-
granted/cross-playlist/ambiguous blocked; orphan-grant rejected by the FK; cascade on
playlist-grant revoke, on playlist delete, on zone/layout delete; and foreign_keys=ON
asserted (a cascade that no-ops because FKs are off is the trap). 153 suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:57:27 -05:00

92 lines
5.1 KiB
JavaScript

'use strict';
// #73 zone-grant security model. Proves the structural narrow guarantee BEFORE any UI rides
// on it: zone grants confine placement, are FK-anchored to the playlist grant (orphan-
// impossible), and cascade away with it. The whole thing depends on PRAGMA foreign_keys=ON,
// so this test asserts that too (a cascade that silently no-ops because FKs are off is the trap).
const { test } = require('node:test');
const assert = require('node:assert/strict');
const Database = require('better-sqlite3');
const { resolveGrantedZone } = require('../lib/agency-targets');
function freshDb() {
const db = new Database(':memory:');
db.pragma('foreign_keys = ON'); // mirrors db/database.js:13
db.exec(`
CREATE TABLE api_tokens (id TEXT PRIMARY KEY);
CREATE TABLE playlists (id TEXT PRIMARY KEY, workspace_id TEXT);
CREATE TABLE layouts (id TEXT PRIMARY KEY);
CREATE TABLE layout_zones (id TEXT PRIMARY KEY, layout_id TEXT REFERENCES layouts(id) ON DELETE CASCADE);
CREATE TABLE api_token_targets (
token_id TEXT NOT NULL REFERENCES api_tokens(id) ON DELETE CASCADE,
playlist_id TEXT NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
PRIMARY KEY (token_id, playlist_id));
CREATE TABLE api_token_target_zones (
token_id TEXT NOT NULL, playlist_id TEXT NOT NULL,
zone_id TEXT NOT NULL REFERENCES layout_zones(id) ON DELETE CASCADE,
created_at INTEGER,
PRIMARY KEY (token_id, playlist_id, zone_id),
FOREIGN KEY (token_id, playlist_id) REFERENCES api_token_targets(token_id, playlist_id) ON DELETE CASCADE);
INSERT INTO api_tokens VALUES ('tok1');
INSERT INTO playlists VALUES ('plA','wsA'), ('plB','wsA');
INSERT INTO layouts VALUES ('L1'), ('L2');
INSERT INTO layout_zones VALUES ('zA1','L1'), ('zA2','L1'), ('zB1','L2');
INSERT INTO api_token_targets VALUES ('tok1','plA'), ('tok1','plB');
INSERT INTO api_token_target_zones VALUES ('tok1','plA','zA1', 0); -- plA narrowed to zA1; plB has none
`);
return db;
}
test('#73 foreign_keys is ON (the cascade/FK guarantees are real, not silent no-ops)', () => {
assert.equal(freshDb().pragma('foreign_keys', { simple: true }), 1);
});
test('#73 zone confinement: granted YES, non-granted/cross-playlist/ambiguous all blocked', () => {
const db = freshDb();
// granted zone within a designated playlist -> YES
assert.deepEqual(resolveGrantedZone(db, 'tok1', 'plA', 'zA1'), { ok: true, zoneId: 'zA1' });
// NON-granted zone within the SAME designated playlist -> blocked (the refinement bites)
assert.equal(resolveGrantedZone(db, 'tok1', 'plA', 'zA2').ok, false);
// a zone from a DIFFERENT playlist's layout -> blocked (no cross-playlist)
assert.equal(resolveGrantedZone(db, 'tok1', 'plA', 'zB1').ok, false);
// no requested zone, exactly one grant -> auto-place into it
assert.deepEqual(resolveGrantedZone(db, 'tok1', 'plA', null), { ok: true, zoneId: 'zA1' });
// playlist with NO zone grants -> whole-playlist (full-screen); a body zone is IGNORED
assert.deepEqual(resolveGrantedZone(db, 'tok1', 'plB', null), { ok: true, zoneId: null });
assert.deepEqual(resolveGrantedZone(db, 'tok1', 'plB', 'zB1'), { ok: true, zoneId: null });
// multiple grants, no pick -> must specify
db.prepare("INSERT INTO api_token_target_zones VALUES ('tok1','plA','zA2',0)").run();
assert.equal(resolveGrantedZone(db, 'tok1', 'plA', null).reason, 'ambiguous');
assert.deepEqual(resolveGrantedZone(db, 'tok1', 'plA', 'zA2'), { ok: true, zoneId: 'zA2' }); // picks among grants
});
test('#73 orphan-grant is IMPOSSIBLE: a zone grant cannot exist without its playlist grant', () => {
const db = freshDb();
// (tok1, plC) is NOT in api_token_targets -> the composite FK must reject the zone grant
assert.throws(
() => db.prepare("INSERT INTO api_token_target_zones VALUES ('tok1','plC','zA1',0)").run(),
/FOREIGN KEY/i,
'inserting a zone grant without its playlist grant must be rejected by the FK');
});
test('#73 cascade: revoking the playlist grant removes its zone grants (structural, not manual)', () => {
const db = freshDb();
db.prepare("DELETE FROM api_token_targets WHERE token_id='tok1' AND playlist_id='plA'").run();
assert.equal(db.prepare("SELECT COUNT(*) c FROM api_token_target_zones WHERE playlist_id='plA'").get().c, 0,
'zone grants cascade out when the parent playlist grant is deleted');
});
test('#73 cascade chain: deleting a playlist removes BOTH the playlist grant and the zone grants', () => {
const db = freshDb();
db.prepare("DELETE FROM playlists WHERE id='plA'").run();
assert.equal(db.prepare("SELECT COUNT(*) c FROM api_token_targets WHERE playlist_id='plA'").get().c, 0, 'playlist grant gone');
assert.equal(db.prepare("SELECT COUNT(*) c FROM api_token_target_zones WHERE playlist_id='plA'").get().c, 0, 'zone grants gone (no orphans)');
});
test('#73 cascade: deleting a zone (or its layout) drops the grant referencing it', () => {
const db = freshDb();
db.prepare("DELETE FROM layouts WHERE id='L1'").run(); // -> layout_zones zA1/zA2 cascade -> zone grants cascade
assert.equal(db.prepare("SELECT COUNT(*) c FROM api_token_target_zones WHERE zone_id='zA1'").get().c, 0);
});