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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
633 lines · 20.1 KB Raw History
import { describe, expect, test, vi } from 'vitest';
import { LexiconResolver } from '../src/resolver.js';
import { knownRecords } from '../src/schemas/known-records.js';

/** @type {import('@bigmoves/lexicon').Lexicon} */
const postSchema = {
  lexicon: 1,
  id: 'app.bsky.feed.post',
  defs: {
    main: {
      type: 'record',
      key: 'tid',
      record: {
        type: 'object',
        required: ['text', 'createdAt'],
        properties: {
          text: { type: 'string', maxLength: 3000 },
          createdAt: { type: 'string', format: 'datetime' },
        },
      },
    },
  },
};

describe('LexiconResolver', () => {
  describe('with static schemas', () => {
    test('validates record against pre-loaded schema', async () => {
      const resolver = new LexiconResolver({
        schemas: [postSchema],
      });

      const status = await resolver.assertValid('app.bsky.feed.post', {
        $type: 'app.bsky.feed.post',
        text: 'Hello world',
        createdAt: new Date().toISOString(),
      });

      expect(status).toBe('valid');
    });

    test('throws on invalid record', async () => {
      const resolver = new LexiconResolver({
        schemas: [postSchema],
      });

      await expect(
        resolver.assertValid('app.bsky.feed.post', {
          $type: 'app.bsky.feed.post',
          // missing required fields
        }),
      ).rejects.toThrow();
    });

    test('returns unknown for unloaded schema', async () => {
      const resolver = new LexiconResolver({
        schemas: [postSchema],
      });

      const status = await resolver.assertValid('com.example.unknown', {
        $type: 'com.example.unknown',
      });

      expect(status).toBe('unknown');
    });
  });

  describe('with an unresolvable ref', () => {
    /** @type {import('@bigmoves/lexicon').Lexicon} */
    const photoSchema = {
      lexicon: 1,
      id: 'com.example.photo',
      defs: {
        main: {
          type: 'record',
          key: 'tid',
          record: {
            type: 'object',
            required: ['alt'],
            properties: {
              alt: { type: 'string' },
              aspectRatio: { type: 'ref', ref: 'com.example.defs#aspectRatio' },
            },
          },
        },
      },
    };

    /** A resolver holding the record schema but unable to fetch its ref. */
    const offlineResolver = () =>
      new LexiconResolver({
        schemas: [photoSchema],
        fetch: vi.fn().mockRejectedValue(new Error('Network error')),
      });

    test('returns unknown instead of rejecting the record', async () => {
      const status = await offlineResolver().assertValid('com.example.photo', {
        $type: 'com.example.photo',
        alt: 'a photo',
        aspectRatio: { width: 3000, height: 2000 },
      });

      expect(status).toBe('unknown');
    });

    test('still rejects a record broken independently of the ref', async () => {
      await expect(
        offlineResolver().assertValid('com.example.photo', {
          $type: 'com.example.photo',
          // missing required 'alt', and no ref field to excuse it
        }),
      ).rejects.toThrow(/alt/);
    });

    test('reports valid when the record omits the unresolvable field', async () => {
      const status = await offlineResolver().assertValid('com.example.photo', {
        $type: 'com.example.photo',
        alt: 'a photo',
      });

      expect(status).toBe('valid');
    });
  });

  describe('with a network-resolved schema', () => {
    /** @type {import('@bigmoves/lexicon').Lexicon} */
    const gallerySchema = {
      lexicon: 1,
      id: 'com.example.gallery',
      defs: {
        main: {
          type: 'record',
          key: 'tid',
          record: {
            type: 'object',
            required: ['title', 'alt'],
            properties: {
              title: { type: 'string' },
              alt: { type: 'string' },
            },
          },
        },
      },
    };

    /** Mocks the DNS -> DID -> getRecord chain so the schema arrives fetched. */
    const fetchedResolver = () => {
      const fetch = vi.fn().mockImplementation((url) => {
        const u = String(url);
        if (u.includes('dns-query')) {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                Answer: [{ type: 16, data: '"did=did:plc:testdid"' }],
              }),
          });
        }
        if (u.includes('plc.directory')) {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                service: [
                  {
                    id: '#atproto_pds',
                    type: 'AtprotoPersonalDataServer',
                    serviceEndpoint: 'https://pds.example.com',
                  },
                ],
              }),
          });
        }
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve({ value: gallerySchema }),
        });
      });
      return new LexiconResolver({ fetch });
    };

    // The record omits 'alt', which the fetched lexicon requires — the shape of
    // a published lexicon lagging the client that writes the records.
    const drifted = { $type: 'com.example.gallery', title: 'hi' };

    test('reports unknown rather than rejecting the write', async () => {
      expect(
        await fetchedResolver().assertValid('com.example.gallery', drifted),
      ).toBe('unknown');
    });

    test('reports unknown without resolving, even when the record conforms', async () => {
      const resolver = fetchedResolver();

      expect(
        await resolver.assertValid('com.example.gallery', {
          ...drifted,
          alt: 'a photo',
        }),
      ).toBe('unknown');
      // The reference PDS answers 'unknown' for a lexicon it does not hold.
      // Resolving one cannot reject the write either way, so the chain never
      // runs on the write path.
      expect(resolver.fetch).not.toHaveBeenCalled();
    });

    test('enforces it when the caller asks for strict validation', async () => {
      await expect(
        fetchedResolver().assertValid('com.example.gallery', drifted, {
          strict: true,
        }),
      ).rejects.toThrow(/alt/);
    });

    test('enforces a collection on the known-records list', async () => {
      // Same drifted-record shape, but for a collection the reference PDS
      // validates strictly — so it must be rejected, not waved through.
      const resolver = fetchedResolver();
      resolver.enforced.add('com.example.gallery');

      await expect(
        resolver.assertValid('com.example.gallery', drifted),
      ).rejects.toThrow(/alt/);
    });

    test('honours an operator-supplied enforce list', async () => {
      const bare = new LexiconResolver({ enforce: [] });
      expect(bare.enforced.has('app.bsky.feed.post')).toBe(false);
      // Pre-loaded schemas are enforced regardless of the list.
      const withStatic = new LexiconResolver({
        enforce: [],
        schemas: [postSchema],
      });
      expect(withStatic.enforced.has('app.bsky.feed.post')).toBe(true);
    });

    test('strict validation fails when nothing resolves', async () => {
      const resolver = new LexiconResolver({
        fetch: vi.fn().mockRejectedValue(new Error('Network error')),
      });

      await expect(
        resolver.assertValid(
          'com.example.unknown',
          { $type: 'com.example.unknown' },
          { strict: true },
        ),
      ).rejects.toThrow(/no published lexicon/);
    });
  });

  describe('ensureSchemaWithRefs', () => {
    test('collects refs it could not resolve', async () => {
      /** @type {import('@bigmoves/lexicon').Lexicon} */
      const schema = {
        lexicon: 1,
        id: 'com.example.photo',
        defs: {
          main: {
            type: 'record',
            key: 'tid',
            record: {
              type: 'object',
              properties: {
                aspectRatio: {
                  type: 'ref',
                  ref: 'com.example.defs#aspectRatio',
                },
              },
            },
          },
        },
      };
      const resolver = new LexiconResolver({
        schemas: [schema],
        fetch: vi.fn().mockRejectedValue(new Error('Network error')),
      });

      /** @type {Set<string>} */
      const unresolved = new Set();
      expect(
        await resolver.ensureSchemaWithRefs('com.example.photo', unresolved),
      ).toBe(true);
      expect([...unresolved]).toEqual(['com.example.defs']);
    });

    test('terminates on a ref cycle', async () => {
      /** @type {import('@bigmoves/lexicon').Lexicon} */
      const a = {
        lexicon: 1,
        id: 'com.example.a',
        defs: {
          main: {
            type: 'object',
            properties: { b: { type: 'ref', ref: 'com.example.b#main' } },
          },
        },
      };
      /** @type {import('@bigmoves/lexicon').Lexicon} */
      const b = {
        lexicon: 1,
        id: 'com.example.b',
        defs: {
          main: {
            type: 'object',
            properties: { a: { type: 'ref', ref: 'com.example.a#main' } },
          },
        },
      };
      const resolver = new LexiconResolver({ schemas: [a, b] });

      /** @type {Set<string>} */
      const unresolved = new Set();
      expect(
        await resolver.ensureSchemaWithRefs('com.example.a', unresolved),
      ).toBe(true);
      expect([...unresolved]).toEqual([]);
    });
  });

  describe('ensureSchema', () => {
    test('returns true for pre-loaded schema', async () => {
      const resolver = new LexiconResolver({
        schemas: [postSchema],
      });

      const result = await resolver.ensureSchema('app.bsky.feed.post');
      expect(result).toBe(true);
    });

    test('returns false for unknown schema when resolution fails', async () => {
      const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const resolver = new LexiconResolver({ fetch: mockFetch });

      const result = await resolver.ensureSchema('com.example.unknown');
      expect(result).toBe(false);
    });

    test('concurrent callers share one resolution and all get its answer', async () => {
      /** @type {(value: unknown) => void} */
      let releaseDns = () => {};
      const mockFetch = vi
        .fn()
        // DNS lookup, held open until released so callers pile up mid-flight
        .mockImplementationOnce(
          () =>
            new Promise((resolve) => {
              releaseDns = resolve;
            }),
        )
        // DID resolution
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              service: [
                {
                  id: '#atproto_pds',
                  type: 'AtprotoPersonalDataServer',
                  serviceEndpoint: 'https://pds.example.com',
                },
              ],
            }),
        })
        // Lexicon fetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ value: postSchema }),
        });

      // A collection outside the preloaded set, so the chain actually runs.
      const resolver = new LexiconResolver({ fetch: mockFetch });
      const first = resolver.ensureSchema('com.example.thing');
      const second = resolver.ensureSchema('com.example.thing');
      const third = resolver.ensureSchema('com.example.thing');

      releaseDns({
        ok: true,
        json: () =>
          Promise.resolve({
            Answer: [{ type: 16, data: '"did=did:plc:testdid"' }],
          }),
      });

      expect(await Promise.all([first, second, third])).toEqual([
        true,
        true,
        true,
      ]);
      // One resolution chain (DNS, DID, record), not one per caller.
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });
  });

  describe('isPublished', () => {
    test('returns true for a pre-loaded schema', async () => {
      const resolver = new LexiconResolver({ schemas: [postSchema] });
      expect(await resolver.isPublished('app.bsky.feed.post')).toBe(true);
    });

    test('returns false for an unpublished NSID', async () => {
      const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const resolver = new LexiconResolver({ fetch: mockFetch });
      expect(await resolver.isPublished('com.example.unknown')).toBe(false);
    });

    test('caches the negative so a repeat check does not re-hit DNS', async () => {
      const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const resolver = new LexiconResolver({ fetch: mockFetch });

      expect(await resolver.isPublished('com.example.unknown')).toBe(false);
      const callsAfterFirst = mockFetch.mock.calls.length;
      expect(await resolver.isPublished('com.example.unknown')).toBe(false);
      expect(mockFetch.mock.calls.length).toBe(callsAfterFirst);
    });

    test('re-resolves a negative once negativeTtl elapses', async () => {
      const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const resolver = new LexiconResolver({
        fetch: mockFetch,
        negativeTtl: 0,
      });

      expect(await resolver.isPublished('com.example.unknown')).toBe(false);
      const callsAfterFirst = mockFetch.mock.calls.length;
      expect(await resolver.isPublished('com.example.unknown')).toBe(false);
      expect(mockFetch.mock.calls.length).toBeGreaterThan(callsAfterFirst);
    });
  });

  describe('getLexicon', () => {
    test('returns the resolved document for a pre-loaded schema', async () => {
      const resolver = new LexiconResolver({ schemas: [postSchema] });
      expect(await resolver.getLexicon('app.bsky.feed.post')).toEqual(
        postSchema,
      );
    });

    test('returns null for an unresolvable NSID', async () => {
      const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const resolver = new LexiconResolver({ fetch: mockFetch });
      expect(await resolver.getLexicon('com.example.unknown')).toBeNull();
    });
  });

  describe('fetchLexicon', () => {
    test('resolves lexicon via DNS -> DID -> PDS chain', async () => {
      const mockFetch = vi
        .fn()
        // DNS lookup
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              Answer: [{ type: 16, data: '"did=did:plc:testdid"' }],
            }),
        })
        // DID resolution
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              service: [
                {
                  id: '#atproto_pds',
                  type: 'AtprotoPersonalDataServer',
                  serviceEndpoint: 'https://pds.example.com',
                },
              ],
            }),
        })
        // Lexicon fetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ value: postSchema }),
        });

      const resolver = new LexiconResolver({ fetch: mockFetch });
      const lexicon = await resolver.fetchLexicon('app.bsky.feed.post');

      expect(lexicon).toEqual(postSchema);
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });
  });

  describe('preloaded lexicons', () => {
    test('validates an enforced record type without any network call', async () => {
      const fetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const resolver = new LexiconResolver({ fetch });

      const status = await resolver.assertValid('app.bsky.feed.post', {
        $type: 'app.bsky.feed.post',
        text: 'Hello world',
        createdAt: new Date().toISOString(),
        facets: [
          {
            index: { byteStart: 0, byteEnd: 5 },
            features: [
              {
                $type: 'app.bsky.richtext.facet#link',
                uri: 'https://a.example',
              },
            ],
          },
        ],
      });

      expect(status).toBe('valid');
      expect(fetch).not.toHaveBeenCalled();
    });

    test('rejects a broken record from the preloaded lexicon', async () => {
      const fetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const resolver = new LexiconResolver({ fetch });

      await expect(
        resolver.assertValid('app.bsky.feed.post', {
          $type: 'app.bsky.feed.post',
          text: 'no createdAt',
        }),
      ).rejects.toThrow(/createdAt/);
      expect(fetch).not.toHaveBeenCalled();
    });

    test('every enforced record type is preloaded', async () => {
      const fetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const resolver = new LexiconResolver({ fetch });

      for (const nsid of knownRecords) {
        expect(resolver.schemas.has(nsid)).toBe(true);
      }
      expect(fetch).not.toHaveBeenCalled();
    });

    test('a caller-supplied schema wins over the preloaded one', async () => {
      const stub = { ...postSchema, id: 'app.bsky.feed.post' };
      const resolver = new LexiconResolver({ schemas: [stub] });

      expect(await resolver.getLexicon('app.bsky.feed.post')).toEqual(stub);
    });
  });

  describe('resolution caching', () => {
    /** Answers the DNS -> DID -> getRecord chain, counting each leg. */
    const chainFetch = () =>
      vi.fn().mockImplementation((url) => {
        const u = String(url);
        if (u.includes('dns-query')) {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                Answer: [{ type: 16, data: '"did=did:plc:testdid"' }],
              }),
          });
        }
        if (u.includes('plc.directory')) {
          return Promise.resolve({
            ok: true,
            json: () =>
              Promise.resolve({
                service: [
                  {
                    id: '#atproto_pds',
                    type: 'AtprotoPersonalDataServer',
                    serviceEndpoint: 'https://pds.example.com',
                  },
                ],
              }),
          });
        }
        return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
      });

    const legs = (/** @type {ReturnType<typeof chainFetch>} */ fetch) => {
      const urls = fetch.mock.calls.map((/** @type {unknown[]} */ c) =>
        String(c[0]),
      );
      return {
        dns: urls.filter((u) => u.includes('dns-query')).length,
        plc: urls.filter((u) => u.includes('plc.directory')).length,
      };
    };

    test('two NSIDs under one authority share the DNS and DID lookups', async () => {
      const fetch = chainFetch();
      const resolver = new LexiconResolver({ fetch });

      await resolver.ensureSchema('com.example.one');
      await resolver.ensureSchema('com.example.two');

      // com.example.one and com.example.two both live at example.com.
      expect(legs(fetch)).toEqual({ dns: 1, plc: 1 });
    });

    test('concurrent NSIDs under one authority share them too', async () => {
      const fetch = chainFetch();
      const resolver = new LexiconResolver({ fetch });

      await Promise.all([
        resolver.ensureSchema('com.example.one'),
        resolver.ensureSchema('com.example.two'),
        resolver.ensureSchema('com.example.three'),
      ]);

      expect(legs(fetch)).toEqual({ dns: 1, plc: 1 });
    });

    test('an unresolvable NSID does not repeat the chain', async () => {
      const fetch = vi.fn().mockRejectedValue(new Error('Network error'));
      const resolver = new LexiconResolver({ fetch });

      expect(await resolver.ensureSchema('com.example.unknown')).toBe(false);
      const afterFirst = fetch.mock.calls.length;
      expect(afterFirst).toBeGreaterThan(0);

      expect(await resolver.ensureSchema('com.example.unknown')).toBe(false);
      expect(fetch.mock.calls.length).toBe(afterFirst);
    });

    test('a preloaded lexicon whose refresh fails does not retry every call', async () => {
      const fetch = vi.fn().mockRejectedValue(new Error('Network error'));
      // ttl 0 ages the preloaded schemas out immediately.
      const resolver = new LexiconResolver({ fetch, ttl: 0 });

      expect(await resolver.ensureSchema('app.bsky.feed.post')).toBe(true);
      const afterFirst = fetch.mock.calls.length;
      expect(afterFirst).toBeGreaterThan(0);

      expect(await resolver.ensureSchema('app.bsky.feed.post')).toBe(true);
      expect(fetch.mock.calls.length).toBe(afterFirst);
    });
  });
});