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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
165 lines · 5.6 KB Raw History
// The dev.pdsjs.git client over a fake fetch: describe asked once, params
// encoded, a refusal named, a procedure posted as JSON.
import { describe, expect, it } from 'vitest';
import { GitClient } from '../src/client.js';
import { XrpcError } from '../src/xrpc.js';

const SERVICE = 'https://pds.example.com';

/**
 * A fetch that records every request and answers by path.
 * @param {(url: URL, init: RequestInit|undefined) => Response} answer
 */
function fakeFetch(answer) {
  /** @type {Array<{url: URL, init: RequestInit|undefined}>} */
  const calls = [];
  /** @type {typeof fetch} */
  const carrier = async (input, init) => {
    const url = new URL(String(input));
    calls.push({ url, init });
    return answer(url, init);
  };
  return { calls, carrier };
}

const describing = (/** @type {string[]} */ methods) =>
  Response.json({
    roles: ['pds'],
    methods: methods.map((value) => ({
      $type: 'community.lexicon.service.describe#nsid',
      value,
    })),
  });

describe('GitClient', () => {
  it('asks describe once and reads what the server serves', async () => {
    const { calls, carrier } = fakeFetch(() =>
      describing(['dev.pdsjs.git.listRepos', 'dev.pdsjs.git.getTree']),
    );
    const client = new GitClient({ service: `${SERVICE}/`, fetch: carrier });
    expect(await client.serves('getTree')).toBe(true);
    expect(await client.serves('dev.pdsjs.git.listRepos')).toBe(true);
    expect(await client.serves('getFile')).toBe(false);
    expect(await client.servesReads()).toBe(false);
    expect(calls).toHaveLength(1);
    expect(calls[0].url.href).toBe(
      `${SERVICE}/xrpc/community.lexicon.service.describe`,
    );
  });

  it('reads a server that cannot describe itself as serving nothing', async () => {
    const { carrier } = fakeFetch(() => new Response('nope', { status: 404 }));
    const client = new GitClient({ service: SERVICE, fetch: carrier });
    expect(await client.serves('listRepos')).toBe(false);
    const failing = new GitClient({
      service: SERVICE,
      fetch: async () => {
        throw new Error('offline');
      },
    });
    expect(await failing.servesReads()).toBe(false);
  });

  it('serves every read once the server lists them all', async () => {
    const { carrier } = fakeFetch(() =>
      describing([
        'dev.pdsjs.git.getTree',
        'dev.pdsjs.git.listFiles',
        'dev.pdsjs.git.getFile',
        'dev.pdsjs.git.getRaw',
        'dev.pdsjs.git.listCommits',
        'dev.pdsjs.git.getCommit',
        'dev.pdsjs.git.getLastCommit',
      ]),
    );
    const client = new GitClient({ service: SERVICE, fetch: carrier });
    expect(await client.servesReads()).toBe(true);
  });

  it('encodes the parameters a read names and leaves the empty ones out', async () => {
    const { calls, carrier } = fakeFetch(() =>
      Response.json({
        ref: 'refs/heads/main',
        sha: 'a',
        path: '',
        entries: [],
      }),
    );
    const client = new GitClient({
      service: SERVICE,
      fetch: carrier,
      headers: { Authorization: 'Bearer t' },
    });
    const tree = await client.getTree({
      repo: 'proj',
      ref: undefined,
      path: '',
      cid: 'bafyrepo',
    });
    expect(tree.entries).toEqual([]);
    expect(calls[0].url.pathname).toBe('/xrpc/dev.pdsjs.git.getTree');
    expect([...calls[0].url.searchParams]).toEqual([
      ['repo', 'proj'],
      ['cid', 'bafyrepo'],
    ]);
    const sent = /** @type {Record<string, string>} */ (
      calls[0].init?.headers ?? {}
    );
    expect(sent.Authorization).toBe('Bearer t');

    await client.listCommits({ repo: 'proj', ref: 'main', limit: 20 });
    expect(calls[1].url.searchParams.get('limit')).toBe('20');
  });

  it('throws the name a refusal carries', async () => {
    const { carrier } = fakeFetch(() =>
      Response.json(
        { error: 'RepoNotFound', message: 'No repository named x.' },
        { status: 404 },
      ),
    );
    const client = new GitClient({ service: SERVICE, fetch: carrier });
    const failure = await client.getRepo({ repo: 'x' }).catch((err) => err);
    expect(failure).toBeInstanceOf(XrpcError);
    expect(failure.status).toBe(404);
    expect(failure.error).toBe('RepoNotFound');
    expect(failure.message).toBe('No repository named x.');
  });

  it('names an unserved method from a bare 501', async () => {
    const { carrier } = fakeFetch(() => new Response(null, { status: 501 }));
    const client = new GitClient({ service: SERVICE, fetch: carrier });
    const failure = await client.getTree({ repo: 'x' }).catch((err) => err);
    expect(failure.error).toBe('MethodNotImplemented');
  });

  it('answers raw bytes as the response itself', async () => {
    const { carrier } = fakeFetch(
      () => new Response('hi', { headers: { 'Content-Type': 'text/plain' } }),
    );
    const client = new GitClient({ service: SERVICE, fetch: carrier });
    const res = await client.getRaw({ repo: 'proj', path: 'a.txt' });
    expect(await res.text()).toBe('hi');
  });

  it('posts a procedure as JSON', async () => {
    const { calls, carrier } = fakeFetch(() =>
      Response.json({ name: 'proj', space: null }),
    );
    const client = new GitClient({ service: SERVICE, fetch: carrier });
    expect(
      await client.createRepo({ name: 'proj', description: 'A project.' }),
    ).toEqual({
      name: 'proj',
      space: null,
    });
    expect(calls[0].init?.method).toBe('POST');
    expect(calls[0].init?.body).toBe(
      '{"name":"proj","description":"A project."}',
    );
    const sent = /** @type {Record<string, string>} */ (
      calls[0].init?.headers ?? {}
    );
    expect(sent['Content-Type']).toBe('application/json');
  });
});