// The /account/api/spaces endpoints, driven through pds.fetch with a cookie // session against fake storage, with the space routes composed over a fake // browser and admin port. import { describe, expect, it } from 'vitest'; import { apiGet, apiPost, createPds as createBasePds, DID, sessionCookie, } from '../../../test/helpers/account-api-harness.js'; import { createSpaceAccountRoutes } from '../src/account.js'; /** * @param {Object} [options] * @param {Object} [options.spaceBrowser] * @param {Object} [options.spaceAdmin] */ function createPds({ spaceBrowser, spaceAdmin } = {}) { return createBasePds({ extensions: [ (host) => ({ name: 'spaces', routes: createSpaceAccountRoutes({ host, spaceBrowser: /** @type {any} */ (spaceBrowser ?? null), spaceAdmin: /** @type {any} */ (spaceAdmin ?? null), }), features: { spaceAdmin: Boolean(spaceAdmin) }, }), ], }); } describe('/account/api/spaces', () => { it('reports the feature off without a browser', async () => { const pds = createPds(); const body = await ( await apiGet(pds, '/account/api/spaces', await sessionCookie()) ).json(); expect(body).toEqual({ enabled: false, spaces: [] }); }); it('lists spaces with their member and record counts', async () => { const pds = createPds({ spaceBrowser: { async listSpaces() { return [ { uri: 'at://space.example/1', spaceType: 'chat.example.space', spaceDid: 'did:web:space.example', isOwner: true, policy: { $type: 'com.atproto.memberspace.policy', publicWrite: true, }, createdAt: '2026-01-01T00:00:00.000Z', deletedAt: null, }, ]; }, async countMembers() { return 3; }, async countRecords() { return 12; }, }, }); const body = await ( await apiGet(pds, '/account/api/spaces', await sessionCookie()) ).json(); expect(body.enabled).toBe(true); expect(body.spaces).toEqual([ { uri: 'at://space.example/1', spaceType: 'chat.example.space', authority: 'did:web:space.example', isOwner: true, policy: { $type: 'com.atproto.memberspace.policy', publicWrite: true, }, createdAt: '2026-01-01T00:00:00.000Z', deleted: false, members: 3, collections: 12, }, ]); }); }); describe('space admin endpoints', () => { const SPACE = `at://${DID}/space/com.example.team/main`; /** A spaceAdmin recording its calls. */ function recordingAdmin() { /** @type {string[]} */ const calls = []; return { calls, async createSpace(/** @type {{type: string, skey?: string}} */ opts) { calls.push(`create ${opts.type} ${opts.skey ?? ''}`); return { uri: SPACE }; }, async putMember( /** @type {string} */ space, /** @type {string} */ did, /** @type {{read: boolean, write: boolean}} */ access, ) { calls.push(`put ${space} ${did} ${access.read} ${access.write}`); }, async removeMember( /** @type {string} */ space, /** @type {string} */ did, ) { calls.push(`remove ${space} ${did}`); }, }; } it('answers with an error when spaces are not enabled', async () => { const pds = createPds(); const cookie = await sessionCookie(); for (const [path, body] of [ ['/account/api/spaces/create', { type: 'com.example.team' }], ['/account/api/spaces/members/add', { space: SPACE, actor: DID }], ['/account/api/spaces/members/remove', { space: SPACE, did: DID }], ]) { const response = await apiPost(pds, String(path), body, cookie); expect(response.status).toBe(400); expect((await response.json()).error).toMatch(/not enabled/); } }); it('creates a space and requires a type', async () => { const spaceAdmin = recordingAdmin(); const pds = createPds({ spaceAdmin }); const cookie = await sessionCookie(); const created = await apiPost( pds, '/account/api/spaces/create', { type: 'com.example.team', skey: 'main' }, cookie, ); expect(created.status).toBe(200); expect((await created.json()).uri).toBe(SPACE); expect(spaceAdmin.calls).toContain('create com.example.team main'); const missing = await apiPost( pds, '/account/api/spaces/create', {}, cookie, ); expect(missing.status).toBe(400); expect((await missing.json()).error).toMatch(/type is required/); }); it('lists members through the browser', async () => { const pds = createPds({ spaceBrowser: { async listMembers() { return [DID, 'did:plc:friend']; }, }, }); const response = await apiGet( pds, `/account/api/spaces/members?space=${encodeURIComponent(SPACE)}`, await sessionCookie(), ); expect(await response.json()).toEqual({ enabled: true, members: [DID, 'did:plc:friend'], }); }); it('adds a member by DID without resolving anything', async () => { const spaceAdmin = recordingAdmin(); const pds = createPds({ spaceAdmin }); const response = await apiPost( pds, '/account/api/spaces/members/add', { space: SPACE, actor: 'did:plc:friend' }, await sessionCookie(), ); expect(response.status).toBe(200); expect((await response.json()).did).toBe('did:plc:friend'); expect(spaceAdmin.calls).toContain(`put ${SPACE} did:plc:friend true true`); }); it('resolves a handle through the well-known document', async () => { const spaceAdmin = recordingAdmin(); const pds = createPds({ spaceAdmin }); const realFetch = globalThis.fetch; globalThis.fetch = /** @type {any} */ ( async (/** @type {any} */ input) => { const url = String(input); if (url === 'https://friend.example/.well-known/atproto-did') { return new Response('did:plc:friend\n'); } return realFetch(input); } ); try { const response = await apiPost( pds, '/account/api/spaces/members/add', { space: SPACE, actor: 'friend.example' }, await sessionCookie(), ); expect(response.status).toBe(200); expect((await response.json()).did).toBe('did:plc:friend'); } finally { globalThis.fetch = realFetch; } }); it('falls back to DNS and fails closed when both miss', async () => { const spaceAdmin = recordingAdmin(); const pds = createPds({ spaceAdmin }); const realFetch = globalThis.fetch; globalThis.fetch = /** @type {any} */ ( async (/** @type {any} */ input) => { const url = String(input); if (url.includes('.well-known')) { return new Response('nope', { status: 404 }); } if (url.startsWith('https://cloudflare-dns.com/')) { if (url.includes('_atproto.friend.example')) { return Response.json({ Answer: [{ data: '"did=did:plc:dnsfriend"' }], }); } return Response.json({ Answer: [] }); } return realFetch(input); } ); try { const resolved = await apiPost( pds, '/account/api/spaces/members/add', { space: SPACE, actor: 'friend.example' }, await sessionCookie(), ); expect((await resolved.json()).did).toBe('did:plc:dnsfriend'); const unresolved = await apiPost( pds, '/account/api/spaces/members/add', { space: SPACE, actor: 'nobody.example' }, await sessionCookie(), ); expect(unresolved.status).toBe(400); expect((await unresolved.json()).error).toMatch(/could not resolve/); } finally { globalThis.fetch = realFetch; } }); it('refuses an actor that is not a plausible handle', async () => { const pds = createPds({ spaceAdmin: recordingAdmin() }); const response = await apiPost( pds, '/account/api/spaces/members/add', { space: SPACE, actor: 'not a handle' }, await sessionCookie(), ); expect(response.status).toBe(400); expect((await response.json()).error).toMatch(/not a handle/); }); it('removes a member and requires both fields', async () => { const spaceAdmin = recordingAdmin(); const pds = createPds({ spaceAdmin }); const cookie = await sessionCookie(); const removed = await apiPost( pds, '/account/api/spaces/members/remove', { space: SPACE, did: 'did:plc:friend' }, cookie, ); expect(removed.status).toBe(200); expect(spaceAdmin.calls).toContain(`remove ${SPACE} did:plc:friend`); const missing = await apiPost( pds, '/account/api/spaces/members/remove', { space: SPACE }, cookie, ); expect(missing.status).toBe(400); }); });