import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createBindingApi, ensureBindingSession, preferredRole, roleKey } from '../../public/js/skland/api.js'; test('the title client puts its game token only in an authorization header and never sends a player id', async () => { const calls = []; const request = createBindingApi({ token: 'private-game-token', fetchImpl: async (url, init) => { calls.push({ url, init }); return { ok: true, status: 200, json: async () => ({ ok: true }) }; } }); await request('status'); await request('password/login', { phone: '13800000000', password: 'local-input' }); assert.equal(calls[0].url, '/skland/session/status'); assert.equal(calls[0].init.headers.authorization, 'Bearer private-game-token'); assert.equal(calls[0].init.method, 'GET'); assert.equal(calls[1].init.method, 'POST'); assert.deepEqual(JSON.parse(calls[1].init.body), { phone: '13800000000', password: 'local-input' }); assert.ok(calls.every(({ url, init }) => !url.includes('private-game-token') && !init.body?.includes('private-game-token'))); }); test('the title client distinguishes disabled module, expired session, network and malformed responses', async () => { for (const [status, code] of [[404, 'off'], [401, 'session']]) { await assert.rejects(createBindingApi({ token: 'T', fetchImpl: async () => ({ status }) })('status'), { code }); } await assert.rejects(createBindingApi()('status'), { code: 'session' }); await assert.rejects(createBindingApi({ token: 'T', fetchImpl: async () => { throw new Error('offline'); } })('status'), { code: 'network' }); await assert.rejects(createBindingApi({ token: 'T', fetchImpl: async () => ({ status: 200, ok: true, json: async () => { throw new Error(); } }) })('status'), { code: 'response' }); }); test('binding starts hello without entering the game, reuses an online identity, and cleans up on failure', async () => { const listeners = new Map(); let current = null, connects = 0, sentName; const identity = { init: async () => {}, getToken: () => current }; const net = { status: 'connected', on: (event, fn) => { listeners.set(event, fn); return () => listeners.delete(event); }, setName: (name) => { sentName = name; }, connect: () => { connects++; setTimeout(() => { current = 'T'; listeners.get('welcome')?.({ token: 'T' }); }, 0); }, }; assert.equal(await ensureBindingSession({ net, identity, name: '博士代号' }), 'T'); assert.equal(sentName, '博士代号'); assert.equal(listeners.size, 0); net.status = 'online'; assert.equal(await ensureBindingSession({ net, identity, name: '另一个名字' }), 'T'); assert.equal(connects, 1); net.status = 'connected'; net.connect = () => {}; await assert.rejects(ensureBindingSession({ net, identity, name: '博士', timeoutMs: 1 }), { code: 'network' }); assert.equal(listeners.size, 0); }); test('nickname selection uses the default role, preserves a selected channel, and supports no roles', () => { const a = { uid: '123', channelName: '官服', gameId: 1, nickName: '官服博士' }; const b = { uid: '123', channelName: 'B服', gameId: 2, nickName: 'B服博士', isDefault: true }; assert.equal(preferredRole([a, b]), b); assert.equal(preferredRole([a, b], roleKey(a)), a); assert.equal(preferredRole([a, b], 'removed-role'), b); assert.equal(preferredRole([a]), a); assert.equal(preferredRole([]), null); assert.equal(preferredRole([{ uid: 'missing-name', isDefault: true }, { ...b, nickName: ' ' }, a]), a); assert.equal(preferredRole([{ uid: 'missing-name' }]), null); assert.equal(preferredRole([{ uid: null, channelName: '官服', nickName: '缺少身份' }]), null); assert.equal(roleKey(a), roleKey({ ...a, uid: 123, gameId: null, nickName: '改名以后' }), 'nickname and optional gameId never change identity'); }); test('an expired recovery is distinguishable from a transport error and never becomes a successful entry', async () => { const request = createBindingApi({ token: 'T', fetchImpl: async () => ({ status: 409, ok: false, json: async () => ({ ok: false, code: 'resume_expired', error: 'expired' }), }) }); await assert.rejects(request('resume', { id: 'stale' }), { code: 'resume_expired' }); });