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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
201 lines · 6.1 KB Raw History
import { describe, expect, it } from 'vitest';
import {
  CONSTELLATION_URL,
  createConstellationDiscovery,
  noDiscovery,
} from '../src/discovery.js';

const SUBJECT = 'at://did:plc:alice/dev.pdsjs.git.repo/demo-app';

/**
 * A fake index answering from a script of pages, recording each request.
 * @param {Array<{records?: unknown[], cursor?: unknown}|Response>} pages
 */
function fakeIndex(pages) {
  /** @type {Array<{url: URL, init: RequestInit|undefined}>} */
  const asked = [];
  /** @type {typeof fetch} */
  const send = async (input, init) => {
    asked.push({ url: new URL(String(input)), init });
    const page = pages.shift();
    if (page instanceof Response) return page;
    return Response.json(page ?? {});
  };
  return { asked, send };
}

describe('createConstellationDiscovery', () => {
  it('asks for the collection on the subject.uri path, naming itself', async () => {
    const index = fakeIndex([
      {
        records: [
          { did: 'did:plc:bob', collection: 'dev.pdsjs.git.pull', rkey: '3a' },
        ],
        cursor: null,
      },
    ]);
    const discovery = createConstellationDiscovery({
      fetch: index.send,
      userAgent: 'test (@example.com)',
    });

    const found = await discovery.about(SUBJECT, 'dev.pdsjs.git.pull');

    expect(found).toEqual([
      { did: 'did:plc:bob', collection: 'dev.pdsjs.git.pull', rkey: '3a' },
    ]);
    const [{ url, init }] = index.asked;
    expect(url.origin).toBe(CONSTELLATION_URL);
    expect(url.pathname).toBe('/xrpc/blue.microcosm.links.getBacklinks');
    expect(url.searchParams.get('subject')).toBe(SUBJECT);
    expect(url.searchParams.get('source')).toBe(
      'dev.pdsjs.git.pull:subject.uri',
    );
    expect(url.searchParams.get('limit')).toBe('100');
    expect(new Headers(init?.headers).get('user-agent')).toBe(
      'test (@example.com)',
    );
  });

  it('asks on another path where the link is elsewhere in the record', async () => {
    const index = fakeIndex([{ records: [] }]);
    await createConstellationDiscovery({ fetch: index.send }).about(
      SUBJECT,
      'dev.pdsjs.git.repo',
      'upstream',
    );
    expect(index.asked[0].url.searchParams.get('source')).toBe(
      'dev.pdsjs.git.repo:upstream',
    );
  });

  it('follows the cursor until the index says there is no more', async () => {
    const index = fakeIndex([
      {
        records: [{ did: 'did:plc:a', collection: 'c', rkey: '1' }],
        cursor: 'next',
      },
      {
        records: [{ did: 'did:plc:b', collection: 'c', rkey: '2' }],
        cursor: null,
      },
    ]);
    const discovery = createConstellationDiscovery({ fetch: index.send });

    const found = await discovery.about(SUBJECT, 'c');

    expect(found.map((one) => one.rkey)).toEqual(['1', '2']);
    expect(index.asked).toHaveLength(2);
    expect(index.asked[0].url.searchParams.has('cursor')).toBe(false);
    expect(index.asked[1].url.searchParams.get('cursor')).toBe('next');
  });

  it('stops at the page limit rather than following a cursor forever', async () => {
    const endless = Array.from({ length: 5 }, (_, at) => ({
      records: [{ did: 'did:plc:a', collection: 'c', rkey: String(at) }],
      cursor: 'more',
    }));
    const index = fakeIndex(endless);
    const discovery = createConstellationDiscovery({
      fetch: index.send,
      maxPages: 3,
    });

    const found = await discovery.about(SUBJECT, 'c');

    expect(found).toHaveLength(3);
    expect(index.asked).toHaveLength(3);
  });

  it('drops an entry missing any part of its address', async () => {
    const index = fakeIndex([
      {
        records: [
          { did: 'did:plc:a', collection: 'c', rkey: '1' },
          { did: 'did:plc:a', collection: 'c' },
          { collection: 'c', rkey: '2' },
          null,
          'nonsense',
        ],
      },
    ]);
    const discovery = createConstellationDiscovery({ fetch: index.send });

    expect(await discovery.about(SUBJECT, 'c')).toEqual([
      { did: 'did:plc:a', collection: 'c', rkey: '1' },
    ]);
  });

  it('answers what it has when the index refuses or fails', async () => {
    // A refusal on the second page keeps the first.
    const refused = fakeIndex([
      {
        records: [{ did: 'did:plc:a', collection: 'c', rkey: '1' }],
        cursor: 'next',
      },
      new Response('no', { status: 503 }),
    ]);
    expect(
      await createConstellationDiscovery({ fetch: refused.send }).about(
        SUBJECT,
        'c',
      ),
    ).toEqual([{ did: 'did:plc:a', collection: 'c', rkey: '1' }]);

    // A network failure on the first page is an empty answer, not an error.
    const down = createConstellationDiscovery({
      fetch: async () => {
        throw new TypeError('fetch failed');
      },
    });
    expect(await down.about(SUBJECT, 'c')).toEqual([]);

    // A body that is not JSON reads the same way.
    const garbled = createConstellationDiscovery({
      fetch: async () => new Response('<html>'),
    });
    expect(await garbled.about(SUBJECT, 'c')).toEqual([]);
  });

  it('asks the service it was given, without a trailing slash doubled', async () => {
    const index = fakeIndex([{ records: [] }]);
    const discovery = createConstellationDiscovery({
      service: 'https://index.example/',
      fetch: index.send,
    });

    await discovery.about(SUBJECT, 'c');

    expect(
      index.asked[0].url.href.startsWith('https://index.example/xrpc/'),
    ).toBe(true);
  });
});

describe('createConstellationDiscovery, on the global fetch', () => {
  it('carries the request on globalThis.fetch when given no carrier', async () => {
    const real = globalThis.fetch;
    /** @type {string[]} */
    const asked = [];
    globalThis.fetch = async (input) => {
      asked.push(String(input));
      return Response.json({ records: [], cursor: null });
    };
    try {
      expect(await createConstellationDiscovery().about(SUBJECT, 'c')).toEqual(
        [],
      );
      expect(asked[0]?.startsWith(`${CONSTELLATION_URL}/xrpc/`)).toBe(true);
    } finally {
      globalThis.fetch = real;
    }
  });
});

describe('noDiscovery', () => {
  it('finds nothing', async () => {
    expect(await noDiscovery().about(SUBJECT, 'dev.pdsjs.git.pull')).toEqual(
      [],
    );
  });
});