chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { describe, expect, test, vi } from 'vitest';
import {
createCachedDidResolver,
extractPdsEndpoint,
resolveDid,
} from '../src/did.js';
describe('did', () => {
describe('extractPdsEndpoint', () => {
test('extracts PDS endpoint from DID document', () => {
const didDoc = {
service: [
{
id: '#atproto_pds',
type: 'AtprotoPersonalDataServer',
serviceEndpoint: 'https://pds.example.com',
},
],
};
expect(extractPdsEndpoint(didDoc)).toBe('https://pds.example.com');
});
test('finds PDS by type if id differs', () => {
const didDoc = {
service: [
{
id: '#pds',
type: 'AtprotoPersonalDataServer',
serviceEndpoint: 'https://pds.example.com',
},
],
};
expect(extractPdsEndpoint(didDoc)).toBe('https://pds.example.com');
});
test('returns null if no PDS service', () => {
const didDoc = { service: [] };
expect(extractPdsEndpoint(didDoc)).toBeNull();
});
test('returns null if no service array', () => {
const didDoc = {};
expect(extractPdsEndpoint(didDoc)).toBeNull();
});
});
describe('resolveDid', () => {
test('resolves did:plc via PLC directory', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
service: [
{
id: '#atproto_pds',
type: 'AtprotoPersonalDataServer',
serviceEndpoint: 'https://bsky.social',
},
],
}),
});
const endpoint = await resolveDid('did:plc:xyz123', { fetch: mockFetch });
expect(mockFetch).toHaveBeenCalledWith(
'https://plc.directory/did:plc:xyz123',
);
expect(endpoint).toBe('https://bsky.social');
});
test('resolves did:web via .well-known', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
service: [
{
id: '#atproto_pds',
type: 'AtprotoPersonalDataServer',
serviceEndpoint: 'https://example.com',
},
],
}),
});
const endpoint = await resolveDid('did:web:example.com', {
fetch: mockFetch,
});
expect(mockFetch).toHaveBeenCalledWith(
'https://example.com/.well-known/did.json',
);
expect(endpoint).toBe('https://example.com');
});
test('returns null for unknown DID method', async () => {
const endpoint = await resolveDid('did:unknown:xyz', { fetch: vi.fn() });
expect(endpoint).toBeNull();
});
test('returns null on fetch error', async () => {
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
const endpoint = await resolveDid('did:plc:xyz', { fetch: mockFetch });
expect(endpoint).toBeNull();
});
});
describe('createCachedDidResolver', () => {
const DID = 'did:plc:cachedaccount0000000000000';
/**
* A fetch answering with `doc`, or 404 once `doc` is null.
* @param {Object|null} doc
*/
function fetchServing(doc) {
return vi.fn(async () => ({
ok: doc !== null,
json: async () => doc,
}));
}
/**
* The mocks here answer with the two fields resolveDidDocument reads, not
* a whole Response.
* @param {Object} opts
*/
const cachedResolver = (opts) =>
createCachedDidResolver(/** @type {any} */ (opts));
test('reads the directory once for repeated resolutions', async () => {
const fetch = fetchServing({ id: DID });
const resolve = cachedResolver({ fetch });
expect(await resolve(DID)).toEqual({ id: DID });
expect(await resolve(DID)).toEqual({ id: DID });
expect(fetch).toHaveBeenCalledTimes(1);
});
test('reads once for resolutions that arrive together', async () => {
const fetch = fetchServing({ id: DID });
const resolve = cachedResolver({ fetch });
const answers = await Promise.all([
resolve(DID),
resolve(DID),
resolve(DID),
]);
expect(answers).toEqual([{ id: DID }, { id: DID }, { id: DID }]);
expect(fetch).toHaveBeenCalledTimes(1);
});
test('reads again once the answer has expired', async () => {
vi.useFakeTimers();
try {
const fetch = fetchServing({ id: DID });
const resolve = cachedResolver({ fetch, ttl: 1000 });
await resolve(DID);
vi.advanceTimersByTime(1001);
await resolve(DID);
expect(fetch).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
test('forceRefresh reads past the held answer', async () => {
const fetch = fetchServing({ id: DID });
const resolve = cachedResolver({ fetch });
await resolve(DID);
await resolve(DID, { forceRefresh: true });
expect(fetch).toHaveBeenCalledTimes(2);
});
test('serves a rotated document after forceRefresh', async () => {
const fetch = vi
.fn()
.mockResolvedValueOnce({ ok: true, json: async () => ({ key: 'old' }) })
.mockResolvedValue({ ok: true, json: async () => ({ key: 'new' }) });
const resolve = cachedResolver({ fetch });
expect(await resolve(DID)).toEqual({ key: 'old' });
expect(await resolve(DID, { forceRefresh: true })).toEqual({
key: 'new',
});
// The refreshed answer replaces the held one.
expect(await resolve(DID)).toEqual({ key: 'new' });
});
test('holds an unresolvable DID for the shorter negative interval', async () => {
vi.useFakeTimers();
try {
const fetch = fetchServing(null);
const resolve = cachedResolver({
fetch,
ttl: 100000,
negativeTtl: 1000,
});
expect(await resolve(DID)).toBeNull();
expect(await resolve(DID)).toBeNull();
expect(fetch).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(1001);
expect(await resolve(DID)).toBeNull();
expect(fetch).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
test('serves the held document when the directory stops answering', async () => {
vi.useFakeTimers();
try {
const fetch = vi
.fn()
.mockResolvedValueOnce({ ok: true, json: async () => ({ id: DID }) })
.mockRejectedValue(new Error('directory unreachable'));
const resolve = cachedResolver({ fetch, ttl: 1000 });
await resolve(DID);
vi.advanceTimersByTime(1001);
expect(await resolve(DID)).toEqual({ id: DID });
} finally {
vi.useRealTimers();
}
});
test('stops serving a held document past maxTtl', async () => {
vi.useFakeTimers();
try {
const fetch = vi
.fn()
.mockResolvedValueOnce({ ok: true, json: async () => ({ id: DID }) })
.mockRejectedValue(new Error('directory unreachable'));
const resolve = cachedResolver({
fetch,
ttl: 1000,
maxTtl: 5000,
});
await resolve(DID);
vi.advanceTimersByTime(5001);
expect(await resolve(DID)).toBeNull();
} finally {
vi.useRealTimers();
}
});
});
});