const { test, expect, describe } = require('bun:test'); const crypto = require('crypto'); const { MEMBER_URN_PREFIX, PARTICIPATION_SECRET, participationSecretOf, memberUrn } = require('./member-urn'); const BOB = 'http://localhost:3000/bob'; const CAROL = 'http://localhost:3000/carol'; const EVENT = 'http://localhost:3000/alice/data/events/e1'; const OTHER_EVENT = 'http://localhost:3000/alice/data/events/e2'; const SECRET = '0123456789abcdef'.repeat(4); describe('memberUrn — urn:festipod:userref: ")>', () => { test('the contract: the lowercase-hex sha256 of the three values joined by single spaces', () => { const expected = crypto.createHash('sha256').update(`${BOB} ${EVENT} ${SECRET}`).digest('hex'); expect(memberUrn({ participantUri: BOB, eventUri: EVENT, secret: SECRET })).toBe(`urn:festipod:userref:${expected}`); expect(MEMBER_URN_PREFIX).toBe('urn:festipod:userref:'); expect(memberUrn({ participantUri: BOB, eventUri: EVENT, secret: SECRET })).toMatch(/^urn:festipod:userref:[0-9a-f]{64}$/); }); test('deterministic for one person and one event; different for another event, person or secret', () => { const base = { participantUri: BOB, eventUri: EVENT, secret: SECRET }; const urn = memberUrn(base); expect(memberUrn({ ...base })).toBe(urn); expect(memberUrn({ ...base, eventUri: OTHER_EVENT })).not.toBe(urn); expect(memberUrn({ ...base, participantUri: CAROL })).not.toBe(urn); expect(memberUrn({ ...base, secret: 'f'.repeat(64) })).not.toBe(urn); }); test('a missing value is a programming error, never a URN', () => { expect(() => memberUrn({ participantUri: BOB, eventUri: undefined, secret: SECRET })).toThrow('eventUri'); expect(() => memberUrn({ participantUri: BOB, eventUri: EVENT, secret: '' })).toThrow('secret'); }); }); describe('participationSecretOf — 64 lowercase hex characters under fp:participationSecret, or nothing', () => { test('read under the full IRI, the CURIE, or as a value node', () => { expect(PARTICIPATION_SECRET).toBe('http://festipod.org/participationSecret'); expect(participationSecretOf({ [PARTICIPATION_SECRET]: SECRET })).toBe(SECRET); expect(participationSecretOf({ 'fp:participationSecret': SECRET })).toBe(SECRET); expect(participationSecretOf({ [PARTICIPATION_SECRET]: { '@value': SECRET } })).toBe(SECRET); expect(participationSecretOf({ [PARTICIPATION_SECRET]: [SECRET] })).toBe(SECRET); }); test('absent, malformed or ambiguous: no secret', () => { for (const value of [undefined, '', SECRET.toUpperCase(), SECRET.slice(1), `${SECRET}0`, 'g'.repeat(64), 42, { id: SECRET }, [SECRET, 'f'.repeat(64)]]) { expect(participationSecretOf({ [PARTICIPATION_SECRET]: value })).toBeUndefined(); } expect(participationSecretOf(undefined)).toBeUndefined(); expect(participationSecretOf({ name: 'Bob' })).toBeUndefined(); }); });