Adapt the failing direct sign-up tests to the declarer notification decision

This commit is contained in:
Sylvain Duchesne
2026-11-06 23:09:48 +01:00
parent 9cbe78948b
commit af0cba6920
7 changed files with 178 additions and 3 deletions
+95 -3
View File
@@ -866,13 +866,105 @@ describe('registration — a direct sign-up: a Join naming an event itself', ()
expect(w.logs.error).toEqual([]);
});
test("a direct sign-up notifies nobody: the declarer of an event is not its host", async () => {
const readsOf = (w, uri) => w.calls.filter(c => ['pod-resources.get', 'pod-collections.getItems'].includes(c.action) && (c.params.resourceUri ?? c.params.collectionUri) === uri);
const expectAnonymous = (w, n) => {
expect(n.recipientUri).toBe(ALICE);
expect(JSON.stringify(n.template)).not.toContain('participantName');
expect(n).not.toHaveProperty('participantName');
expect(n).not.toHaveProperty('activity');
expect(JSON.stringify(n)).not.toContain(BOB);
expect(readsOf(w, contactsOf(BOB))).toEqual([]);
};
test("a direct sign-up notifies the event's declarer, anonymously", async () => {
const j = join(PLAIN);
const w = plainWorld([j]);
await emitJoin(w, j);
expect(attendees(w, PLAIN)).toEqual([plainUrn()]);
expect(w.calls.some(c => c.action === 'pod-notifications.send')).toBe(false);
expect(w.state.notifications).toEqual([]);
expect(w.state.notifications).toHaveLength(1);
const [n] = w.state.notifications;
expectAnonymous(w, n);
expect(n.template.title.fr).toContain('Un participant a rejoint');
expect(n.template.title.en).toContain('A participant joined');
expect(n.eventUri).toBe(PLAIN);
expect(Object.values(n)).toContain('Evénement plain');
expect(JSON.stringify(n.template)).not.toContain('Evénement plain');
expect(n.template.title.fr).toContain('{{{');
expect(n.template.actions).toHaveLength(1);
expect(n.template.actions[0].link).toContain('/events/{{encodeUri eventUri}}');
expect(n.template.actions[0].link).not.toContain('meeting-points');
});
test('a Leave naming the event notifies the declarer, anonymously', async () => {
const j = join(PLAIN);
const l = leave(PLAIN);
const w = plainWorld([j, l]);
await emitJoin(w, j);
await emitLeave(w, l);
expect(w.state.notifications).toHaveLength(2);
const n = w.state.notifications[1];
expectAnonymous(w, n);
expect(n.template.title.fr).toContain('Un participant a quitté');
expect(n.template.title.en).toContain('A participant left');
expect(n.eventUri).toBe(PLAIN);
});
test('a repeated direct Join notifies once; a Leave of someone never signed up notifies no one', async () => {
const first = join(PLAIN, T0, BOB, 1);
const again = join(PLAIN, T1, BOB, 2);
const stray = leave(PLAIN, T1, CAROL, 1);
const w = plainWorld([first, again, stray]);
await emitJoin(w, first);
await emitJoin(w, again);
expect(w.state.notifications).toHaveLength(1);
await emitLeave(w, stray);
expect(w.state.notifications).toHaveLength(1);
});
test('a failed declarer notification does not fail the direct sign-up', async () => {
const j = join(PLAIN);
const w = plainWorld([j], { fail: { notify: true } });
await emitJoin(w, j);
expect(ledgerOf(w, PLAIN).change).toBe('join');
expect(attendees(w, PLAIN)).toEqual([plainUrn()]);
expect(participations(w)).toEqual([PLAIN]);
expect(w.logs.error.some(m => m.includes('Could not notify'))).toBe(true);
});
test('leaving the event and its meeting point notifies the declarer and the host, each once', async () => {
const DAVE_EVENT = `${DAVE}/data/events/festival`;
const acts = [join(DAVE_EVENT), join(MP1), leave(DAVE_EVENT, T1), leave(MP1, T1)];
const w = setup({
acts,
stored: {
[DAVE_EVENT]: plainEvent(DAVE_EVENT, { 'dc:creator': DAVE }),
[MP1]: meetingPoint(MP1, { context: DAVE_EVENT })
}
});
await emitJoin(w, acts[0]);
await emitJoin(w, acts[1]);
w.state.notifications.length = 0;
await emitLeave(w, acts[2]);
await emitLeave(w, acts[3]);
expect(w.state.notifications).toHaveLength(2);
expect(w.state.notifications.map(n => n.recipientUri).sort()).toEqual([ALICE, DAVE].sort());
for (const n of w.state.notifications) {
expect(n.template.title.fr).toContain('Un participant a quitté');
expect(n).not.toHaveProperty('participantName');
expect(JSON.stringify(n)).not.toContain(BOB);
}
});
test('the same person carries a different URN on another event (R7e)', async () => {
const OTHER = `${ALICE}/data/events/other`;
const a = join(PLAIN, T0, BOB, 1);
const b = join(OTHER, T0, BOB, 2);
const w = setup({ acts: [a, b], stored: { [PLAIN]: plainEvent(PLAIN), [OTHER]: plainEvent(OTHER) } });
await emitJoin(w, a);
await emitJoin(w, b);
expect(attendees(w, PLAIN)).toEqual([plainUrn(BOB, PLAIN)]);
expect(attendees(w, OTHER)).toEqual([plainUrn(BOB, OTHER)]);
expect(attendees(w, PLAIN)[0]).not.toBe(attendees(w, OTHER)[0]);
});
test("the participant carries the SAME URN in the event's own list and in its meeting points' lists (R5, R6)", async () => {
+42
View File
@@ -1337,6 +1337,47 @@ async function directSignup(): Promise<void> {
}
/**
* A direct sign-up to an event whose declarer is NOT the participant's contact
* (FP-001, R6/R7): Alice signs up to Carol's event CE, Carol being no contact of
* hers. The declarer is told, anonymously, and gains no right to read the act, the
* list or the participations. Runs once CE exists (forged.setup-carol-meeting-point).
*/
async function directSignupToDeclared(): Promise<void> {
const G = "direct sign-up to CE (Alice; Carol declared CE and is not Alice's contact)";
const alice = (): ApodsSession => need(S.alice, 'Alice');
const carol = (): ApodsSession => need(S.carol, 'Carol');
const CE = (): string => need(S.CE, "Carol's event CE");
const noticed = (change: 'join' | 'leave'): Promise<CheckOutcome> =>
notifiedLine(CE(), change, false, { creator: carol(), participant: alice(), name: USERS.alice });
await m.hard('direct.notify-anonymous', G, 'Alice posts a Join naming CE: Carol, its declarer, is notified in her own inbox, naming neither her name nor her WebID', 'a join notification about CE naming neither the display name nor the WebID, within 30 s', async () => {
await postOutboxActivity(alice(), 'Join', CE(), appActorUri(config));
return noticed('join');
});
await m.hard('direct.activity-declarer-refused', G, "Carol — CE's declarer, not her contact — GETs Alice's Join naming CE (its URI handed out of band)", 'refused 403', async () => {
const act = need(await poll(() => ownActivity(alice(), 'Join', CE()), v => !!v, 15000), "Alice's Join naming CE");
const r = await http('GET', need(recordUri(act), "the Join's URI"), { token: carol().token });
return { ok: refused(r.status, AUTHN), actual: brief(r).slice(0, 80) };
});
await m.hard('direct.list-opaque', G, "CE's own attendee list (anonymous read) holds Alice's member URN, computed from her secret with CE as anchor, and no WebID", 'her URN ∈ items, her WebID ∉ items', async () => {
const list = need(await attachedCollection(CE(), carol()), "CE's attendees collection");
const urn = await ownMemberUrn(alice(), CE());
const r = await refIn(list, urn, true);
return { ok: r.ok && !r.items.includes(alice().webId), actual: `HTTP ${r.status} items=${JSON.stringify(r.items)}` };
});
await m.hard('direct.declarer-refused-participations', G, "Carol, the declarer and no contact of Alice, is refused Alice's participations collection although it lists CE (URI handed out of band)", 'refused (403, or the pod\'s 404 while Alice reads it 200)', async () => {
const uri = need(await ownParticipations(alice()), 'her participations collection');
const listed = await refIn(uri, CE(), true, alice().token);
need(listed.ok, 'the direct sign-up landed (direct.notify-anonymous)');
const refusal = await collectionRefusal(uri, carol().token, alice(), AUTHN);
return { ok: refusal.ok, actual: refusal.actual };
});
await m.hard('direct.leave-notify-anonymous', G, 'Alice posts a Leave naming CE (the group leaves the event as it found it): Carol is notified of the withdrawal, anonymously', 'a leave notification about CE naming neither the display name nor the WebID, within 30 s', async () => {
await postOutboxActivity(alice(), 'Leave', CE(), appActorUri(config));
return noticed('leave');
});
}
/**
* Who reads a person's participations is her /contacts WAC group, through ONE
* stable `acl:agentGroup` grant the backend never revisits: the platform keeps the
@@ -1893,6 +1934,7 @@ async function cleanupAccounts(): Promise<void> {
await directSignup();
await visibility();
await forgedContact();
await directSignupToDeclared();
await contactChanges();
} finally {
await uiHygiene();
@@ -63,6 +63,15 @@ Fonctionnalité: US-7 M'inscrire/me désinscrire à un événement
Alors aucune fenêtre de confirmation n'est ouverte
Et je vois le bouton "Se désinscrire de l'événement"
Scénario: Un contact inscrit directement apparaît parmi les participants de l'événement
Étant donné que je suis sur le détail de l'événement "Résidence Reconnexion"
Alors la liste des participants nomme "Jean Durand"
Scénario: Un participant inscrit directement qui n'est pas un contact n'est que compté
Étant donné que je suis sur le détail de l'événement "Formation CNV"
Alors la liste des participants compte des participants inconnus
Et la liste des participants ne nomme pas "user-hors-reseau"
Plan du Scénario: Le bouton Inviter suit l'inscription à l'événement, directe ou par un point de rencontre
Étant donné que je suis sur le détail de l'événement "<événement>"
Alors le bouton "Inviter" est <visibilité>
@@ -93,3 +93,15 @@ Then('je suis toujours inscrit aux points de rencontre {string} et {string}', as
const joined = buttons(this).filter(b => (b.textContent ?? '').trim() === '✓ Inscrit');
expect(joined.length, 'Both meeting points should still show as joined').to.equal(2);
});
Then('la liste des participants nomme {string}', async function (this: FestipodWorld, name: string) {
expect(this.getDomText(), `The participants should name "${name}"`).to.include(name);
});
Then('la liste des participants ne nomme pas {string}', async function (this: FestipodWorld, name: string) {
expect(this.getDomText(), `The participants should not name "${name}"`).to.not.include(name);
});
Then('la liste des participants compte des participants inconnus', async function (this: FestipodWorld) {
expect(this.getDomText(), 'Someone who is not a contact should be counted, not shown').to.include('Participant inconnu');
});
@@ -17,6 +17,10 @@ Fonctionnalité: US-10 Visualiser la fiche/le profil d'un participant
Étant donné que je suis sur la page "profil utilisateur"
Alors je peux voir les événements auxquels l'utilisateur a participé
Scénario: Le profil d'un contact liste les événements auxquels il est inscrit directement
Étant donné que je suis sur le profil de "Jean Durand"
Alors le profil liste l'événement "Résidence Reconnexion"
Scénario: Voir la localisation des événements
Étant donné que je suis sur la page "profil utilisateur"
Alors les événements affichent leur localisation et distance
+14
View File
@@ -1,6 +1,8 @@
import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from 'chai';
import type { FestipodWorld } from '../../../../shared/support/world';
import { renderScreen } from '../../../../shared/test-harness/renderHelper';
import { seedUsers } from '../../../../shared/data/seedData';
When('je clique sur un participant', async function (this: FestipodWorld) {
await this.navigateTo('#/demo/user-profile');
@@ -96,3 +98,15 @@ function expectProfileDom(
expect((avatar?.textContent ?? '').trim(), `Avatar should show initials "${expected.initials}"`)
.to.equal(expected.initials);
}
Given('je suis sur le profil de {string}', async function (this: FestipodWorld, name: string) {
const user = seedUsers.find(u => u.name === name);
expect(user, `No seed user named "${name}"`).to.not.be.undefined;
this.currentScreenId = 'user-profile';
this.renderedDoc = await renderScreen('user-profile', `/users/${user!.id}`);
await new Promise<void>(resolve => setTimeout(resolve, 50));
});
Then('le profil liste l\'événement {string}', async function (this: FestipodWorld, title: string) {
expect(this.getDomText(), `The profile should list the event "${title}"`).to.include(title);
});
+2
View File
@@ -199,6 +199,8 @@ export const seedParticipations: FpParticipationData[] = [
// Marie is a member of both meeting points of event 5, and not signed up to it directly
{ id: 'part-6', eventId: 'mp-3', userId: 'user-1', isConfirmed: true },
{ id: 'part-7', eventId: 'mp-4', userId: 'user-1', isConfirmed: true },
// Someone Marie cannot read signed up directly to event 4: counted, never shown
{ id: 'part-8', eventId: 'event-4', userId: 'user-hors-reseau', isConfirmed: true },
];
export const seedMeetingPoints: FpMeetingPointData[] = [