chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// 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);
});
});