screentinker/server/test/agency-zone-grants.test.js
ScreenTinker c5550f5bc9 feat: agency zone-grant issuance UI + reactive placement card (#73)
Issuance (on the proven seam):
- tokens.js create + PUT /:id/targets accept per-playlist zone grants (target_zones), inserted
  into api_token_target_zones inside the same transaction as the playlist grants (FK requires
  the parent, so order matters and is correct).
- Issuance validation (the mirror of runtime confinement): grantableZoneIds() - can grant ONLY
  a zone the playlist's layout actually feeds; can't grant one it doesn't have or one from
  another playlist's layout. Bite-tested. PUT re-designate stays atomic: delete parent rows ->
  zone grants cascade out (no manual child delete).
- settings.js: checking a designated playlist reveals its grantable zones (GET
  /api/playlists/:id/zones, JWT); leave unchecked = whole-playlist. i18n across all 5 locales.

Card:
- GET /api/agency/playlists/:playlistId/layout (rides router.param - confined; a non-
  designated playlist -> 403, asserted). "Your zone" = the GRANTED zones. Retired the
  token-wide /layouts (the per-playlist card replaces the disconnected lump).
- Portal card reacts to the playlist selector: pick a playlist -> its layout renders, the
  granted zone highlighted with px size, siblings as context.

Full suite + agency bite-suite green (154).

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

103 lines
5.8 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, grantableZoneIds } = 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');
CREATE TABLE playlist_items (id INTEGER PRIMARY KEY, playlist_id TEXT, zone_id TEXT);
INSERT INTO playlist_items VALUES (1,'plA','zA1'), (2,'plB','zB1'); -- plA feeds L1, plB feeds 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);
});
test('#73 ISSUANCE validation: can only grant a zone the playlist\'s layout feeds', () => {
const db = freshDb();
// plA feeds L1, so its layout's zones (zA1, zA2) are grantable - and nothing else
assert.deepEqual([...grantableZoneIds(db, 'plA')].sort(), ['zA1', 'zA2']);
// zB1 belongs to L2 (plB's layout) - NOT grantable for plA (no cross-playlist-layout grant)
assert.equal(grantableZoneIds(db, 'plA').has('zB1'), false);
assert.deepEqual([...grantableZoneIds(db, 'plB')], ['zB1']);
});