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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
162 lines · 5.1 KB Raw History
// The reader a page gets answers every read the in-page reader does, and
// sends each to the server's method where the server serves the reads.
import { beforeEach, describe, expect, it, vi } from 'vitest';

const READS = [
  'listTree',
  'listFiles',
  'readFile',
  'readImage',
  'listCommits',
  'readCommit',
  'lastCommit',
];

/** The methods a server that serves every read advertises. */
const READS_SERVED = [
  'getTree',
  'listFiles',
  'getFile',
  'getRaw',
  'listCommits',
  'getCommit',
  'getLastCommit',
];

async function load() {
  vi.resetModules();
  const git = await import('../src/lib/git.js');
  git.account.did = 'did:plc:owner';
  git.account.pds = 'https://pds.example.com';
  git.account.handle = 'owner.example.com';
  return git;
}

/** @type {string[]} */
let asked = [];

beforeEach(() => {
  asked = [];
  vi.unstubAllGlobals();
  // The git service decision probes this page's own origin first; a probe
  // this stub refuses leaves the account's PDS as the service.
  vi.stubGlobal('location', new URL('https://site.example/'));
  vi.stubGlobal('fetch', async (/** @type {string|URL|Request} */ input) => {
    const url = String(
      typeof input === 'object' && 'url' in input ? input.url : input,
    );
    asked.push(url);
    if (url.includes('dev.pdsjs.git.listRepos')) {
      return new Response('no', { status: 404 });
    }
    if (url.includes('community.lexicon.service.describe')) {
      return Response.json({
        methods: READS_SERVED.map((name) => ({
          value: `dev.pdsjs.git.${name}`,
        })),
      });
    }
    if (url.includes('dev.pdsjs.git.getRaw')) {
      return new Response('png', { headers: { 'Content-Type': 'image/png' } });
    }
    return Response.json({
      ref: 'refs/heads/main',
      sha: 'a'.repeat(40),
      path: '',
      entries: [],
      files: [],
      commits: [],
      commit: null,
      parents: [],
      truncated: false,
    });
  });
});

describe('readerFor', () => {
  it('answers every read the pages make', async () => {
    const { readerFor } = await load();
    const reader = readerFor('repo');
    for (const method of READS) {
      expect(typeof reader[method], method).toBe('function');
    }
  });

  it('sends each read to its method', async () => {
    const { readerFor } = await load();
    const reader = readerFor('repo');
    await reader.listTree('did:plc:owner', 'repo', 'main', 'src');
    await reader.listFiles('did:plc:owner', 'repo', 'main');
    await reader.readFile('did:plc:owner', 'repo', 'main', 'a.txt');
    const image = await reader.readImage(
      'did:plc:owner',
      'repo',
      'main',
      'a.png',
    );
    await reader.listCommits('did:plc:owner', 'repo', 'main', 20);
    await reader.readCommit('did:plc:owner', 'repo', 'a'.repeat(40));
    await reader.lastCommit('did:plc:owner', 'repo', 'main', 'a.txt');
    const methods = asked
      .filter((url) => url.includes('/xrpc/dev.pdsjs.git.'))
      // The service decision probes listRepos before the first read.
      .filter((url) => !url.includes('listRepos'))
      .map((url) => new URL(url).pathname.split('.').pop());
    expect(methods).toEqual([
      'getTree',
      'listFiles',
      'getFile',
      'getRaw',
      'listCommits',
      'getCommit',
      'getLastCommit',
    ]);
    expect(image.mediaType).toBe('image/png');
    expect(new TextDecoder().decode(image.bytes)).toBe('png');
    expect(asked.find((url) => url.includes('getTree'))).toContain(
      'repo=repo&ref=main&path=src',
    );
  });

  it('does not take a single-page fallback for the git service', async () => {
    // A host serving the app answers every path with the app's own HTML and
    // a 200, listRepos included. The probe has to read the answer's type,
    // or every later read is sent to a page that cannot answer it.
    const { readerFor } = await load();
    vi.stubGlobal('fetch', async (/** @type {string|URL|Request} */ input) => {
      const url = String(
        typeof input === 'object' && 'url' in input ? input.url : input,
      );
      asked.push(url);
      if (url.startsWith('https://site.example/')) {
        return new Response('<!doctype html><title>app</title>', {
          headers: { 'Content-Type': 'text/html' },
        });
      }
      if (url.includes('dev.pdsjs.git.listRepos')) {
        return Response.json({ repos: [] });
      }
      if (url.includes('community.lexicon.service.describe')) {
        return Response.json({
          methods: READS_SERVED.map((name) => ({
            value: `dev.pdsjs.git.${name}`,
          })),
        });
      }
      return Response.json({ path: '', entries: [] });
    });
    await readerFor('repo').listTree('did:plc:owner', 'repo', 'main', '');
    const tree = asked.find((url) => url.includes('getTree'));
    expect(tree).toContain('https://pds.example.com/');
  });

  it('says where a read went, for the wait a page shows', async () => {
    const { readerFor, readSource, onRepoProgress } = await load();
    expect(readSource()).toBeNull();
    let told = 0;
    onRepoProgress(() => told++);
    await readerFor('repo').listTree('did:plc:owner', 'repo', 'main', '');
    expect(readSource()).toBe('server');
    expect(told).toBe(1);
  });
});