const { test, expect, describe } = require('bun:test'); const { grantsPublicRead, hasOwnPublicRead, hasInheritedPublicRead, revocationGraph, isPublicRead, FOAF_AGENT } = require('./public-read-acl'); const ACL = 'http://www.w3.org/ns/auth/acl#'; const ALICE = 'http://localhost:3000/alice'; const E = `${ALICE}/data/events/e1`; const ACL_URI = `http://localhost:3000/_acl/alice/data/events/e1`; const defaultRead = { '@id': '#DefaultRead', 'acl:mode': 'acl:Read', 'acl:agent': ALICE }; const publicRead = (agentClass = 'foaf:Agent') => ({ '@id': '#Read', 'acl:mode': 'acl:Read', 'acl:agentClass': agentClass }); // Flatten the PUT graph's agent triples for assertions. const triples = doc => { const out = []; for (const n of doc['@graph']) for (const p of ['acl:agent', 'acl:agentClass', 'acl:agentGroup', 'acl:origin']) for (const o of [].concat(n[p] ?? [])) out.push({ auth: n['@id'], p, o }); return out; }; describe('grantsPublicRead / isPublicRead', () => { test('detects the foaf:Agent read rule in both spellings', () => { expect(grantsPublicRead([defaultRead, publicRead('foaf:Agent')])).toBe(true); expect(grantsPublicRead([defaultRead, publicRead(FOAF_AGENT)])).toBe(true); expect(grantsPublicRead([defaultRead, publicRead({ '@id': FOAF_AGENT })])).toBe(true); }); test('a non-public ACL is not public-read', () => { expect(grantsPublicRead([defaultRead])).toBe(false); expect(isPublicRead({ '@id': '#Read', 'acl:mode': 'acl:Read', 'acl:agent': ALICE })).toBe(false); }); }); describe('revocationGraph — a PUT body that drops foaf:Agent, keeps the rest, never empty', () => { test('typical event (only own rule is #Read foaf:Agent): keeps owner, drops public, omits #Default', () => { const doc = revocationGraph([defaultRead, publicRead('foaf:Agent')], E, ALICE, ACL_URI); const ts = triples(doc); expect(ts.some(t => t.o === 'foaf:Agent' || t.o === FOAF_AGENT)).toBe(false); expect(ts.some(t => t.auth.includes('#Default'))).toBe(false); // PUT must not touch inherited defaults expect(ts.some(t => t.p === 'acl:agent' && t.o === ALICE && t.auth.includes('#Read'))).toBe(true); expect(doc['@context']['@base']).toBe(ACL_URI); }); test('full foaf IRI is dropped too', () => { const ts = triples(revocationGraph([publicRead(FOAF_AGENT)], E, ALICE, ACL_URI)); expect(ts.some(t => t.o === FOAF_AGENT || t.o === 'foaf:Agent')).toBe(false); expect(ts.some(t => t.p === 'acl:agent' && t.o === ALICE)).toBe(true); // owner fallback, not empty }); test('a co-existing per-contact grant on #Read is preserved', () => { const carol = 'http://localhost:3000/carol'; const node = { '@id': '#Read', 'acl:mode': 'acl:Read', 'acl:agentClass': 'foaf:Agent', 'acl:agent': carol }; const ts = triples(revocationGraph([node], E, ALICE, ACL_URI)); expect(ts.some(t => t.p === 'acl:agent' && t.o === carol)).toBe(true); expect(ts.some(t => t.o === 'foaf:Agent' || t.o === FOAF_AGENT)).toBe(false); // The owner fallback is NOT added when another agent already keeps the set non-empty. expect(ts.some(t => t.p === 'acl:agent' && t.o === ALICE)).toBe(false); }); test('a separate own #Control rule is retained (only the public Read triple goes)', () => { const control = { '@id': '#Control', 'acl:mode': 'acl:Control', 'acl:agent': ALICE }; const ts = triples(revocationGraph([control, publicRead('foaf:Agent')], E, ALICE, ACL_URI)); expect(ts.some(t => t.auth.includes('#Control') && t.o === ALICE)).toBe(true); expect(ts.some(t => t.o === 'foaf:Agent')).toBe(false); }); }); describe('revocationGraph is NARROW — only the public READ goes', () => { test('a foaf:Agent on the own #Append rule (a world-appendable collection) is kept; only #Read loses it', () => { const append = { '@id': '#Append', 'acl:mode': 'acl:Append', 'acl:agentClass': 'foaf:Agent' }; const ts = triples(revocationGraph([publicRead('foaf:Agent'), append], E, ALICE, ACL_URI)); expect(ts.some(t => t.auth === '#Append' && t.p === 'acl:agentClass' && t.o === 'foaf:Agent')).toBe(true); expect(ts.some(t => t.auth === '#Read' && (t.o === 'foaf:Agent' || t.o === FOAF_AGENT))).toBe(false); }); test('a foaf:Agent on the own #Write rule is kept too, under either spelling', () => { const write = { '@id': '#Write', 'acl:mode': 'acl:Write', 'acl:agentClass': FOAF_AGENT }; const ts = triples(revocationGraph([publicRead(FOAF_AGENT), write], E, ALICE, ACL_URI)); expect(ts.some(t => t.auth === '#Write' && t.o === FOAF_AGENT)).toBe(true); expect(ts.some(t => t.auth === '#Read' && (t.o === 'foaf:Agent' || t.o === FOAF_AGENT))).toBe(false); }); }); describe('own versus inherited public read', () => { const inheritedPublic = { '@id': '#DefaultRead', 'acl:mode': 'acl:Read', 'acl:agentClass': 'foaf:Agent' }; test('the own #Read rule is what a revocation can remove', () => { expect(hasOwnPublicRead([defaultRead, publicRead()])).toBe(true); expect(hasInheritedPublicRead([defaultRead, publicRead()])).toBe(false); }); test('a public read inherited from the container is told apart (a PUT cannot remove it)', () => { expect(hasOwnPublicRead([inheritedPublic])).toBe(false); expect(hasInheritedPublicRead([inheritedPublic])).toBe(true); expect(grantsPublicRead([inheritedPublic])).toBe(true); }); test('a foaf:Agent on another own rule is not a public read', () => { expect(hasOwnPublicRead([{ '@id': '#Append', 'acl:mode': 'acl:Append', 'acl:agentClass': 'foaf:Agent' }])).toBe(false); }); });