// ── 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') })