CI / test (ubuntu-latest, node 22) (push) Failing after 33m10s
CI / test (ubuntu-latest, node 24) (push) Failing after 34m41s
CI / tooling (lint, imports, typecheck) (push) Failing after 36m11s
CI / test (windows-latest, node 22) (push) Canceled after 0s
CI / test (windows-latest, node 24) (push) Canceled after 0s
416 lines
22 KiB
JavaScript
416 lines
22 KiB
JavaScript
// The /skland/* HTTP surface and its per-player orchestration (server/skland/service.js, server/skland/http.js).
|
|
//
|
|
// The server is started for real (startServer with `skland: { storeFile: ':memory:', fetchImpl }`), so the wiring —
|
|
// routes.js handing /skland/* to the module before its GET/HEAD gate, the JSON bodies, the store — is what is under
|
|
// test; only the upstream HTTP is a stub. Each case drives one login path the way the panel would.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { startServer } from '../../server/index.js';
|
|
|
|
/** The stub upstream (the shapes the community documentation records). */
|
|
function stubUpstream(overrides = {}) {
|
|
const calls = [];
|
|
const j = (o) => ({ status: 200, json: async () => o });
|
|
const answers = {
|
|
genScan: () => j({ status: 0, msg: 'OK', data: { scanId: 'SCAN1', scanUrl: 'hypergryph://scan_login?scanId=SCAN1' } }),
|
|
scanStatus: () => j({ status: 100, msg: '未扫码' }),
|
|
scanToken: () => j({ status: 0, msg: 'OK', data: { token: 'T-SCAN', hgId: 'HG1', deviceToken: 'DEV1' } }),
|
|
sendCode: () => j({ status: 0, msg: 'OK' }),
|
|
phoneToken: () => j({ status: 0, msg: 'OK', data: { token: 'T-CODE', hgId: 'HG1', deviceToken: 'DEV1' } }),
|
|
passwordToken: () => j({ status: 0, msg: 'OK', data: { token: 'T-PW' } }),
|
|
grant: ({ body }) => j({ status: 0, msg: 'OK', data: body.appCode === 'be36d44aa36bfb5b'
|
|
? { token: 'BINDINGTOKEN' } : { uid: 'U1', code: 'GRANTCODE' } }),
|
|
cred: () => j({ code: 0, data: { cred: 'CRED', token: 'CREDTOKEN' } }),
|
|
binding: () => j({ code: 0, data: { list: [{ appCode: 'arknights', bindingList: [{ uid: '123', nickName: '博士', channelName: '官服', gameId: 1 }] }] } }),
|
|
accountBinding: () => j({ status: 0, data: { list: [{ appCode: 'arknights', bindingList: [{ uid: '123', nickName: '博士', channelName: '官服', isDefault: true }] }] } }),
|
|
refresh: () => j({ code: 0, data: { token: 'CREDTOKEN2' } }),
|
|
};
|
|
const routes = [
|
|
['gen_scan/login', 'genScan'], ['scan_status', 'scanStatus'], ['token_by_scan_code', 'scanToken'],
|
|
['send_phone_code', 'sendCode'], ['token_by_phone_code', 'phoneToken'],
|
|
['token_by_phone_password', 'passwordToken'], ['oauth2/v2/grant', 'grant'],
|
|
['generate_cred_by_code', 'cred'], ['auth/refresh', 'refresh'], ['player/binding', 'binding'],
|
|
['account/binding/v1/binding_list', 'accountBinding'],
|
|
];
|
|
const fetchImpl = async (url, init = {}) => {
|
|
const key = routes.find(([frag]) => url.includes(frag))?.[1];
|
|
calls.push({ key, url, method: init.method || 'GET', body: init.body ? JSON.parse(init.body) : null });
|
|
const answer = overrides[key] ?? answers[key];
|
|
if (!answer) throw new Error(`stub: no route for ${url}`);
|
|
return answer(calls.at(-1));
|
|
};
|
|
return { fetchImpl, calls, byKey: (key) => calls.filter((c) => c.key === key) };
|
|
}
|
|
|
|
/** Start a server with the stubbed Skland module. */
|
|
async function withServer(overrides, fn) {
|
|
const up = stubUpstream(overrides);
|
|
const srv = await startServer({
|
|
port: 0, host: '127.0.0.1', quiet: true,
|
|
skland: { storeFile: ':memory:', fetchImpl: up.fetchImpl, diagnosticKey: 'test-admin-only' },
|
|
});
|
|
const post = (path, body) => fetch(srv.url + path, {
|
|
method: 'POST', headers: { 'content-type': 'application/json', 'x-sp-diagnostic-key': 'test-admin-only' }, body: JSON.stringify(body),
|
|
}).then(async (r) => ({ status: r.status, body: await r.json() }));
|
|
const get = (path) => fetch(srv.url + path, { headers: { 'x-sp-diagnostic-key': 'test-admin-only' } }).then(async (r) => ({ status: r.status, body: await r.json() }));
|
|
try { await fn({ srv, post, get, up }); } finally { await srv.close(); }
|
|
}
|
|
|
|
test('the server exposes the module when skland is configured', async () => {
|
|
await withServer({}, async ({ srv }) => {
|
|
assert.ok(srv.skland, 'startServer returns the service');
|
|
});
|
|
});
|
|
|
|
test('the module is off by default, and /skland/* still answers one uniform JSON 404', async () => {
|
|
const srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
|
|
try {
|
|
assert.equal(srv.skland, null, 'no account store on an unconfigured server');
|
|
const got = await fetch(srv.url + '/skland/status?player=p1');
|
|
assert.equal(got.status, 404, 'GET /skland/* is not routed to a store');
|
|
assert.equal((await got.json()).ok, false, 'the off-surface is JSON, not the HTML error page');
|
|
|
|
// A POST must not reach routes.js's GET/HEAD gate and answer "405 method not allowed" for a feature that is
|
|
// simply off — the panel would show a cryptic method error instead of "not enabled".
|
|
const posted = await fetch(srv.url + '/skland/scan/start', {
|
|
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ player: 'p1' }),
|
|
});
|
|
assert.equal(posted.status, 404, 'POST is 404 too, not 405');
|
|
assert.match((await posted.json()).error, /SP_SKLAND|未启用/);
|
|
} finally { await srv.close(); }
|
|
});
|
|
|
|
test('GET /skland/status: an unknown player reads as unbound', async () => {
|
|
await withServer({}, async ({ get }) => {
|
|
const { status, body } = await get('/skland/status?player=p1');
|
|
assert.equal(status, 200);
|
|
assert.deepEqual(body, { playerId: 'p1', status: 'unbound', bound: false, nickname: null, hgId: null, roles: [] });
|
|
});
|
|
});
|
|
|
|
test('GET /skland/status without a player is a JSON 400', async () => {
|
|
await withServer({}, async ({ get }) => {
|
|
const { status, body } = await get('/skland/status');
|
|
assert.equal(status, 400);
|
|
assert.equal(body.ok, false, 'a JSON client must never be handed the HTML error page');
|
|
assert.match(body.error, /player/);
|
|
});
|
|
});
|
|
|
|
test('扫码: start → unscanned → awaiting phone confirmation → confirmed binds the same session', async () => {
|
|
const pending = [
|
|
{ status: 100, msg: '未扫码' },
|
|
{ status: 101, msg: '已扫码待确认' },
|
|
{ status: 101, msg: '已扫码待确认' },
|
|
];
|
|
await withServer({
|
|
scanStatus: () => ({ status: 200, json: async () => pending.shift()
|
|
?? { status: 0, msg: 'OK', data: { scanCode: 'SC1' } } }),
|
|
binding: () => ({ status: 200, json: async () => ({ code: 0, data: { list: [{ appCode: 'arknights', bindingList: [
|
|
{ uid: '123', nickName: '博士', channelName: '官服', gameId: 1 },
|
|
{ uid: '456', nickName: 'B服博士', channelName: 'B服', gameId: 2 },
|
|
] }] } }) }),
|
|
}, async ({ post, get, up }) => {
|
|
const start = await post('/skland/scan/start', { player: 'p1' });
|
|
assert.equal(start.status, 200);
|
|
assert.equal(start.body.ok, true);
|
|
assert.equal(start.body.scanUrl, 'hypergryph://scan_login?scanId=SCAN1');
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
const waiting = await post('/skland/scan/poll', { player: 'p1' });
|
|
assert.equal(waiting.body.ok, true);
|
|
assert.equal(waiting.body.state, 'waiting');
|
|
assert.equal(waiting.body.awaitingConfirmation, i > 0, 'status 101 is preserved for the longer QR refresh window');
|
|
assert.equal(waiting.body.scanUrl, start.body.scanUrl, 'the QR URL survives every pending poll');
|
|
const scanning = await get('/skland/status?player=p1');
|
|
assert.equal(scanning.body.status, 'scanning');
|
|
assert.equal(scanning.body.bound, false);
|
|
assert.equal(up.byKey('scanToken').length, 0, 'no token exchange before confirmation');
|
|
}
|
|
|
|
const done = await post('/skland/scan/poll', { player: 'p1' });
|
|
assert.equal(done.body.state, 'confirmed');
|
|
assert.equal(done.body.status, 'bound');
|
|
|
|
const status = await get('/skland/status?player=p1');
|
|
assert.equal(status.body.bound, true);
|
|
assert.equal(status.body.nickname, '博士');
|
|
assert.deepEqual(status.body.roles, [
|
|
{ uid: '123', nickName: '博士', channelName: '官服', isDefault: false, gameId: 1 },
|
|
{ uid: '456', nickName: 'B服博士', channelName: 'B服', isDefault: false, gameId: 2 },
|
|
]);
|
|
assert.equal(up.byKey('scanToken')[0].body.scanCode, 'SC1');
|
|
assert.equal(up.byKey('scanToken').length, 1);
|
|
assert.equal(up.byKey('grant').length, 1);
|
|
assert.equal(up.byKey('accountBinding').length, 0, 'QR login keeps the Skland binding provider');
|
|
assert.equal(up.byKey('genScan').length, 1, 'no replacement QR is needed after status 101');
|
|
});
|
|
});
|
|
|
|
test('扫码: a poll without a session answers state none', async () => {
|
|
await withServer({}, async ({ post }) => {
|
|
const r = await post('/skland/scan/poll', { player: 'never-started' });
|
|
assert.equal(r.body.ok, true);
|
|
assert.equal(r.body.state, 'none');
|
|
});
|
|
});
|
|
|
|
test('扫码: a failing poll still names a state, so the client never reads `undefined`', async () => {
|
|
// The panel showed "未知状态 · Unknown state: undefined" when the upstream died mid-scan: the answer was a bare
|
|
// { ok: false } with no `state`, and a caller that only switches on `state` fell through to its unknown branch.
|
|
await withServer({ scanStatus: () => { throw new Error('upstream down'); } }, async ({ post }) => {
|
|
await post('/skland/scan/start', { player: 'p1' });
|
|
const { body } = await post('/skland/scan/poll', { player: 'p1' });
|
|
assert.equal(body.ok, false);
|
|
assert.equal(body.state, 'error', 'every poll answer carries a terminal state');
|
|
assert.match(body.error, /upstream down/);
|
|
});
|
|
});
|
|
|
|
test('验证码: send → login binds the player', async () => {
|
|
await withServer({}, async ({ post, get, up }) => {
|
|
const sent = await post('/skland/phone/send', { player: 'p1', phone: '13800000000' });
|
|
assert.equal(sent.body.ok, true);
|
|
assert.deepEqual(up.byKey('sendCode')[0].body, { phone: '13800000000', type: 2 });
|
|
|
|
const login = await post('/skland/phone/login', { player: 'p1', phone: '13800000000', code: '123456' });
|
|
assert.equal(login.body.ok, true);
|
|
assert.equal(login.body.nickname, '博士');
|
|
assert.equal(login.body.roles.length, 1);
|
|
|
|
const status = await get('/skland/status?player=p1');
|
|
assert.equal(status.body.bound, true);
|
|
assert.equal(status.body.hgId, 'HG1', 'the hgId from the login is kept');
|
|
assert.equal(up.byKey('grant').length, 1, 'exactly one grant per login');
|
|
assert.equal(up.byKey('grant')[0].body.deviceToken, 'DEV1');
|
|
});
|
|
});
|
|
|
|
test('验证码: 需要人机验证 is passed through with the geetest challenge', async () => {
|
|
await withServer({
|
|
sendCode: () => ({ status: 200, json: async () => ({ status: 1, msg: '需要人机验证', data: { captcha: { gt: 'GT', challenge: 'CH', new_captcha: true } } }) }),
|
|
}, async ({ post }) => {
|
|
const r = await post('/skland/phone/send', { player: 'p1', phone: '13800000000' });
|
|
assert.equal(r.body.ok, false);
|
|
assert.equal(r.body.needCaptcha, true);
|
|
assert.deepEqual(r.body.captcha, { gt: 'GT', challenge: 'CH', new_captcha: true });
|
|
});
|
|
});
|
|
|
|
test('验证码: a solved captcha travels back to the upstream', async () => {
|
|
await withServer({}, async ({ post, up }) => {
|
|
const solved = { geetest_challenge: 'a', geetest_validate: 'b', geetest_seccode: 'c|d' };
|
|
await post('/skland/phone/send', { player: 'p1', phone: '13800000000', captcha: solved });
|
|
assert.deepEqual(up.byKey('sendCode')[0].body, { phone: '13800000000', type: 2, captcha: solved });
|
|
});
|
|
});
|
|
|
|
test('密码: a fresh player can bind without a previous 验证码/扫码 login', async () => {
|
|
await withServer({}, async ({ post, get, up }) => {
|
|
const r = await post('/skland/password/login', { player: 'p1', phone: '13800000000', password: 'pw' });
|
|
assert.equal(r.body.ok, true);
|
|
assert.equal(r.body.nickname, '博士');
|
|
assert.equal((await get('/skland/status?player=p1')).body.bound, true);
|
|
assert.equal(up.byKey('phoneToken').length, 0);
|
|
assert.equal(up.byKey('scanToken').length, 0);
|
|
assert.deepEqual(up.byKey('grant')[0].body, { token: 'T-PW', appCode: 'be36d44aa36bfb5b', type: 1 });
|
|
assert.equal(r.body.roles[0].isDefault, true, 'official isDefault is preserved');
|
|
assert.equal(up.byKey('cred').length, 0, 'no Skland credentials are acquired');
|
|
assert.equal(up.byKey('binding').length, 0);
|
|
});
|
|
});
|
|
|
|
test('密码: upstream device verification (109) is explained after attempting login', async () => {
|
|
await withServer({
|
|
grant: () => ({ status: 200, json: async () => ({ status: 109, msg: '需要设备验证' }) }),
|
|
}, async ({ post, up, srv }) => {
|
|
const r = await post('/skland/password/login', { player: 'p1', phone: '13800000000', password: 'pw' });
|
|
assert.equal(r.body.ok, false);
|
|
assert.equal(r.body.needDevice, true);
|
|
assert.match(r.body.error, /验证码|扫码/);
|
|
assert.equal(up.byKey('passwordToken').length, 1);
|
|
assert.equal(up.byKey('grant').length, 1);
|
|
assert.equal(up.byKey('cred').length, 0);
|
|
assert.equal(srv.skland._store.get('p1').access_token, null, 'failed login credentials are not persisted');
|
|
});
|
|
});
|
|
|
|
test('密码: an incorrect password stays a password error without requiring a prior login', async () => {
|
|
await withServer({
|
|
passwordToken: () => ({ status: 200, json: async () => ({ status: 100, msg: '手机号或密码错误' }) }),
|
|
}, async ({ post, up }) => {
|
|
const r = await post('/skland/password/login', { player: 'p1', phone: '13800000000', password: 'wrong' });
|
|
assert.equal(r.body.ok, false);
|
|
assert.equal(r.body.needDevice, undefined);
|
|
assert.equal(r.body.error, '手机号或密码错误');
|
|
assert.equal(up.byKey('passwordToken').length, 1);
|
|
assert.equal(up.byKey('grant').length, 0);
|
|
});
|
|
});
|
|
|
|
test('密码: interactive captcha proofs resume the password and account-grant steps separately', async () => {
|
|
const challenge = { geetestId: 'TEST-ID', challenge: 'TEST-CHALLENGE', riskType: 'slide' };
|
|
const passwordProof = { captcha_output: 'TEST-PASSWORD-PROOF', challenge: 'TEST-CHALLENGE' };
|
|
const grantProof = { captcha_output: 'TEST-GRANT-PROOF', challenge: 'TEST-CHALLENGE' };
|
|
const answer = (data) => ({ status: 200, json: async () => data });
|
|
await withServer({
|
|
passwordToken: ({ body }) => answer(body.captcha
|
|
? { status: 0, data: { token: 'T-AFTER-CAPTCHA' } } : { status: 1, data: { captcha: challenge } }),
|
|
grant: ({ body }) => answer(body.captcha
|
|
? { status: 0, data: { token: 'BINDINGTOKEN' } } : { status: 1, data: { captcha: challenge } }),
|
|
}, async ({ post, up, srv }) => {
|
|
const login = { player: 'p1', phone: '13800000000', password: 'pw' };
|
|
const first = await post('/skland/password/login', login);
|
|
assert.equal(first.body.needCaptcha, true);
|
|
assert.equal(first.body.captchaStep, 'password');
|
|
assert.deepEqual(first.body.captcha, challenge);
|
|
assert.equal(up.byKey('grant').length, 0);
|
|
const second = await post('/skland/password/login', { ...login, captcha: passwordProof, captchaStep: 'password' });
|
|
assert.equal(second.body.needCaptcha, true);
|
|
assert.equal(second.body.captchaStep, 'accountGrant');
|
|
assert.deepEqual(up.byKey('passwordToken')[1].body.captcha, passwordProof);
|
|
assert.equal(srv.skland._store.get('p1'), null, 'pending credentials are not persisted');
|
|
const third = await post('/skland/password/login', { ...login, captcha: grantProof, captchaStep: 'accountGrant' });
|
|
assert.equal(third.body.ok, true);
|
|
assert.equal(third.body.roles.length, 1);
|
|
assert.equal(up.byKey('passwordToken').length, 2, 'the successful password exchange is not repeated');
|
|
assert.deepEqual(up.byKey('grant').map((c) => c.body.token), ['T-AFTER-CAPTCHA', 'T-AFTER-CAPTCHA']);
|
|
assert.deepEqual(up.byKey('grant')[1].body.captcha, grantProof);
|
|
});
|
|
});
|
|
|
|
test('密码: unbind discards a pending grant captcha and cannot resurrect its login', async () => {
|
|
await withServer({
|
|
grant: () => ({ status: 200, json: async () => ({ status: 1, data: { captcha: { geetestId: 'TEST-ID' } } }) }),
|
|
}, async ({ post, up }) => {
|
|
const login = { player: 'p1', phone: '13800000000', password: 'pw' };
|
|
assert.equal((await post('/skland/password/login', login)).body.captchaStep, 'accountGrant');
|
|
await post('/skland/unbind', { player: 'p1' });
|
|
const retry = await post('/skland/password/login', { ...login, captcha: { captcha_output: 'TEST' }, captchaStep: 'accountGrant' });
|
|
assert.equal(retry.body.ok, false);
|
|
assert.match(retry.body.error, /过期/);
|
|
assert.equal(up.byKey('grant').length, 1);
|
|
assert.equal(up.byKey('accountBinding').length, 0);
|
|
});
|
|
});
|
|
|
|
test('密码: a previous Skland binding switches to official account lookup without reusing its deviceToken', async () => {
|
|
await withServer({}, async ({ post, up, srv }) => {
|
|
await post('/skland/phone/login', { player: 'p1', phone: '13800000000', code: '123456' });
|
|
const r = await post('/skland/password/login', { player: 'p1', phone: '13800000000', password: 'pw' });
|
|
assert.equal(r.body.ok, true);
|
|
assert.equal(r.body.nickname, '博士');
|
|
assert.deepEqual(up.byKey('passwordToken')[0].body, { phone: '13800000000', password: 'pw' },
|
|
'the password endpoint takes phone + password only');
|
|
const grant = up.byKey('grant').at(-1);
|
|
assert.equal(grant.body.deviceToken, undefined, 'official account lookup does not need the Skland deviceToken');
|
|
assert.equal(grant.body.appCode, 'be36d44aa36bfb5b');
|
|
assert.equal(grant.body.type, 1);
|
|
assert.equal(grant.body.token, 'T-PW');
|
|
const row = srv.skland._store.get('p1');
|
|
assert.equal(row.binding_source, 'hypergryph');
|
|
assert.equal(row.cred, null);
|
|
assert.equal(row.cred_token, null);
|
|
assert.equal(row.device_token, null);
|
|
});
|
|
});
|
|
|
|
test('密码: role refresh uses official account bindings and rebind via SMS restores the Skland source', async () => {
|
|
await withServer({}, async ({ post, get, up, srv }) => {
|
|
await post('/skland/password/login', { player: 'p1', phone: '13800000000', password: 'pw' });
|
|
assert.equal((await get('/skland/roles?player=p1')).body.roles.length, 1);
|
|
assert.equal(up.byKey('accountBinding').length, 1, 'cached lookup does not call upstream');
|
|
const refreshed = await get('/skland/roles?player=p1&refresh=1');
|
|
assert.equal(refreshed.body.ok, true);
|
|
assert.equal(refreshed.body.roles[0].isDefault, true);
|
|
assert.equal(up.byKey('accountBinding').length, 2);
|
|
assert.equal(up.byKey('cred').length, 0);
|
|
await post('/skland/phone/login', { player: 'p1', phone: '13800000000', code: '123456' });
|
|
assert.equal(srv.skland._store.get('p1').binding_source, 'skland');
|
|
await get('/skland/roles?player=p1&refresh=1');
|
|
assert.equal(up.byKey('binding').length, 2);
|
|
assert.equal(up.byKey('accountBinding').length, 2);
|
|
});
|
|
});
|
|
|
|
test('解绑 clears the credentials and the status reads unbound again', async () => {
|
|
await withServer({}, async ({ post, get }) => {
|
|
await post('/skland/phone/login', { player: 'p1', phone: '138', code: '1' });
|
|
assert.equal((await get('/skland/status?player=p1')).body.bound, true);
|
|
assert.equal((await post('/skland/unbind', { player: 'p1' })).body.ok, true);
|
|
const after = await get('/skland/status?player=p1');
|
|
assert.equal(after.body.bound, false);
|
|
assert.equal(after.body.status, 'unbound');
|
|
assert.deepEqual(after.body.roles, []);
|
|
});
|
|
});
|
|
|
|
test('players are isolated: one binding never leaks into another', async () => {
|
|
await withServer({}, async ({ post, get }) => {
|
|
await post('/skland/phone/login', { player: 'p1', phone: '138', code: '1' });
|
|
assert.equal((await get('/skland/status?player=p1')).body.bound, true);
|
|
assert.equal((await get('/skland/status?player=p2')).body.bound, false, 'p2 stays unbound');
|
|
});
|
|
});
|
|
|
|
test('GET /skland/roles returns the cached binding, and ?refresh=1 re-queries upstream', async () => {
|
|
await withServer({}, async ({ post, get, up }) => {
|
|
await post('/skland/phone/login', { player: 'p1', phone: '138', code: '1' });
|
|
const cached = await get('/skland/roles?player=p1');
|
|
assert.equal(cached.body.ok, true);
|
|
assert.equal(cached.body.roles.length, 1);
|
|
assert.equal(up.byKey('binding').length, 1, 'the cached read does not hit the upstream');
|
|
|
|
const refreshed = await get('/skland/roles?player=p1&refresh=1');
|
|
assert.equal(refreshed.body.ok, true);
|
|
assert.equal(up.byKey('binding').length, 2, 'refresh re-queries');
|
|
});
|
|
});
|
|
|
|
test('a failed 验证码 login records the error on the status', async () => {
|
|
await withServer({
|
|
phoneToken: () => ({ status: 200, json: async () => ({ status: 1, msg: '验证码错误' }) }),
|
|
}, async ({ post, get }) => {
|
|
const r = await post('/skland/phone/login', { player: 'p1', phone: '138', code: '000000' });
|
|
assert.equal(r.body.ok, false);
|
|
assert.equal(r.body.error, '验证码错误');
|
|
const status = await get('/skland/status?player=p1');
|
|
assert.equal(status.body.status, 'error');
|
|
assert.equal(status.body.error, '验证码错误');
|
|
assert.equal(status.body.bound, false);
|
|
});
|
|
});
|
|
|
|
test('an unknown /skland endpoint is a JSON 404 and a bad method a JSON 405', async () => {
|
|
await withServer({}, async ({ post, srv }) => {
|
|
const unknown = await post('/skland/nope', { player: 'p1' });
|
|
assert.equal(unknown.status, 404);
|
|
assert.equal(unknown.body.ok, false);
|
|
|
|
const del = await fetch(srv.url + '/skland/status?player=p1', { method: 'DELETE', headers: { 'x-sp-diagnostic-key': 'test-admin-only' } });
|
|
assert.equal(del.status, 405);
|
|
const body = await del.json();
|
|
assert.equal(body.ok, false, 'the method error is JSON too');
|
|
});
|
|
});
|
|
|
|
test('a malformed JSON body is a 400, not a crash', async () => {
|
|
await withServer({}, async ({ srv }) => {
|
|
const res = await fetch(srv.url + '/skland/phone/login', {
|
|
method: 'POST', headers: { 'content-type': 'application/json', 'x-sp-diagnostic-key': 'test-admin-only' }, body: '{not json',
|
|
});
|
|
assert.equal(res.status, 400);
|
|
assert.equal((await res.json()).ok, false);
|
|
});
|
|
});
|
|
|
|
test('the rest of the server is unaffected: /healthz and / still answer', async () => {
|
|
await withServer({}, async ({ srv }) => {
|
|
const health = await fetch(srv.url + '/healthz');
|
|
assert.equal(health.status, 200);
|
|
assert.equal((await health.json()).ok, true);
|
|
const home = await fetch(srv.url + '/');
|
|
assert.ok([200, 404].includes(home.status), 'the static mount still owns /');
|
|
});
|
|
});
|