const { test, expect, describe } = require('bun:test'); const { itemsOf, readItems, expectItem } = require('./collection-items'); const C = 'http://localhost:3000/alice/data/as/collection/c1'; const ALICE = 'http://localhost:3000/alice'; const A = 'urn:festipod:userref:aa'; const B = 'urn:festipod:userref:bb'; /** A ctx whose `pod-resources.get` answers `reply` (a `{ status, body }`, or a function of the call). */ const pod = reply => { const calls = []; return { calls, call: async (action, params) => { calls.push({ action, params }); if (action !== 'pod-resources.get') throw new Error(`unexpected call ${action}`); return typeof reply === 'function' ? reply(params) : reply; } }; }; const collection = extra => ({ status: 200, body: { id: C, type: 'Collection', ...extra } }); describe('itemsOf — the item URIs a collection lists inline', () => { test('every spelling of items / orderedItems; strings and nodes; none', () => { expect(itemsOf({ items: [A, B] })).toEqual([A, B]); expect(itemsOf({ items: A })).toEqual([A]); expect(itemsOf({ items: [{ id: A }, { '@id': B }] })).toEqual([A, B]); expect(itemsOf({ orderedItems: [A] })).toEqual([A]); expect(itemsOf({ 'as:items': [A] })).toEqual([A]); expect(itemsOf({ 'https://www.w3.org/ns/activitystreams#items': [{ '@id': A }] })).toEqual([A]); expect(itemsOf({ totalItems: 0 })).toEqual([]); }); }); describe('readItems — a failed read throws, it never means "empty"', () => { test('a 200 with the collection: its items, read as the given actor', async () => { const ctx = pod(collection({ items: [A, B] })); expect(await readItems(ctx, { collectionUri: C, actorUri: ALICE })).toEqual([A, B]); expect(ctx.calls).toEqual([{ action: 'pod-resources.get', params: { resourceUri: C, actorUri: ALICE } }]); expect(await readItems(pod(collection({ totalItems: 0 })), { collectionUri: C, actorUri: ALICE })).toEqual([]); }); test('any other status, no body, another resource, a paged root: throws', async () => { for (const status of [0, 401, 403, 404, 500]) { await expect(readItems(pod({ status }), { collectionUri: C, actorUri: ALICE })).rejects.toThrow(`(HTTP ${status})`); } await expect(readItems(pod({ status: 200 }), { collectionUri: C, actorUri: ALICE })).rejects.toThrow('does not return the collection'); await expect(readItems(pod({ status: 200, body: { id: `${C}-other`, items: [A] } }), { collectionUri: C, actorUri: ALICE })).rejects.toThrow('another resource'); await expect(readItems(pod(collection({ first: `${C}?page=1`, totalItems: 3 })), { collectionUri: C, actorUri: ALICE })).rejects.toThrow('is paged'); }); }); describe('expectItem — a write is checked by reading the collection back', () => { test('present after an add, absent after a remove: resolves', async () => { await expect(expectItem(pod(collection({ items: [A] })), { collectionUri: C, itemUri: A, actorUri: ALICE, present: true })).resolves.toBeUndefined(); await expect(expectItem(pod(collection({ items: [B] })), { collectionUri: C, itemUri: A, actorUri: ALICE, present: false })).resolves.toBeUndefined(); }); test('a write that did not take effect, or a read-back that fails: throws', async () => { await expect(expectItem(pod(collection({ items: [B] })), { collectionUri: C, itemUri: A, actorUri: ALICE, present: true })).rejects.toThrow(`does not hold ${A} after the add`); await expect(expectItem(pod(collection({ items: [A] })), { collectionUri: C, itemUri: A, actorUri: ALICE, present: false })).rejects.toThrow(`still holds ${A} after the remove`); await expect(expectItem(pod({ status: 500 }), { collectionUri: C, itemUri: A, actorUri: ALICE, present: false })).rejects.toThrow('(HTTP 500)'); }); });