import assert from 'node:assert/strict'; import test from 'node:test'; import { filtersOf, requiresKeyIdFor, validateProviders } from './registry.js'; const def = (overrides = {}) => ({ id: 'mapillary', name: 'Mapillary', label: 'MAPILLARY', requiresKeyId: 'mapillary', pickPrefix: 'mly:', colors: { coverage: '#05cb63' }, credit: { html: 'Mapillary' }, create: () => ({}), ...overrides, }); test('a valid list is frozen in registration order', () => { const list = validateProviders([ def(), def({ id: 'panoramax', pickPrefix: 'pnx:', requiresKeyId: null }), ]); assert.ok(Object.isFrozen(list)); assert.deepEqual( list.map((p) => p.id), ['mapillary', 'panoramax'], ); }); test('an empty provider list is valid and has no key requirement', () => { assert.deepEqual(validateProviders([]), []); assert.ok(Object.isFrozen(validateProviders([]))); assert.equal(requiresKeyIdFor([]), null); }); test('non-arrays, duplicate ids and malformed ids are rejected', () => { assert.throws(() => validateProviders(null), /array/); assert.throws(() => validateProviders([def(), def()]), /Duplicate/); assert.throws(() => validateProviders([def({ id: 'Bad Id' })]), /slug/); }); test('every required field is checked by name', () => { for (const key of [ 'name', 'label', 'pickPrefix', 'colors', 'credit', 'create', ]) assert.throws( () => validateProviders([def({ [key]: undefined })]), new RegExp(`lacks ${key}`), key, ); assert.throws(() => validateProviders([def({ create: 1 })]), /create/); assert.throws(() => validateProviders([def({ credit: {} })]), /credit/); assert.throws( () => validateProviders([def({ colors: { pano: '#f0f' } })]), /coverage colour/, ); assert.throws( () => validateProviders([def({ pickPrefix: '' })]), /pick prefix/, ); }); test('overlapping pick prefixes are rejected either way round', () => { assert.throws( () => validateProviders([def(), def({ id: 'other', pickPrefix: 'mly:x' })]), /overlaps/, ); assert.throws( () => validateProviders([def({ pickPrefix: 'mly:x' }), def({ id: 'other' })]), /overlaps/, ); assert.doesNotThrow(() => validateProviders([def(), def({ id: 'other', pickPrefix: 'ml:' })]), ); }); test('requiresKeyIdFor is the shared key, or null once providers differ', () => { assert.equal(requiresKeyIdFor([def()]), 'mapillary'); assert.equal( requiresKeyIdFor([def(), def({ id: 'b', pickPrefix: 'b:' })]), 'mapillary', ); assert.equal( requiresKeyIdFor([ def(), def({ id: 'b', pickPrefix: 'b:', requiresKeyId: null }), ]), null, ); assert.equal( requiresKeyIdFor([ def(), def({ id: 'b', pickPrefix: 'b:', requiresKeyId: 'google-maps' }), ]), null, ); assert.equal(requiresKeyIdFor([def({ requiresKeyId: null })]), null); }); test('a provider may narrow the filters it honours and pick a view-cone marker', () => { assert.deepEqual(filtersOf(def()), ['pano', 'since'], 'both by default'); const narrowed = def({ filters: [], marker: { kind: 'view-cone', color: '#4285f4' }, }); assert.deepEqual(filtersOf(validateProviders([narrowed])[0]), []); assert.throws( () => validateProviders([def({ filters: ['colour'] })]), /filters must list pano or since/, ); assert.throws( () => validateProviders([def({ marker: { kind: 'pegman' } })]), /marker must be/, ); validateProviders([def({ sinceBasis: 'upload' })]); assert.throws( () => validateProviders([def({ sinceBasis: 'shot' })]), /sinceBasis must be 'capture' or 'upload'/, ); });