~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
379 lines · 13.3 KB Raw History
// A render of every screen, to the extent one process without a browser can.
//
// The build already proves the module graph resolves. What this adds is that
// each screen renders what it is given: a component reading a field the wire
// shape does not have fails here rather than on the page.
//
// renderToString runs no effects, so a query never fetches. Each screen's
// answers are put into the cache under the key that screen reads, and a key
// that moves shows up as a screen rendering its skeleton.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderToString } from 'react-dom/server';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { App } from '#/app.jsx';
import { SessionProvider } from '#/lib/session.jsx';

const ALICE = 'did:plc:alice';
const BOB = 'did:plc:bob';
const CAROL = 'did:plc:carol';
const SPACE = `at://${ALICE}/space/example.bsky.forum/general`;
const RKEY = '3mtjzqmmgxsrb';
const COLLECTION = 'com.example.post';

/** @type {any} */
const session = {
  did: ALICE,
  handle: 'alice.example.com',
  pds: 'https://alice.example',
  scope: 'atproto space:*?authority=*&skey=*&collection=*&action=create',
};

const VALUE = {
  $type: COLLECTION,
  text: 'a post in a space',
  tags: ['one', 'two'],
  createdAt: '2026-08-20T00:00:00.000Z',
  image: {
    $type: 'blob',
    ref: { $link: 'bafkreiblob' },
    mimeType: 'image/png',
    size: 70,
  },
};

/** @param {QueryClient} client */
function prime(client) {
  client.setQueryData(['spaces', ALICE], {
    spaces: [
      { uri: SPACE, isOwner: true },
      { uri: `at://${BOB}/space/example.bsky.forum/theirs`, isOwner: false },
    ],
  });
  client.setQueryData(['space-config', SPACE], {
    value: {
      uri: SPACE,
      policy: { $type: 'com.atproto.memberspace.policy' },
    },
    source: {
      method: 'com.atproto.space.getSpace',
      host: 'https://alice.example',
      auth: 'session',
    },
  });
  client.setQueryData(['commit', SPACE, null], {
    rev: RKEY,
    data: { $bytes: 'AAECqv8' },
  });
  client.setQueryData(['index', SPACE, null], {
    records: [{ collection: COLLECTION, rkey: RKEY, cid: 'bafyrecord' }],
    complete: true,
  });
  client.setQueryData(['repos', SPACE], {
    repos: [
      { did: ALICE, rev: RKEY, data: { $bytes: 'AAEC' } },
      { did: BOB, rev: RKEY, data: { $bytes: 'AAED' } },
    ],
  });
  client.setQueryData(['members', SPACE], { members: [{ did: ALICE }] });
  client.setQueryData(['records', SPACE, null, COLLECTION], {
    records: [
      { collection: COLLECTION, rkey: RKEY, cid: 'bafyrecord', value: VALUE },
    ],
  });
  client.setQueryData(['record', SPACE, null, COLLECTION, RKEY], {
    uri: `${SPACE}/${ALICE}/${COLLECTION}/${RKEY}`,
    cid: 'bafyrecord',
    value: VALUE,
  });
  client.setQueryData(['ops', SPACE, null], {
    ops: [
      {
        rev: RKEY,
        collection: COLLECTION,
        rkey: RKEY,
        cid: 'bafy',
        prev: null,
      },
      { rev: RKEY, collection: COLLECTION, rkey: 'gone', cid: null, prev: 'x' },
    ],
  });
  client.setQueryData(['blobs', SPACE, null], { cids: ['bafkreiblob'] });
  client.setQueryData(['identity', ALICE], {
    did: ALICE,
    handle: 'alice.example.com',
    verified: true,
  });
  client.setQueryData(['identity', BOB], {
    did: BOB,
    handle: 'bob.example.com',
    verified: false,
  });
  client.setQueryData(['profile', BOB], {
    avatar:
      'https://bob.example/xrpc/com.atproto.sync.getBlob?did=did:plc:bob&cid=bafkrei',
    displayName: 'Bob',
  });
  client.setQueryData(['app-profile', 'example.bsky.forum'], {
    avatar:
      'https://app.example/xrpc/com.atproto.sync.getBlob?did=did:plc:app&cid=bafkapp',
    displayName: 'Forum',
  });
  client.setQueryData(['identity', CAROL], {
    did: CAROL,
    handle: 'carol.example.com',
    verified: true,
  });
}

