70 lines
5.1 KiB
JavaScript
70 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-list-prop-unit — the people-group LIST prop (library/lists/), measured WITHOUT a browser.
|
|
*
|
|
* Runs the real library module in Node against a stand-in window/location/fetch:
|
|
* - the ladder: profile-config `list` > ?list= > window.MAP_LIST > 'doxa'; ids and aliases ('700');
|
|
* an unknown value falls back to the default;
|
|
* - the default list returns the SAME array, untouched, with no fetch at all;
|
|
* - a list keeps exactly the rows whose slug is in its membership file, fetched from the asset pool;
|
|
* - a membership that cannot load FAILS CLOSED: no rows, never the whole DOXA list;
|
|
* - every registry list with an imb filter has a membership file, and its counts match the slugs.
|
|
* USAGE node internal/check-list-prop-unit.mjs EXIT 0 pass · 1 fail
|
|
*/
|
|
import { register } from 'node:module'
|
|
import { readFileSync, existsSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
// Vite imports JSON without an attribute; Node 20 does not. Teach this process the same thing.
|
|
register('data:text/javascript,' + encodeURIComponent(
|
|
"export async function load(url, ctx, next) { if (url.endsWith('.json')) return { format: 'json', shortCircuit: true, source: (await import('node:fs')).readFileSync(new URL(url)) }; return next(url, ctx) }"))
|
|
|
|
globalThis.window = globalThis
|
|
let search = ''
|
|
Object.defineProperty(globalThis, 'location', { configurable: true, get: () => ({ search, href: 'https://host.example/page' + search }) })
|
|
const fetched = []
|
|
let serve = () => ({ ok: false, status: 404 })
|
|
globalThis.fetch = async (url) => { fetched.push(String(url)); const r = serve(String(url)); return { ok: r.ok, status: r.status, json: async () => r.body } }
|
|
|
|
const L = await import('../library/lists/peopleGroupList.js')
|
|
const results = []
|
|
const check = (name, got, want) => {
|
|
const ok = JSON.stringify(got) === JSON.stringify(want)
|
|
results.push(ok)
|
|
console.log(` ${ok ? 'ok ' : 'FAIL'} ${name.padEnd(66)} ${JSON.stringify(got)}${ok ? '' : ' want ' + JSON.stringify(want)}`)
|
|
}
|
|
const reset = () => { search = ''; delete globalThis.MAP_LIST; fetched.length = 0 }
|
|
|
|
console.log('ladder')
|
|
reset(); check('nothing set -> the default', L.resolvePeopleGroupList(null), 'doxa')
|
|
reset(); check('profile-config list wins', (search = '?list=unreached', globalThis.MAP_LIST = 'frontier', L.resolvePeopleGroupList({ list: 'frontier-500k' })), 'frontier-500k')
|
|
reset(); check('?list= beats window.MAP_LIST', (search = '?list=frontier', globalThis.MAP_LIST = 'unreached', L.resolvePeopleGroupList({})), 'frontier')
|
|
reset(); check('window.MAP_LIST when nothing else', (globalThis.MAP_LIST = 'unreached', L.resolvePeopleGroupList(null)), 'unreached')
|
|
reset(); check('an alias: 700', L.resolvePeopleGroupList({ list: '700' }), 'frontier-500k')
|
|
reset(); check('an alias, any case: Project-7000', L.resolvePeopleGroupList({ list: 'Project-7000' }), 'unreached')
|
|
reset(); check('a JSON-string profile-config', L.resolvePeopleGroupList('{"list":"4700"}'), 'frontier')
|
|
reset(); check('a ref-shaped profile-config', L.resolvePeopleGroupList({ value: { list: '2000' } }), 'doxa')
|
|
reset(); check('unknown -> the default', L.resolvePeopleGroupList({ list: 'project-9999' }), 'doxa')
|
|
|
|
console.log('apply')
|
|
const rows = [{ slug: 'a' }, { slug: 'b' }, { slug: 'c' }]
|
|
reset(); { const out = await L.applyPeopleGroupList(rows, null); check('default: the same array, no fetch', [out === rows, fetched.length], [true, 0]) }
|
|
reset(); serve = (u) => (u.endsWith('/people-groups/lists/frontier.json') ? { ok: true, status: 200, body: { slugs: ['a', 'c'] } } : { ok: false, status: 404 })
|
|
{ const out = await L.applyPeopleGroupList(rows, { list: 'frontier', assetsBase: 'https://cdn.example/tree/assets' }); check('a list keeps exactly its members', out.map((r) => r.slug), ['a', 'c']) }
|
|
check('…fetched from the asset pool', fetched, ['https://cdn.example/tree/assets/people-groups/lists/frontier.json'])
|
|
reset(); serve = () => ({ ok: false, status: 404 })
|
|
{ const err = console.error; console.error = () => {}; const out = await L.applyPeopleGroupList(rows, { list: 'unreached', assetsBase: 'https://cdn.example/x/assets' }); console.error = err; check('an unloadable list FAILS CLOSED: no rows', out, []) }
|
|
|
|
console.log('membership files')
|
|
for (const l of L.peopleGroupLists().filter((x) => x.source === 'imb')) {
|
|
const f = join(ROOT, 'data/people-groups/lists', `${l.id}.json`)
|
|
if (!existsSync(f)) { check(`${l.id}: membership file exists`, false, true); continue }
|
|
const d = JSON.parse(readFileSync(f, 'utf8'))
|
|
check(`${l.id}: file matches its registry filter, counts match its slugs`, [JSON.stringify(d.filter) === JSON.stringify(l.filter), d.counts.doxa === d.slugs.length, d.counts.imb >= d.counts.doxa], [true, true, true])
|
|
}
|
|
|
|
const failed = results.filter((x) => !x).length
|
|
console.log(failed ? `LIST-PROP: FAIL — ${failed} case(s)` : `LIST-PROP: PASS — ${results.length} cases: the ladder, aliases, the untouched default, membership filtering, fail-closed, and the membership files`)
|
|
process.exit(failed ? 1 : 0)
|