Run the pod provider from images with make start, and fix what a fresh stack exposed
- docker-compose-dev.yml: the redis host port is overridable (REDIS_PORT in the root .env, 6379 by default as in the sibling apps), and the pod provider, in host network mode, follows it for its queue and OIDC redis. All of ./data/fuseki is mounted at the image's SEMAPPS_FUSEKI_BASE, so account deletion removes the backups and, 24 h later, the secure dataset's files (it answered 500 without the mount). - backend: GET /app served the app actor snapshot the mixin takes before it creates or replaces the access-need group, so a dataset's first boot lacked the link until a restart; the app service refreshes the snapshot from the store at the end of started(). - e2e smoke: the post-install poll sends Accept: application/ld+json; the pod answers 500 to a container read in any other format. - Docs: cookbook (Redis port, mounts, hot reload, dataset-named accounts, fresh-boot note), deployment note, env examples; claims about a parallel race are no longer stated as fact.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user