/** @param {string} href */
function render(href) {
  const url = new URL(href, 'https://spaces.example.com');
  vi.stubGlobal('window', {
    location: {
      origin: url.origin,
      hostname: url.hostname,
      pathname: url.pathname,
      search: url.search,
    },
    addEventListener() {},
    removeEventListener() {},
    history: { pushState() {} },
    scrollTo() {},
  });
  const client = new QueryClient();
  prime(client);
  return renderToString(
    <QueryClientProvider client={client}>
      <SessionProvider>
        <App />
      </SessionProvider>
    </QueryClientProvider>,
  );
}

beforeEach(() => {
  vi.stubGlobal('localStorage', {
    getItem: () => JSON.stringify(session),
    setItem() {},
    removeItem() {},
  });
});

describe('the screens', () => {
  it('lists the spaces the account holds', () => {
    const html = render('/');
    expect(html).toContain('example.bsky.forum');
    expect(html).toContain('general');
    expect(html).toContain('you host this');
    expect(html).toContain('alice.example.com');
  });

  it('shows a space with its policy, its commit and its collections', () => {
    const html = render(`/space?uri=${encodeURIComponent(SPACE)}`);
    expect(html).toContain('Members on a list');
    expect(html).toContain('Any client');
    // The state hash reads as hex, which is how it compares by eye against
    // another implementation's output.
    expect(html).toContain('000102aaff');
    expect(html).toContain(COLLECTION);
    // The collection card links to that collection's records, carrying the
    // space in the query rather than in the path.
    expect(html).toContain(`/records?uri=${encodeURIComponent(SPACE)}`);
    // Members is offered because this account is the space's own authority.
    expect(html).toContain('>members<');
  });

  it('renders a record value, blob reference and all', () => {
    const html = render(
      `/records?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}`,
    );
    expect(html).toContain('a post in a space');
    expect(html).toContain('image/png');
    expect(html).toContain('70 B');
    expect(html).toContain(RKEY);
  });

  it('renders one record with its URI and CID', () => {
    const html = render(
      `/record?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}&rkey=${RKEY}`,
    );
    expect(html).toContain('a post in a space');
    expect(html).toContain('bafyrecord');
    // A record URI names the authority and the author, so both DIDs read as
    // handles while the copy stays canonical.
    expect(html).toContain('alice.example.com');
    expect(html).toContain(`title="${SPACE}/${ALICE}/${COLLECTION}/${RKEY}"`);
    // The scope granted a write, so the editor is offered.
    expect(html).toContain('Edit');
    expect(html).toContain('Delete');
  });

  it('names each op by what it did', () => {
    const html = render(`/ops?uri=${encodeURIComponent(SPACE)}`);
    expect(html).toContain('Write log');
    expect(html).toContain('create');
    expect(html).toContain('delete');
  });

  // A page of DIDs says nothing about who anybody is.
  // Clicking into another member's repo lands on their records with the list
  // that was clicked no longer on screen, so every screen under a space says
  // whose repo it is and offers the way back.
  it("names another account's repo and offers a way back to one's own", () => {
    const html = render(
      `/records?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}&repo=${BOB}`,
    );
    expect(html).toContain('bob.example.com');
    expect(html).toContain('Your repo');
    // The way back drops the repo, and carries no collection: a collection one
    // member holds is not one another does.
    expect(html).toContain(`/space?uri=${encodeURIComponent(SPACE)}"`);
  });

  // A copy control keeps its width whether or not it is showing, so in running
  // text it opens a hole between the name and the word after it.
  it('sets a handle in a sentence with nothing between it and the next word', () => {
    const html = render(
      `/records?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}&repo=${CAROL}`,
    );
    const name = html.indexOf('carol.example.com');
    const after = html.indexOf('&#x27;s repo', name);
    expect(after).toBeGreaterThan(name);
    expect(html.slice(name, after)).not.toContain('<svg');
  });

  it('says a screen is showing your own repo rather than saying nothing', () => {
    const html = render(
      `/records?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}`,
    );
    expect(html).toContain('Your repo in this space');
  });

  it('scopes every screen under a space, not just the records list', () => {
    for (const path of ['/space', '/ops', '/blobs']) {
      const html = render(
        `${path}?uri=${encodeURIComponent(SPACE)}&repo=${BOB}`,
      );
      expect(html).toContain('bob.example.com');
      expect(html).toContain('Your repo');
    }
  });

  it('names the authority of a space it does not host', () => {
    const html = render('/');
    expect(html).toContain('bob.example.com');
  });

  // An NSID reverses to a domain, and these domains are atproto handles, so
  // the app's icon is an account's picture served by that account's own PDS.
  it("shows the space app's picture, read from a PDS rather than a web server", () => {
    const html = render('/');
    expect(html).toContain('app.example/xrpc/com.atproto.sync.getBlob');
    expect(html).not.toContain('unavatar');
    expect(html).not.toContain('favicon');
  });

  // The picture comes from the account's own PDS, a host this app is already
  // reading from, rather than from an appview and its CDN.
  it("shows the authority's picture, served by their own PDS", () => {
    const html = render('/');
    expect(html).toContain('bob.example/xrpc/com.atproto.sync.getBlob');
    expect(html).not.toContain('cdn.bsky.app');
  });

  // Anybody may claim any handle in their own DID document, so one that does
  // not resolve back is marked rather than shown as an identity.
  it('marks a handle that is claimed but not confirmed', () => {
    const html = render('/');
    expect(html).toContain('unconfirmed handle');
  });

  it('falls back to the DID while the lookup is outstanding', () => {
    const html = render(
      `/records?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}&repo=did:plc:stranger`,
    );
    expect(html).toContain('did:plc:stranger');
  });

  // A space is served by two roles on two hosts, and which one answered is the
  // thing a reader most often wants and can least often tell.
  // A URI reads as a fact, so an unconfirmed handle in one would put somebody
  // else's name on an address.
  it('leaves an unconfirmed handle as its DID inside a URI', () => {
    const theirs = `at://${BOB}/space/example.bsky.forum/theirs`;
    const html = render('/');
    const shown = html.slice(html.indexOf(theirs.slice(0, 20)));
    expect(html).toContain(`title="${theirs}"`);
    // bob.example.com is claimed but does not resolve back, so the URI keeps
    // the DID even though the prose beside it names him.
    expect(shown.slice(0, 400)).toContain(BOB);
  });

  it('offers the provenance of the configuration it shows', () => {
    const html = render(`/space?uri=${encodeURIComponent(SPACE)}`);
    expect(html).toContain('Where these values came from');
  });

  it('renders the blob list', () => {
    const html = render(`/blobs?uri=${encodeURIComponent(SPACE)}`);
    expect(html).toContain('Blobs');
    expect(html).toContain('bafkreiblob');
  });

  it('offers no editor to a read-only session', () => {
    vi.stubGlobal('localStorage', {
      getItem: () =>
        JSON.stringify({ ...session, scope: 'atproto space:*?action=read' }),
      setItem() {},
      removeItem() {},
    });
    const html = render(
      `/record?uri=${encodeURIComponent(SPACE)}&collection=${COLLECTION}&rkey=${RKEY}`,
    );
    expect(html).toContain('read only');
    expect(html).not.toContain('>Delete<');
  });

  it('offers a way into a space the list does not name', () => {
    const html = render('/');
    // listSpaces reports only spaces this account has written to, so a URI box
    // is the only route to one it has been admitted to and not written in.
    expect(html).toContain('Open a space by URI');
  });

  it('offers the policy editor to the space authority', () => {
    const html = render(`/space?uri=${encodeURIComponent(SPACE)}`);
    expect(html).toContain('>Edit<');
    expect(html).toContain('Delete this space');
  });

  it('offers neither editor for a space this account does not host', () => {
    const theirs = `at://did:plc:someone/space/example.bsky.forum/general`;
    const html = render(`/space?uri=${encodeURIComponent(theirs)}`);
    expect(html).not.toContain('Delete this space');
    expect(html).not.toContain('>members<');
  });

  it('renders the credential inspector before anything is exchanged', () => {
    const html = render(`/credential?uri=${encodeURIComponent(SPACE)}`);
    // React escapes the apostrophe in "The credential's key".
    expect(html).toContain('credential&#x27;s key');
    expect(html).toContain('Run the exchange');
  });

  it('offers sign-in when no session is held', () => {
    vi.stubGlobal('localStorage', {
      getItem: () => null,
      setItem() {},
      removeItem() {},
    });
    const html = render('/');
    expect(html).toContain('Handle or DID');
    expect(html).toContain('Read only');
    expect(html).toContain('Read and write');
    // A deployment serves whoever visits it, so the field names nobody. The
    // host's own handle in there would be wrong for everybody else.
    expect(html).toContain('value=""');
  });
});