johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
// What knotless-serve answers over XRPC, against the lexicons that say
// what it answers: each answer the screenshots recorded (shots/
// fixtures.json), as `just shots-record` takes them from the real server,
// or, given a directory, each one knotless-serve's tests received, as
// harness/cov.mjs gathers them there (lines of `{path, body}`). A query's
// output must be valid for its lexicon, a failure must be XRPC's
// `{error, message}`, and every parameter asked must be one it declares.
//
// node harness/answers.mjs [dir]
//
// @atproto/lexicon is the harness's, or, where its packages are not
// installed, one NODE_PATH names.
import { readdirSync, readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const { Lexicons } = createRequire(import.meta.url)('@atproto/lexicon')
const files = (d) =>
readdirSync(d, { withFileTypes: true }).flatMap((e) =>
e.isDirectory() ? files(join(d, e.name)) : e.name.endsWith('.json') ? [join(d, e.name)] : [],
)
const docs = files(join(root, 'lexicons')).map((f) => JSON.parse(readFileSync(f, 'utf8')))
const lexicons = new Lexicons(docs)
// A record in a space is named in full, as atproto's permissioned data
// proposal (0016) addresses it:
// at://<space did>/space/<type>/<key>/<author>/<collection>/<rkey>. The
// harness's @atproto/lexicon knows only the public grammar of an at-uri,
// so such a URI is held to it as its public form, its last three segments;
// knotless's own parser (crates/knotless/src/aturi.rs) holds the rest.
const SPACED = /^at:\/\/did:[^/]+\/space\/[^/]+\/[^/]+\/(did:[^/]+\/[^/]+\/[^/]+)$/
const publicForm = (v) =>
typeof v === 'string'
? v.replace(SPACED, 'at://$1')
: Array.isArray(v)
? v.map(publicForm)
: v && typeof v === 'object'
? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, publicForm(x)]))
: v
const [dir] = process.argv.slice(2)
// Each answer as [path, {status, headers, body}], the body as sent.
const answers = dir
? readdirSync(dir).flatMap((f) =>
readFileSync(join(dir, f), 'utf8')
.split('\n')
.filter(Boolean)
.map((l) => JSON.parse(l))
.map(({ path, body }) => [
path,
{
status: 200,
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
},
]),
)
: Object.entries(
JSON.parse(readFileSync(join(root, 'harness/shots/fixtures.json'), 'utf8')).answers,
)
const problems = []
// A path asked again and again is said once.
const said = new Set()
let checked = 0
for (const [path, answer] of answers) {
const url = new URL(path, 'http://knotless')
const nsid = url.pathname.match(/^\/xrpc\/(.+)$/)?.[1]
if (!nsid) continue
const say = (what) => {
const p = `${url.pathname}: ${what}`
if (!said.has(p)) problems.push(`${path}: ${what}`)
said.add(p)
}
const def = lexicons.getDef(nsid)
if (!def) {
say('no lexicon names this method')
continue
}
const declared = Object.keys(def.parameters?.properties ?? {})
for (const name of url.searchParams.keys())
if (!declared.includes(name)) say(`${name} is no parameter of ${nsid}`)
checked++
if (answer.headers['content-type'] !== 'application/json') continue
const body = JSON.parse(answer.body)
if (answer.status !== 200) {
if (typeof body.error !== 'string' || typeof body.message !== 'string')
say('a failure is {error, message}')
continue
}
try {
lexicons.assertValidXrpcOutput(nsid, publicForm(body))
} catch (e) {
say(e.message)
}
}
for (const p of problems) console.log(p)
if (problems.length) process.exit(1)
console.log(
`answers: ${checked} ${dir ? 'XRPC answers the tests received' : 'recorded XRPC answers'} are as their lexicons say`,
)