'use strict'; const test = require('node:test'); const assert = require('node:assert'); const { flattenChunks, boundsOf, timingSafeEqual, WebConsole } = require('../src/web/server'); const { TerritoryModel } = require('../src/core/territoryModel'); const { defaultConfig } = require('../src/config/expansionConfig'); const { Logger } = require('../src/util/logger'); const chunkPos = require('../src/core/chunkPos'); test('flattenChunks 压平为 [x1,z1,x2,z2,...]', () => { const keys = new Set([chunkPos.key(1, 2), chunkPos.key(-3, 4)]); const flat = flattenChunks(keys); assert.strictEqual(flat.length, 4); const pairs = []; for (let i = 0; i < flat.length; i += 2) pairs.push([flat[i], flat[i + 1]]); pairs.sort((a, b) => a[0] - b[0]); assert.deepStrictEqual(pairs, [[-3, 4], [1, 2]]); }); test('boundsOf 计算外接矩形并对空集合返回 null', () => { const keys = new Set([chunkPos.key(1, 2), chunkPos.key(-3, 4), chunkPos.key(5, -6)]); assert.deepStrictEqual(boundsOf(keys), { minX: -3, maxX: 5, minZ: -6, maxZ: 4 }); assert.strictEqual(boundsOf(new Set()), null); }); test('timingSafeEqual 只接受等长且完全相同的字符串', () => { assert.strictEqual(timingSafeEqual('abc', 'abc'), true); assert.strictEqual(timingSafeEqual('abc', 'abd'), false); assert.strictEqual(timingSafeEqual('abc', 'abcd'), false); assert.strictEqual(timingSafeEqual(undefined, 'abc'), false); }); /** 搭建一个使用桩依赖的控制台实例 */ function setupConsole() { const map = new Map([[ 'A', { name: 'A', capital: { x: 0, z: 0 }, chunks: new Set([chunkPos.key(0, 0), chunkPos.key(1, 0)]), color: 0x2f81f7 } ]]); const model = new TerritoryModel(map); model.setSelfStateName('A'); model.addSessionClaim({ x: 1, z: 0 }); const logger = new Logger({ echo: false }); const botManager = { snapshot: () => ({ state: 'online', username: 'TestBot' }), sendCommand: () => {} }; const controller = { target: { x: 2, z: 0 }, playerChunk: { x: 0, z: 0 }, snapshot: () => ({ state: 'PATHING', running: true }), exportLogs: () => 'data/debug.log', stop: () => {}, start: async () => true, status: '测试中' }; const dataService = { getModel: () => model, snapshot: () => ({ stateCount: 1, claimedChunks: 2 }), refreshAsync: () => {}, fetchAllTerritoryOnce: async () => ({ ok: true, message: '已拉取 0 个区块' }) }; const config = defaultConfig(); const webConsole = new WebConsole({ botManager, controller, dataService, logger, getConfig: () => config, applyConfig: (next) => next }, { host: '127.0.0.1', port: 0 }); return { webConsole, model, logger, controller }; } test('mapPayload 返回己方 / 会话 / 飞地 / 目标与各邦国摘要', () => { const { webConsole } = setupConsole(); const payload = webConsole.mapPayload(false); assert.strictEqual(payload.selfState, 'A'); assert.strictEqual(payload.self.length, 4, '2 个区块 → 4 个数字'); assert.strictEqual(payload.session.length, 2); assert.deepStrictEqual(payload.target, { x: 2, z: 0 }); assert.strictEqual(payload.states.length, 1); assert.strictEqual(payload.states[0].name, 'A'); assert.strictEqual(payload.states[0].color, '#2f81f7'); assert.strictEqual(payload.states[0].count, 2); assert.strictEqual(payload.otherChunks, undefined, '默认不返回全量他国区块'); }); test('mapPayload(full) 额外返回他国区块', () => { const { webConsole, model } = setupConsole(); model.states.set('B', { name: 'B', capital: { x: 9, z: 9 }, chunks: new Set([chunkPos.key(9, 9)]), color: 0xff43de }); const payload = webConsole.mapPayload(true); assert.strictEqual(payload.full, true); assert.strictEqual(payload.otherChunks.length, 1); assert.strictEqual(payload.otherChunks[0].name, 'B'); assert.strictEqual(payload.otherChunks[0].color, '#ff43de'); }); test('statusPayload 汇总 bot / 任务 / 数据源状态', () => { const { webConsole } = setupConsole(); const payload = webConsole.statusPayload(); assert.strictEqual(payload.bot.state, 'online'); assert.strictEqual(payload.task.state, 'PATHING'); assert.strictEqual(payload.data.stateCount, 1); assert.strictEqual(payload.claimTime.start, 11); assert.strictEqual(payload.claimTime.end, 22); }); test('HTTP 接口:/api/status、/api/config、/api/map 可访问', async () => { const { webConsole } = setupConsole(); await webConsole.start(); const base = webConsole.baseUrl(); try { const status = await fetch(base + '/api/status').then((r) => r.json()); assert.strictEqual(status.ok, true); assert.strictEqual(status.data.bot.state, 'online'); const config = await fetch(base + '/api/config').then((r) => r.json()); assert.strictEqual(config.ok, true); assert.strictEqual(config.data.strategy, 1); const map = await fetch(base + '/api/map?full=1').then((r) => r.json()); assert.strictEqual(map.ok, true); assert.strictEqual(map.data.full, true); const html = await fetch(base + '/').then((r) => r.text()); assert.ok(html.includes('AutoStore Bot'), '首页应返回控制台 HTML'); } finally { await webConsole.stop(); } }); test('HTTP 接口:配置令牌后拒绝无令牌的 API 请求,但静态资源仍可加载', async () => { const { webConsole } = setupConsole(); webConsole.token = 'secret-token'; await webConsole.start(); const base = webConsole.baseUrl(); try { const unauthorized = await fetch(base + '/api/status'); assert.strictEqual(unauthorized.status, 401); const authorized = await fetch(base + '/api/status?token=secret-token'); assert.strictEqual(authorized.status, 200); const viaHeader = await fetch(base + '/api/status', { headers: { 'X-Auth-Token': 'secret-token' } }); assert.strictEqual(viaHeader.status, 200); // 静态资源不带令牌也必须能加载,否则浏览器里控制台页面会缺样式 / 缺脚本 const html = await fetch(base + '/'); assert.strictEqual(html.status, 200); const css = await fetch(base + '/style.css'); assert.strictEqual(css.status, 200); const js = await fetch(base + '/app.js'); assert.strictEqual(js.status, 200); } finally { await webConsole.stop(); } }); test('HTTP 接口:POST /api/config 会走 applyConfig 并返回生效配置', async () => { const { webConsole } = setupConsole(); await webConsole.start(); const base = webConsole.baseUrl(); try { const response = await fetch(base + '/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ strategy: 4, targetCount: 12 }) }).then((r) => r.json()); assert.strictEqual(response.ok, true); assert.strictEqual(response.data.strategy, 4); assert.strictEqual(response.data.targetCount, 12); // 未提供的字段保持默认 assert.strictEqual(response.data.retries, 2); } finally { await webConsole.stop(); } }); test('HTTP 接口:POST /api/command 缺参数时返回 400', async () => { const { webConsole } = setupConsole(); await webConsole.start(); const base = webConsole.baseUrl(); try { const response = await fetch(base + '/api/command', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); assert.strictEqual(response.status, 400); } finally { await webConsole.stop(); } });