const { test, expect, describe } = require('bun:test'); const participations = require('./participations.service'); const { hasOwnPublicRead, ownReaders } = require('./public-read-acl'); const { world, profile, profileOf, contactsOf, groupOf, createdCollectionAcl, APP, ALICE, BOB, MALLORY, FP_PARTICIPATIONS } = require('./participation-world'); const ensure = (w, extra = {}) => w.participations.actions.ensure({ actorUri: BOB, profileUri: profileOf(BOB), contactsUri: contactsOf(BOB), ...extra }); const collectionOf = w => w.participations.actions.collectionOf({ actorUri: BOB, profileUri: profileOf(BOB) }); const EXISTING = `${BOB}/data/as/collection/participations-existing`; const linked = uri => ({ [profileOf(BOB)]: profile(BOB, { [FP_PARTICIPATIONS]: { id: uri } }) }); const aclId = (uri, rule) => `http://localhost:3000/_acl${new URL(uri).pathname}#${rule}`; const appControl = uri => ({ '@id': aclId(uri, 'Control'), 'acl:mode': 'acl:Control', 'acl:agent': APP }); /** The /contacts WAC group that may read Bob's participations collection (what the grant names). */ const GROUP = groupOf(BOB); /** A collection readable by Bob's /contacts group, the app keeping its Control. */ const groupRead = uri => ({ '@id': aclId(uri, 'Read'), 'acl:mode': 'acl:Read', 'acl:agentGroup': GROUP }); const protectedAcl = uri => [groupRead(uri), appControl(uri)]; /** The collection created at the first sign-up (the world names the first one `participations-1`). */ const CREATED = `${BOB}/data/as/collection/participations-1`; /** Who the collection's own #Read rule lets read, plus every inherited rule's readers. */ const readersOf = (w, uri) => ownReaders(w.state.acls[uri]); const writes = w => w.calls.filter(c => ['pod-collections.createAndAttach', 'pod-permissions.add', 'signature.proxy.query'].includes(c.action)); describe('participations — the collection is attached at the first sign-up, never on a profile Create', () => { test('no activity handler, the full-IRI attach predicate, unordered, no mirrored group', () => { expect(participations.activities).toEqual({}); expect(participations.settings.attachPredicate).toBe('http://festipod.org/participations'); expect(participations.settings.collectionOptions.ordered).toBe(false); expect(participations.settings.createWacGroup).toBe(false); }); }); describe('participations.collectionOf — the collection the profile links now', () => { test('none, one, one outside the pod, several, an unreadable profile', async () => { expect(await collectionOf(world())).toBeUndefined(); expect(await collectionOf(world({ stored: linked(EXISTING) }))).toBe(EXISTING); expect(await collectionOf(world({ stored: { [profileOf(BOB)]: profile(BOB, { [FP_PARTICIPATIONS]: EXISTING }) } }))).toBe(EXISTING); // a plain IRI value await expect(collectionOf(world({ stored: linked(`${MALLORY}/data/as/collection/x`) }))).rejects.toThrow('outside the pod'); await expect(collectionOf(world({ stored: { [profileOf(BOB)]: profile(BOB, { [FP_PARTICIPATIONS]: [{ id: EXISTING }, { id: `${EXISTING}-2` }] }) } }))).rejects.toThrow('several'); await expect(collectionOf(world({ stored: { [profileOf(BOB)]: undefined } }))).rejects.toThrow('does not return the profile'); }); test('a profile outside the participant\'s pod is never read', async () => { const w = world(); await expect(w.participations.actions.collectionOf({ actorUri: BOB, profileUri: profileOf(MALLORY) })).rejects.toThrow('not in the pod'); expect(w.calls.some(c => c.action === 'pod-resources.get')).toBe(false); }); }); describe('participations.ensure — a collection readable by the participant\'s /contacts group', () => { test('no collection: one is created and attached, the /contacts group granted read, the pod\'s public read removed', async () => { const w = world(); const uri = await ensure(w); expect(uri).toBe(CREATED); expect(w.state.created).toEqual([uri]); expect(w.state.stored[profileOf(BOB)][FP_PARTICIPATIONS]).toEqual({ id: uri }); const attach = w.calls.find(c => c.action === 'pod-collections.createAndAttach'); expect(attach.params).toMatchObject({ resourceUri: profileOf(BOB), attachPredicate: FP_PARTICIPATIONS, actorUri: BOB }); const grants = w.calls.filter(c => c.action === 'pod-permissions.add').map(c => c.params); expect(grants).toEqual([{ uri, agentPredicate: 'acl:agentGroup', agentUri: GROUP, mode: 'acl:Read', actorUri: BOB }]); expect(hasOwnPublicRead(w.state.acls[uri])).toBe(false); expect(readersOf(w, uri)).toEqual({ agent: [], agentClass: [], agentGroup: [GROUP] }); // The group URI is DERIVED from her apods:contacts collection; the list itself is never read. expect(w.calls.find(c => c.action === 'pod-wac-groups.getUriFromCollectionUri').params).toEqual({ collectionUri: contactsOf(BOB) }); expect(w.calls.some(c => c.action === 'pod-collections.getItems')).toBe(false); }); test('the public read goes FIRST (one PUT, the app keeping its Control), then the group grant', async () => { const w = world(); const uri = await ensure(w); const order = writes(w).map(c => c.action); expect(order).toEqual(['pod-collections.createAndAttach', 'signature.proxy.query', 'pod-permissions.add']); const put = w.calls.find(c => c.action === 'signature.proxy.query'); expect(put.params.url).toBe(`http://localhost:3000/_acl${new URL(uri).pathname}`); expect(put.params.actorUri).toBe(APP); const graph = JSON.parse(put.params.body)['@graph']; expect(JSON.stringify(graph)).not.toContain('foaf:Agent'); expect(graph.some(n => n['@id'].endsWith('#Control') && n['acl:agent'] === APP)).toBe(true); }); test('no contact yet: the group is granted all the same — its membership is the pod\'s business', async () => { const w = world(); const uri = await ensure(w); expect(readersOf(w, uri)).toEqual({ agent: [], agentClass: [], agentGroup: [GROUP] }); // No per-contact acl:agent grant is ever made; the group carries everyone. expect(w.calls.every(c => c.action !== 'pod-permissions.add' || c.params.agentPredicate === 'acl:agentGroup')).toBe(true); }); test('idempotent: on a collection already readable by the group, the ACL reads, nothing written', async () => { const w = world({ stored: linked(EXISTING), acls: { [EXISTING]: protectedAcl(EXISTING) } }); expect(await ensure(w)).toBe(EXISTING); expect(await ensure(w)).toBe(EXISTING); expect(writes(w)).toEqual([]); expect(w.calls.filter(c => c.action === 'pod-permissions.fetch')).toHaveLength(2); }); test('an existing collection still world-readable: reused, the public read removed, the group granted', async () => { const w = world({ stored: linked(EXISTING), acls: { [EXISTING]: createdCollectionAcl(EXISTING) } }); expect(await ensure(w)).toBe(EXISTING); expect(w.state.created).toEqual([]); expect(hasOwnPublicRead(w.state.acls[EXISTING])).toBe(false); expect(readersOf(w, EXISTING)).toEqual({ agent: [], agentClass: [], agentGroup: [GROUP] }); }); test('an existing collection granting the group but still world-readable: the public read goes, the grant is not repeated', async () => { const bothOnRead = { '@id': aclId(EXISTING, 'Read'), 'acl:mode': 'acl:Read', 'acl:agentGroup': GROUP, 'acl:agentClass': 'foaf:Agent' }; const w = world({ stored: linked(EXISTING), acls: { [EXISTING]: [bothOnRead, appControl(EXISTING)] } }); await ensure(w); expect(hasOwnPublicRead(w.state.acls[EXISTING])).toBe(false); expect(readersOf(w, EXISTING)).toEqual({ agent: [], agentClass: [], agentGroup: [GROUP] }); expect(writes(w).map(c => c.action)).toEqual(['signature.proxy.query']); // the PUT only, no repeated grant }); test('one creation per participant: two concurrent calls attach ONE collection', async () => { const w = world({ delays: { create: 30 } }); const [a, b] = await Promise.all([ensure(w), ensure(w)]); expect(a).toBe(b); expect(w.state.created).toHaveLength(1); }); test('nothing attached (the profile gained a link meanwhile): the linked collection is used', async () => { const w = world({ stored: linked(EXISTING), acls: { [EXISTING]: protectedAcl(EXISTING) } }); // collectionOf saw no link at first: simulate by an attach that finds one. const original = w.participations.collectionOf; let first = true; w.participations.collectionOf = async (...args) => (first ? ((first = false), undefined) : original(...args)); expect(await ensure(w)).toBe(EXISTING); expect(w.state.created).toEqual([]); }); test('a collection the pod creates but does not link (the profile PATCH refused): ensure throws, the collection is never used', async () => { const w = world({ fail: { attach: true } }); await expect(ensure(w)).rejects.toThrow(`the pod created ${CREATED} but did not link it`); expect(w.state.created).toEqual([CREATED]); expect(w.state.stored[profileOf(BOB)][FP_PARTICIPATIONS]).toBeUndefined(); // Not protected, not granted: nothing is done with a collection no contact could find. expect(writes(w).map(c => c.action)).toEqual(['pod-collections.createAndAttach']); // The profile is read again after the attach: the link counts, not what the attach returned. expect(w.calls.filter(c => c.action === 'pod-resources.get' && c.params.resourceUri === profileOf(BOB))).toHaveLength(2); }); test('every failure throws — nothing may then be added to the collection', async () => { await expect(ensure(world({ fail: { putStatus: 403 } }))).rejects.toThrow('refused to remove the public read'); await expect(ensure(world({ stored: linked(EXISTING), acls: { [EXISTING]: [appControl(EXISTING)] }, fail: { grant: true } }))).rejects.toThrow('refused the read grant of the contacts group'); await expect(ensure(world({ fail: { aclRead: true } }))).rejects.toThrow('could not be read'); await expect(ensure(world(), { contactsUri: undefined })).rejects.toThrow('names no contacts collection'); await expect(ensure(world(), { contactsUri: `${MALLORY}/contacts` })).rejects.toThrow('names no contacts collection'); const inherited = world({ stored: linked(EXISTING), acls: { [EXISTING]: [...protectedAcl(EXISTING), { '@id': 'http://localhost:3000/_acl/bob/data/as/collection#DefaultRead', 'acl:mode': 'acl:Read', 'acl:agentClass': 'foaf:Agent' }] } }); await expect(ensure(inherited)).rejects.toThrow('inherited'); expect(writes(inherited)).toEqual([]); }); });