import { readFileSync } from 'node:fs'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { buildExitRoomRequest, buildJoinRoomRequest, buildLoginRequest, buildPrepareRequest, parseConnectAgentserverPayload, parseConnectRoomserverPayload, parseKickServerPayload, parseLoginResponse, parseOtherExitRoomPayload, parseOtherJoinRoomPayload, parseOtherOfflinePayload, parseOtherOnlinePayload, parsePlayerPreparePayload, parseSelfExitRoomPayload, parseSelfJoinRoomResponse, } from '../../YouleNexus/assets/framework/protocol/contracts/index.ts'; type JsonRecord = Record; function owns(record: object, key: string): boolean { return Object.prototype.hasOwnProperty.call(record, key); } function fixture(name: string): { route: string; rpc: string; data: JsonRecord } { return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')); } const runtimeConfig = { identity: { agentid: 'veRa0qrBf', gameid: 41, version: 10000, channelid: 'FtJf073aa', marketid: 4, }, }; const account = { openid: 'o1', nickname: '测试号', avatar: 'http://a', sex: 0, province: '江西', city: '南昌', unionid: 'test_unionid', }; const device = { ip: '127.0.0.1', location: { latitude: 1, longitude: 2 }, machineid: 'machine-1', machineroom: 'room-install-1', deviceLogin: { enabled: false as const }, loginPlayerId: { enabled: false as const }, }; test('buildLoginRequest 生成逐字段精确的单层登录信封且不修改来源', () => { const before = structuredClone(account); const request = buildLoginRequest(runtimeConfig, account, device); assert.deepEqual(request, { app: 'youle', route: 'agent', rpc: 'player_login', data: { agentid: 'veRa0qrBf', gameid: 41, openid: 'o1', nickname: '测试号', avatar: 'http://a', sex: 0, province: '江西', city: '南昌', unionid: 'test_unionid', version: 10000, channelid: 'FtJf073aa', marketid: 4, ip: '127.0.0.1', location: { latitude: 1, longitude: 2 }, machineid: 'machine-1', machineroom: 'room-install-1', }, }); assert.deepEqual(account, before); assert.notEqual(request.data, account); }); test('buildLoginRequest 只在显式设备登录模式加入电话字段', () => { const request = buildLoginRequest(runtimeConfig, account, { ...device, deviceLogin: { enabled: true, telphone: '13800000000' }, }); assert.equal(request.data.telphone, '13800000000'); assert.equal(request.data.telphoneAuto, true); const disabled = buildLoginRequest(runtimeConfig, account, { ...device, deviceLogin: { enabled: false, telphone: '不得发送' } as { enabled: false }, }); assert.equal(owns(disabled.data, 'telphone'), false); assert.equal(owns(disabled.data, 'telphoneAuto'), false); }); test('buildLoginRequest 只在显式复用模式且缓存 ID 有效时加入 playerid', () => { const enabled = buildLoginRequest(runtimeConfig, account, { ...device, loginPlayerId: { enabled: true, cachedPlayerId: 430511 }, }); assert.equal(enabled.data.playerid, 430511); for (const cachedPlayerId of [-1, 0, 1.5, Number.NaN, Infinity, '430511', null, undefined]) { const type = cachedPlayerId === null ? 'null' : typeof cachedPlayerId; assert.throws( () => buildLoginRequest(runtimeConfig, account, { ...device, loginPlayerId: { enabled: true, cachedPlayerId }, }), new RegExp(`\\$\\.device\\.loginPlayerId\\.cachedPlayerId.*${type}`), ); } }); test('buildLoginRequest 缺少可选 IP 时省略 ip,但保留可为 null 的 location', () => { const { ip: _drop, ...withoutIp } = device; const request = buildLoginRequest(runtimeConfig, account, { ...withoutIp, location: null }); assert.equal(owns(request.data, 'ip'), false); assert.equal(owns(request.data, 'location'), true); assert.equal(request.data.location, null); }); test('登录与进房拒绝会在 JSON 序列化时消失的 undefined location', () => { assert.throws( () => buildLoginRequest(runtimeConfig, account, { ...device, location: undefined }), /\$\.device\.location.*undefined/, ); assert.throws( () => buildJoinRoomRequest( runtimeConfig, { playerid: 430511, roomcode: '100001' }, { location: undefined, ip: '127.0.0.1' }, ), /\$\.environment\.location.*undefined/, ); }); test('登录与进房的 location 只接受原样对象或 null', () => { assert.throws( () => buildLoginRequest(runtimeConfig, account, { ...device, location: '南昌' }), /\$\.device\.location.*string/, ); assert.throws( () => buildJoinRoomRequest( runtimeConfig, { playerid: 430511, roomcode: '100001' }, { location: 28.7, ip: '127.0.0.1' }, ), /\$\.environment\.location.*number/, ); }); test('buildLoginRequest 拒绝非数字 version,并报告精确 JSON path 与类型', () => { assert.throws( () => buildLoginRequest( { identity: { ...runtimeConfig.identity, version: '10000' } } as never, account, device, ), /\$\.runtimeConfig\.identity\.version.*string/, ); }); test('parseLoginResponse 接受已验证大厅登录金样并保留 raw', () => { const data = fixture('player-login-success.json').data; const parsed = parseLoginResponse(data); assert.equal(parsed.state, 0); assert.equal(parsed.playerid, 430511); assert.equal(parsed.room, null); assert.deepEqual(parsed.reconnect, { present: false }); assert.equal(parsed.raw, data); assert.equal(parsed.raw.agentname, '进贤'); }); test('parseLoginResponse 由 deskinfo truthiness 决定 reconnect 并保持同一引用', () => { const data = fixture('player-login-room.json').data; const parsed = parseLoginResponse(data); assert.equal(parsed.room?.roomcode, '100001'); assert.equal(parsed.room?.seat, 1); assert.equal(parsed.room?.roomtype, data.roomtype); assert.equal(parsed.room?.players, data.players); assert.equal(parsed.reconnect.present, true); assert.equal(parsed.reconnect.present && parsed.reconnect.value, data.deskinfo); const { deskinfo: _drop, ...withoutDeskinfo } = data; assert.equal(parseLoginResponse({ ...withoutDeskinfo, isbattle: 1 }).reconnect.present, false); assert.equal(parseLoginResponse({ ...withoutDeskinfo, deskinfo: null }).reconnect.present, false); }); test('parseLoginResponse 严格拒绝缺 state、成功但缺 playerid 和不完整房间快照', () => { assert.throws(() => parseLoginResponse({ playerid: 1 }), /\$\.data\.state.*undefined/); assert.throws(() => parseLoginResponse({ state: 0 }), /\$\.data\.playerid.*undefined/); const full = fixture('player-login-room.json').data; for (const field of ['roomcode', 'seat', 'roomtype', 'players'] as const) { const malformed = { ...full }; delete malformed[field]; assert.throws(() => parseLoginResponse(malformed), new RegExp(`\\$\\.data\\.${field}`)); } }); test('parseLoginResponse 拒绝负数、越界及非整数 seat', () => { const full = fixture('player-login-room.json').data; for (const seat of [-1, 4, 1.5]) { assert.throws(() => parseLoginResponse({ ...full, seat }), /\$\.data\.seat/); } }); test('parseSelfJoinRoomResponse 接受金样,不深验 roomtype/deskinfo 并保留引用', () => { const data = fixture('self-join-room.json').data; const opaqueDeskinfo = new Date(); const roomtype = [undefined, { gameDefined: true }]; const parsed = parseSelfJoinRoomResponse({ ...data, roomtype, deskinfo: opaqueDeskinfo }); assert.equal(parsed.state, 0); assert.equal(parsed.room?.roomtype, roomtype); assert.equal(parsed.reconnect.present, true); assert.equal(parsed.reconnect.present && parsed.reconnect.value, opaqueDeskinfo); }); test('parseSelfJoinRoomResponse defines the optional deskwar default once and deskinfo wins stage', () => { const cases = [ { deskwar: 'missing', deskinfo: false, expectedStage: 0 }, { deskwar: 0, deskinfo: false, expectedStage: 0 }, { deskwar: 1, deskinfo: false, expectedStage: 1 }, { deskwar: 'missing', deskinfo: true, expectedStage: 1 }, { deskwar: 0, deskinfo: true, expectedStage: 1 }, { deskwar: 1, deskinfo: true, expectedStage: 1 }, ] as const; for (const row of cases) { const data = fixture('self-join-room.json').data; if (row.deskwar === 'missing') delete data.deskwar; else data.deskwar = row.deskwar; if (row.deskinfo) data.deskinfo = { round: 7 }; else delete data.deskinfo; data.isbattle = 1; const parsed = parseSelfJoinRoomResponse(data); assert.equal(parsed.stage, row.expectedStage, JSON.stringify(row)); assert.equal(parsed.reconnect.present, row.deskinfo, JSON.stringify(row)); } for (const deskwar of [-1, 2, 1.5, '1', undefined]) { assert.throws( () => parseSelfJoinRoomResponse({ ...fixture('self-join-room.json').data, deskwar }), /\$\.data\.deskwar/, ); } }); test('房间请求 builder 使用唯一 route,并仅发协议定义字段', () => { const room = { playerid: 430511, roomcode: '100001' }; assert.deepEqual(buildPrepareRequest(runtimeConfig, room), { app: 'youle', route: 'room', rpc: 'player_prepare', data: { agentid: 'veRa0qrBf', playerid: 430511, gameid: 41, roomcode: '100001' }, }); assert.deepEqual(buildExitRoomRequest(runtimeConfig, room), { app: 'youle', route: 'room', rpc: 'self_exit_room', data: { agentid: 'veRa0qrBf', playerid: 430511, gameid: 41, roomcode: '100001' }, }); assert.deepEqual(buildJoinRoomRequest(runtimeConfig, room, { location: null, ip: '127.0.0.1', vipMatch: 1, match_id: 'match-7', }), { app: 'youle', route: 'agent', rpc: 'self_join_room', data: { agentid: 'veRa0qrBf', playerid: 430511, gameid: 41, roomcode: '100001', location: null, ip: '127.0.0.1', vipMatch: 1, match_id: 'match-7', }, }); }); test('房间请求拒绝 agentid/gameid 的非有限数值并报告来源路径', () => { for (const field of ['agentid', 'gameid'] as const) { for (const value of [Number.NaN, Infinity, -Infinity]) { assert.throws( () => buildPrepareRequest( { identity: { ...runtimeConfig.identity, [field]: value } }, { playerid: 430511, roomcode: '100001' }, ), new RegExp(`\\$\\.runtimeConfig\\.identity\\.${field}.*number`), ); } } }); test('buildJoinRoomRequest 不凭空补 vipMatch 或 match_id', () => { const request = buildJoinRoomRequest( runtimeConfig, { playerid: 430511, roomcode: '100001' }, { location: null, ip: '127.0.0.1' }, ); assert.equal(owns(request.data, 'vipMatch'), false); assert.equal(owns(request.data, 'match_id'), false); }); test('buildJoinRoomRequest 拒绝非法或 undefined vipMatch', () => { for (const vipMatch of [undefined, 0, 2, '1', Number.NaN, Infinity]) { const type = typeof vipMatch; assert.throws( () => buildJoinRoomRequest( runtimeConfig, { playerid: 430511, roomcode: '100001' }, { location: null, ip: '127.0.0.1', vipMatch } as never, ), new RegExp(`\\$\\.environment\\.vipMatch.*${type}`), ); } }); test('buildJoinRoomRequest 拒绝非法、undefined 或非有限 match_id', () => { for (const match_id of [undefined, null, false, {}, Number.NaN, Infinity]) { const type = match_id === null ? 'null' : Array.isArray(match_id) ? 'array' : typeof match_id; assert.throws( () => buildJoinRoomRequest( runtimeConfig, { playerid: 430511, roomcode: '100001' }, { location: null, ip: '127.0.0.1', match_id } as never, ), new RegExp(`\\$\\.environment\\.match_id.*${type}`), ); } assert.equal(buildJoinRoomRequest( runtimeConfig, { playerid: 430511, roomcode: '100001' }, { location: null, ip: '127.0.0.1', match_id: 7 }, ).data.match_id, 7); }); test('座位事件 parser 校验实际消费字段并保留 raw', () => { const prepare = { seat: 1, deskwar: 1, extension: 'kept' }; assert.equal(parsePlayerPreparePayload(prepare).raw, prepare); assert.equal(parseOtherJoinRoomPayload({ seat: 1, playerid: 9 }).seat, 1); assert.equal(parseOtherExitRoomPayload({ seat: 1 }).seat, 1); assert.equal(parseOtherOfflinePayload({ seat: 1 }).seat, 1); assert.deepEqual(parseOtherOnlinePayload({ seat: 1, ip: '127.0.0.2' }), { seat: 1, ip: '127.0.0.2', raw: { seat: 1, ip: '127.0.0.2' }, }); assert.throws(() => parseOtherExitRoomPayload({ seat: 1.2 }), /\$\.data\.seat.*number/); assert.throws(() => parseOtherOnlinePayload({ seat: 1 }), /\$\.data\.ip.*undefined/); }); test('self_exit_room parser accepts an omitted seat but strictly validates it when present', () => { const withoutSeat = { isowner: 1, roomcode: '100001', extension: 'kept' }; const parsedWithoutSeat = parseSelfExitRoomPayload(withoutSeat); assert.equal(parsedWithoutSeat.raw, withoutSeat); assert.equal(Object.prototype.hasOwnProperty.call(parsedWithoutSeat, 'seat'), false); const withSeat = { seat: 1, extension: 'kept' }; const parsedWithSeat = parseSelfExitRoomPayload(withSeat); assert.equal(parsedWithSeat.raw, withSeat); assert.equal(parsedWithSeat.seat, 1); for (const seat of [-1, 1.2, '1', undefined]) { assert.throws(() => parseSelfExitRoomPayload({ seat }), /\$\.data\.seat/); } assert.throws(() => parseOtherExitRoomPayload({}), /\$\.data\.seat/); }); test('切服与踢出 parser 拒绝畸形载荷并保留未知扩展', () => { const roomSwitch = { roomserver: '127.0.0.1:9001', epoch: 7 }; assert.deepEqual(parseConnectRoomserverPayload(roomSwitch), { roomserver: '127.0.0.1:9001', raw: roomSwitch, }); const agentSwitch = { agentserver: '127.0.0.1:9002', opt: 'free_room', trace: 'x' }; assert.deepEqual(parseConnectAgentserverPayload(agentSwitch), { agentserver: '127.0.0.1:9002', opt: 'free_room', raw: agentSwitch, }); assert.deepEqual(parseKickServerPayload({ msg: '检查到新版本', code: 1 }).msg, '检查到新版本'); assert.throws(() => parseConnectRoomserverPayload({ roomserver: 9001 }), /\$\.data\.roomserver.*number/); assert.throws(() => parseConnectAgentserverPayload({ agentserver: 'x' }), /\$\.data\.opt.*undefined/); assert.throws(() => parseConnectAgentserverPayload({ agentserver: null, opt: 'free_room' }), /\$\.data\.agentserver.*null/); assert.throws(() => parseKickServerPayload({ msg: false }), /\$\.data\.msg.*boolean/); });