import { FetchRepoTransport, parseAtUri } from '@radial/atproto' import { COLLECTIONS, joinRkey } from '@radial/core' import { FIXTURE_DIDS, fixtureSpace } from '@radial/core/fixture' import { afterEach, describe, expect, it } from 'vitest' import { adopt, signOut } from './auth.svelte.js' import { FAKE_PDS, FakePds, latestVersions, memoryStorage } from './fake-pds.js' import { memoryDatabase } from './idb.js' import { Identities } from './identity.js' import { noteJoin, resetJoins } from './join.svelte.js' import { discoverOwned, forgetOwned, owned } from './owned.svelte.js' import { openSpace, session, stop } from './session.svelte.js' import { openSpaceStores } from './store.js' // Accepting an invitation, which in this protocol is opening the space. // // The grant an admin writes lands in the ADMIN's repo, so nothing in the invited member's repo knows the // space exists — and atproto indexes forward only, so no client can search for it. Opening the space // writes a `join` into their own repo, and that is what closes the loop: the round trip asserted at the // bottom is write-then-discover, because either half alone is useless. // // A join grants nothing. `materializer.test.mjs` is where that is proven; here the concern is the other // half of the same rule — this refuses to write one for anybody a grant does not already cover. const fixture = fixtureSpace() /** A member of the fixture space who has a grant and no join: nothing in their repo names the space. */ const NEWCOMER = FIXTURE_DIDS.reviewer interface Harness { network: FakePds transport: FetchRepoTransport } async function open(): Promise { const network = new FakePds(latestVersions(fixture.records), {}) const identities = new Identities({ fetcher: network.fetch, storage: memoryStorage() }) const database = memoryDatabase() await openSpace(fixture.spaceUri, { identities, transport: () => new FetchRepoTransport(identities.pdsResolver, network.fetch), openStores: (uri) => openSpaceStores(uri, async () => database), poll: false, }) return { network, transport: new FetchRepoTransport(identities.pdsResolver, network.fetch) } } function signInAs(did: string, network: FakePds): void { adopt(did, (path, init) => network.fetch(new URL(path, FAKE_PDS), init)) } const joinsIn = (network: FakePds, did: string) => (network.repos.get(did) ?? []).filter((record) => record.collection === COLLECTIONS.join) afterEach(async () => { await signOut() resetJoins() forgetOwned() stop() session.space = undefined session.status = 'choosing' session.error = '' }) describe('opening a space you were added to', () => { it('writes a join into your own repo, at the key derived from the space', async () => { const harness = await open() signInAs(NEWCOMER, harness.network) expect(joinsIn(harness.network, NEWCOMER)).toHaveLength(0) expect(await noteJoin()).toBe('wrote') const written = joinsIn(harness.network, NEWCOMER) expect(written).toHaveLength(1) expect(parseAtUri(written[0]!.uri).rkey).toBe(joinRkey(FIXTURE_DIDS.tim, parseAtUri(fixture.spaceUri).rkey)) expect(written[0]!.value).toMatchObject({ $type: COLLECTIONS.join, space: { uri: fixture.spaceUri }, }) // The write went to the joiner's own repo and nowhere else — nothing was asked of the admin's. expect( harness.network.calls.filter((call) => call.method === 'createRecord').map((call) => call.did), ).toEqual([NEWCOMER]) }) it('is the only thing that makes that space discoverable afterwards', async () => { const harness = await open() signInAs(NEWCOMER, harness.network) // Before: a grant in tim's repo, and nothing in theirs. A picker has nothing to list. expect(session.space?.index.members.some((member) => member.did === NEWCOMER && member.active)).toBe(true) await discoverOwned(NEWCOMER, { transport: harness.transport }) expect(owned.spaces).toEqual([]) await noteJoin() // After: one `listRecords` over their own repo finds it, and the name comes from the space record. await discoverOwned(NEWCOMER, { transport: harness.transport }) expect(owned.spaces).toHaveLength(1) expect(owned.spaces[0]).toMatchObject({ uri: fixture.spaceUri, name: 'Radial', kind: 'joined' }) }) it('folds into the index without changing anything about membership', async () => { const harness = await open() signInAs(NEWCOMER, harness.network) const before = session.space?.index.members await noteJoin() const index = session.space?.index expect(index?.joins.map((join) => join.did)).toEqual([FIXTURE_DIDS.ana, NEWCOMER]) expect(index?.members).toEqual(before) expect(index?.ignored).toEqual([]) }) it('does not write a second one, however many times it is called', async () => { const harness = await open() signInAs(NEWCOMER, harness.network) expect(await noteJoin()).toBe('wrote') // The fold now holds it, which is the cheap answer… expect(await noteJoin()).toBe('already') // …and it stays the answer even if the fold is somehow behind, because of the per-tab guard. resetJoins() expect(await noteJoin()).toBe('already') expect(joinsIn(harness.network, NEWCOMER)).toHaveLength(1) }) }) describe('who does not get a join written for them', () => { it('nobody, when nobody is signed in', async () => { const harness = await open() expect(await noteJoin()).toBe('no-session') expect(harness.network.counts('createRecord')).toBe(0) }) it('a reader with no grant, because the space would ignore the record', async () => { // Reading a space needs no membership, so this is a common case and not an error. Writing a join // here would put a record in `index.ignored` for every stranger who ever opened the space. const harness = await open() signInAs('did:plc:strangerstranger', harness.network) expect(await noteJoin()).toBe('not-a-member') expect(harness.network.counts('createRecord')).toBe(0) }) it('the root, whose space record is already in their own repo', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) expect(await noteJoin()).toBe('is-root') expect(harness.network.counts('createRecord')).toBe(0) // …and discovery finds it anyway, which is why the join would be a pointer from a repo to itself. await discoverOwned(FIXTURE_DIDS.tim, { transport: harness.transport }) expect(owned.spaces.map((entry) => entry.kind)).toEqual(['created']) }) it('a member already carrying one', async () => { const harness = await open() signInAs(FIXTURE_DIDS.ana, harness.network) expect(await noteJoin()).toBe('already') expect(harness.network.counts('createRecord')).toBe(0) }) it('anyone looking at the fixture, which has no repo behind it', async () => { const harness = await open() signInAs(NEWCOMER, harness.network) if (session.space) session.space = { ...session.space, fixture: true } expect(await noteJoin()).toBe('fixture') expect(harness.network.counts('createRecord')).toBe(0) }) }) describe('when the write fails', () => { it('says so to its caller and to nobody else', async () => { const harness = await open() signInAs(NEWCOMER, harness.network) harness.network.offline = true expect(await noteJoin()).toBe('failed') // A bookmark that did not get written is not worth interrupting anybody about: the space is open, // every view still draws, and the pane bar is not reporting a problem the human has no part in. expect(session.status).toBe('ready') expect(session.space).toBeDefined() // And it does not try again on the next tick — an automatic write that retries in a loop is worse // than one that quietly did not happen. harness.network.offline = false expect(await noteJoin()).toBe('attempted') expect(joinsIn(harness.network, NEWCOMER)).toHaveLength(0) }) })