chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// community.lexicon.service.describe is the only way a client learns whether an
// optional feature is served here. atproto publishes no method list, so without
// this a caller has to invoke a space method and read the failure, where an
// absent feature and a broken one look the same.
import { describe, expect, it } from 'vitest';
import { PersonalDataServer } from '../packages/core/src/pds.js';
const PATH = '/xrpc/community.lexicon.service.describe';
/** @param {import('../packages/core/src/pds.js').Routes} [spaceRoutes] */
function createPds(spaceRoutes) {
// Nothing here reaches storage, so the ports can be bare stubs.
return new PersonalDataServer({
actorStorage: /** @type {any} */ ({}),
sharedStorage: /** @type {any} */ ({}),
blobs: /** @type {any} */ ({}),
jwtSecret: 'test-secret',
extensions: spaceRoutes
? [() => ({ name: 'spaces', routes: spaceRoutes })]
: undefined,
});
}
/** @param {PersonalDataServer} pds */
async function describeBody(pds) {
const response = await pds.fetch(
new Request(`https://pds.example.com${PATH}`),
);
expect(response.status).toBe(200);
return response.json();
}
describe('community.lexicon.service.describe', () => {
it('answers unauthenticated, naming the pds role', async () => {
const body = await describeBody(createPds());
expect(body.roles).toEqual(['pds']);
});
it('names each method with its union member', async () => {
const body = await describeBody(createPds());
for (const entry of body.methods) {
expect(entry.$type).toBe('community.lexicon.service.describe#nsid');
expect(entry.value).toMatch(/^[a-z][a-z0-9.]*\.[a-zA-Z]+$/);
}
});
it('describes exactly the routed methods, sorted', async () => {
const pds = createPds();
const body = await describeBody(pds);
const routed = Object.keys(pds.routes)
.filter((p) => p.startsWith('/xrpc/'))
.map((p) => p.slice('/xrpc/'.length))
.filter((n) => n.includes('.'))
.sort();
expect(body.methods.map((/** @type {any} */ m) => m.value)).toEqual(routed);
});
it('describes itself', async () => {
const body = await describeBody(createPds());
expect(body.methods.map((/** @type {any} */ m) => m.value)).toContain(
'community.lexicon.service.describe',
);
});
it('omits _health, which is an endpoint rather than a method', async () => {
const body = await describeBody(createPds());
const values = body.methods.map((/** @type {any} */ m) => m.value);
expect(values).not.toContain('_health');
expect(values.some((/** @type {string} */ v) => v.startsWith('_'))).toBe(
false,
);
});
it('omits the proxied app.bsky methods it does not route', async () => {
const body = await describeBody(createPds());
const values = body.methods.map((/** @type {any} */ m) => m.value);
// Served locally, so declared.
expect(values).toContain('app.bsky.actor.getPreferences');
// Forwarded to the AppView; claiming it would describe the AppView.
expect(values).not.toContain('app.bsky.feed.getTimeline');
});
it('names no space method while spaces are off', async () => {
const body = await describeBody(createPds());
const values = body.methods.map((/** @type {any} */ m) => m.value);
expect(values.some((/** @type {string} */ v) => v.includes('space'))).toBe(
false,
);
});
it('names the space methods once spaces are on', async () => {
const body = await describeBody(
createPds(
/** @type {any} */ ({
'/xrpc/com.atproto.space.listSpaces': {
handler: async () => new Response(),
},
'/xrpc/com.atproto.space.createSpace': {
handler: async () => new Response(),
},
}),
),
);
const values = body.methods.map((/** @type {any} */ m) => m.value);
expect(values).toContain('com.atproto.space.listSpaces');
expect(values).toContain('com.atproto.space.createSpace');
});
});