Files
youle_cocos/cocoscreator_projects/framework-tests/protocol/contracts.test.ts
T

333 lines
12 KiB
TypeScript

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,
parseSelfJoinRoomResponse,
} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts';
type JsonRecord = Record<string, unknown>;
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('房间请求 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('切服与踢出 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/);
});