Five assertions that stayed behind in core's `apiClient.test.js` when the bindings moved in slice 1 — the atlas-vs-shard path split, the query-string filtering, the slug encoding, the admin atlas methods — plus a new one pinning the seven admin URLs the shipped Android app calls by name. They were asserting UO URLs from inside core's suite, which is the boundary Phase 3 removes, and core's slice-4 deletion of those bindings would otherwise have deleted the coverage with them. The fake `window.__rg` carries the REAL react/react-dom/router rather than stubs: `src/core.js` compares its imported bindings against the published ones and logs a "bundled its own copy" error when they differ, so stubs make every run of this file print the exact wording of a real defect. Co-Authored-By: Claude <noreply@anthropic.com>
140 lines
6.1 KiB
JavaScript
140 lines
6.1 KiB
JavaScript
// ── 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('Serpent’s 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')
|
||
})
|