fix(db): own the two settings seeds, and repair the protocol-3 one-shot (phase 3, slice 4) #5

Merged
whitlocktech merged 2 commits from feature/own-settings-seeds into main 2026-08-12 03:01:34 +00:00
Showing only changes of commit d70e5e10d0 - Show all commits

139
client/test/api.test.js Normal file
View File

@@ -0,0 +1,139 @@
// ── The URLs this module calls ─────────────────────────────────────────────
//
// `src/api.js` binds the paths whose routes live in `server/router/**`, and the
// interesting assertions about it are the ones that encode a DECISION rather
// than a spelling. Three of these came across from core's `apiClient.test.js`
// in slice 4: they had stayed behind when the bindings moved, still asserting
// UO URLs from inside core's suite, which is the boundary this phase removes.
//
// What is NOT re-tested here is the fetch wrapper itself — status mapping, empty
// bodies, FormData, cookie inclusion. That is `req`, core's primitive, and core
// tests it. A module asserting core's contract back at it is a second copy that
// drifts.
//
// The chunk reads its shared bindings off `window.__rg` at module scope
// (src/core.js), so the fake global has to be in place before `src/api.js` is
// imported — hence the dynamic import below rather than a static one.
import { test, beforeEach, afterEach } from 'node:test'
import assert from 'node:assert/strict'
import * as react from 'react'
import * as reactDom from 'react-dom/client'
import * as router from 'react-router-dom'
import * as jsxRuntime from 'react/jsx-runtime'
const BASE = '/api/v1'
let calls = []
function reply({ status = 200, statusText = 'OK', body = '' } = {}) {
return {
ok: status >= 200 && status < 300,
status,
statusText,
text: async () => (typeof body === 'string' ? body : JSON.stringify(body)),
}
}
// Core's `req`, close enough for a path assertion: the only property this file
// cares about is the URL it was handed. Recording it here rather than mocking
// global.fetch keeps the test honest about the boundary — a module never sees
// fetch, it sees the primitive.
function request(path, opts = {}) {
calls.push({ url: BASE + path, opts })
return Promise.resolve(reply({ body: {} }).text().then(() => ({})))
}
// The REAL react/react-dom/router go in, not stubs: `src/core.js` compares the
// bindings it imported against the ones here and logs a "bundled its own copy"
// error when they differ. With stubs that error fires on every run of this file
// — a false alarm in the exact words of a real defect, which is how a check
// gets ignored.
globalThis.window = globalThis.window || {}
globalThis.window.__rg = {
react, reactDom, router, jsxRuntime,
api: { request, BASE },
ui: {},
registry: { registerRoutes() {}, registerNav() {}, registerFeatureProvider() {}, registerExtension() {} },
}
const { shard, atlas, admin } = await import('../src/api.js')
beforeEach(() => {
calls = []
})
afterEach(() => {
calls = []
})
// ── spawn atlas (Protocol 3.0 Part C) ───────────────────────────────────────
// The atlas lives at /public/atlas, NOT under /public/shard: it is static shard
// content parsed from the shard's own files, so it must not look sidecar-backed.
// Asserted because the split is a design decision, not an accident of spelling.
test('atlas reads hit /public/atlas, not /public/shard', async () => {
await atlas.creatures()
assert.equal(calls[0].url, '/api/v1/public/atlas/creatures')
})
test('atlas.creatures() sends only the filters that are set', async () => {
await atlas.creatures({ q: 'lizard man', facet: 'Ter Mur', limit: 25 })
const url = new URL(calls[0].url, 'http://x')
assert.equal(url.pathname, '/api/v1/public/atlas/creatures')
assert.equal(url.searchParams.get('q'), 'lizard man')
assert.equal(url.searchParams.get('facet'), 'Ter Mur')
assert.equal(url.searchParams.get('limit'), '25')
assert.equal(url.searchParams.get('offset'), null) // 0 is not sent
})
test('atlas.creature() encodes the slug and carries the facet filter through', async () => {
await atlas.creature('lizardman/rare', { facet: 'Felucca' })
assert.match(calls[0].url, /\/public\/atlas\/creatures\/lizardman%2Frare\?facet=Felucca$/)
})
test('admin atlas actions use the right methods and bodies', async () => {
await admin.atlas.import(true)
assert.equal(calls[0].url, '/api/v1/admin/shard/atlas/import')
assert.equal(calls[0].opts.method, 'POST')
assert.deepEqual(calls[0].opts.body, { force: true })
await admin.atlas.setPath('/srv/servuo')
assert.equal(calls[1].opts.method, 'PUT')
assert.deepEqual(calls[1].opts.body, { path: '/srv/servuo' })
})
// ── path encoding ───────────────────────────────────────────────────────────
// A city name with an apostrophe and a space is the real case: "Serpent's Hold"
// is a governor city, and an unencoded one would break the route match rather
// than 404 cleanly.
test('path params are URL-encoded', async () => {
await shard.governorHistory('Serpents Hold', 5)
assert.match(calls[0].url, /\/governors\/Serpent%E2%80%99s%20Hold\/history\?limit=5/)
})
// ── the API surface §1.2 freezes ────────────────────────────────────────────
// The shipped Android app calls these seven by name (data/api/AdminApi.kt), which
// is why the extraction moved which repo declares them and not what they are. A
// rename here is a client break, not a refactor.
test('the seven admin URLs the Android app calls are unchanged', async () => {
const expected = [
['kick', '/api/v1/admin/shard/kick'],
['ban', '/api/v1/admin/shard/ban'],
['unban', '/api/v1/admin/shard/unban'],
['broadcast', '/api/v1/admin/shard/broadcast'],
]
for (const [fn, url] of expected) {
calls = []
await admin.shardOps[fn]({})
assert.equal(calls[0].url, url, fn)
}
calls = []
await admin.shardOps.pages()
assert.equal(calls[0].url, '/api/v1/admin/shard/pages')
calls = []
await admin.shardOps.respondPage('7', {})
assert.equal(calls[0].url, '/api/v1/admin/shard/pages/7/respond')
calls = []
await admin.shardOps.closePage('7')
assert.equal(calls[0].url, '/api/v1/admin/shard/pages/7/close')
})