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} */ 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} */ 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} */ 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); }); }); });