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