const { test, expect, describe, afterEach, setSystemTime } = require('bun:test'); const app = require('./app.service'); const ACTOR = 'http://localhost:3005/as/actor/app'; const GROUP = 'http://localhost:3005/interop/access-need-group/g1'; const actor = (extra = {}) => ({ id: ACTOR, type: ['Application', 'interop:Application'], ...extra }); const linked = () => actor({ 'interop:hasAccessNeedGroup': GROUP }); /** * The app service as moleculer runs its own started(), after the mixin's: `snapshot` is the * `this.appActor` the mixin left, `store` the actor the triplestore holds, `groups` what the * group listing answers. Attaching a group writes the link into the store, as the mixin does. * `failReadsFrom` makes the n-th actor read (1-based) and every later one throw. */ function service({ snapshot, store, groups = [], failReadsFrom = Infinity }) { const calls = []; const logs = { info: [], warn: [], error: [] }; let reads = 0; const svc = { appActor: snapshot, logger: { info: m => logs.info.push(m), warn: m => logs.warn.push(m), error: m => logs.error.push(m) }, broker: { call: async (action, params) => { calls.push(action); if (action === 'ldp.resource.get') { reads += 1; if (reads >= failReadsFrom) throw new Error('triplestore down'); return store; } if (action === 'access-needs-groups.list') return { 'ldp:contains': groups }; if (action === 'actors.attachAccessNeedGroup') { store = { ...store, 'interop:hasAccessNeedGroup': params.accessNeedGroupUri }; return undefined; } throw new Error(`unexpected call ${action}`); } } }; return { svc, calls, logs }; } describe('app actor snapshot — GET /app serves the access-need group once the store links it', () => { // The deadline test moves the clock; reset it even when a test times out mid-way. afterEach(() => setSystemTime()); test("a dataset's first boot: the snapshot predates the link the store holds, so it is refreshed", async () => { const { svc, calls } = service({ snapshot: actor(), store: linked() }); await app.started.call(svc); expect(svc.appActor['interop:hasAccessNeedGroup']).toBe(GROUP); expect(calls).not.toContain('actors.attachAccessNeedGroup'); }); test('the cold-start race: the store lacks the link too, the heal attaches the group, then the snapshot follows', async () => { const { svc, calls, logs } = service({ snapshot: actor(), store: actor(), groups: [{ id: GROUP }] }); await app.started.call(svc); expect(calls).toContain('actors.attachAccessNeedGroup'); expect(logs.info.some(m => m.includes(GROUP))).toBe(true); expect(svc.appActor['interop:hasAccessNeedGroup']).toBe(GROUP); }); test('a later boot: the snapshot already carries the link and keeps it', async () => { const { svc } = service({ snapshot: linked(), store: linked() }); await app.started.call(svc); expect(svc.appActor['interop:hasAccessNeedGroup']).toBe(GROUP); }); test('a boot that changes the access needs: the snapshot names the deleted group, the store the new one', async () => { const { svc } = service({ snapshot: actor({ 'interop:hasAccessNeedGroup': `${GROUP}-old` }), store: linked() }); await app.started.call(svc); expect(svc.appActor['interop:hasAccessNeedGroup']).toBe(GROUP); }); test('no group ever appears: the heal gives up and the snapshot is not replaced by an unlinked actor', async () => { const snapshot = actor(); const { svc, logs } = service({ snapshot, store: actor({ name: 'from the store' }) }); // The group listing answers empty; jump the clock past the heal's 60 s deadline at that // point so the loop gives up without sleeping. const call = svc.broker.call; svc.broker.call = async (action, params) => { if (action === 'access-needs-groups.list') setSystemTime(new Date(Date.now() + 61000)); return call(action, params); }; await app.started.call(svc); expect(logs.error.some(m => m.includes('No access-need group appeared'))).toBe(true); expect(svc.appActor).toBe(snapshot); }); test('a failed refresh read leaves the snapshot as it was and says so', async () => { const snapshot = actor(); const { svc, logs } = service({ snapshot, store: linked(), failReadsFrom: 2 }); await app.started.call(svc); expect(svc.appActor).toBe(snapshot); expect(logs.warn.some(m => m.includes('refresh the app actor snapshot'))).toBe(true); }); });