Files
youle_cocos/cocoscreator_projects/framework-tests/integration/login-flow.test.ts
T

183 lines
7.6 KiB
TypeScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { test } from 'node:test';
import type { RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts';
import type { Clock } from '../../YouleNexus/assets/framework/net/heartbeat.ts';
import { WireClient } from '../../YouleNexus/assets/framework/net/wire-client.ts';
import { PlatformRuntime } from '../../YouleNexus/assets/framework/platform/runtime.ts';
import { Router } from '../../YouleNexus/assets/framework/protocol/router.ts';
import type { InboundMessage } from '../../YouleNexus/assets/framework/core/types/envelope.ts';
import type { GameHost } from '../../YouleNexus/assets/framework/sdk/index.ts';
import { FakeTransport } from '../helpers/fake-transport.ts';
const CONFIG: RuntimeConfig = {
mode: 'debug', isDebugger: true,
identity: { agentid: 'A', channelid: 'C', gameid: '41', version: 1, marketid: 4 },
source: 'direct', servers: ['ws://agent'], gameserver: null, rawConfig: null,
};
const ACCOUNT = { openid: 'o', nickname: 'n', avatar: 'a', sex: 0, province: '', city: '', unionid: 'u' };
const LOGIN = {
app: 'youle', route: 'agent', rpc: 'player_login', data: {
agentid: 'A', gameid: '41', openid: 'o', nickname: 'n', avatar: 'a', sex: 0,
province: '', city: '', unionid: 'u', version: 1, channelid: 'C', marketid: 4,
location: null, machineid: 'machine', machineroom: 'install',
},
};
function fixture(name: string): InboundMessage & { data: Record<string, any> } {
return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}.json`, import.meta.url), 'utf8'));
}
function setup() {
const timers = new Map<object, { fn: () => void; ms: number }>();
const clock: Clock = {
setTimeout(fn, ms) { const handle = {}; timers.set(handle, { fn, ms }); return handle; },
clearTimeout(handle) { timers.delete(handle as object); },
};
const transports: FakeTransport[] = [];
const hosts: GameHost[] = [];
const restores: unknown[] = [];
const scenes: string[] = [];
const fatals: Error[] = [];
const gameMessages: unknown[] = [];
const runtime = new PlatformRuntime({
gameEntry: {
key: 'fixture', gameId: '41', route: 'fixture-game', resolveSeatCount: () => 4,
createModule: () => ({
attach(host) { hosts.push(host); },
handlePlatformEvent() {},
handleGameMessage(message) { gameMessages.push(message); },
restore(deskinfo) { restores.push(deskinfo); },
dispose() {},
}),
},
resolveRuntimeConfig: async () => CONFIG,
createWireClient(config) {
return new WireClient({ servers: config.servers, clock, transportFactory() {
const transport = new FakeTransport();
transports.push(transport);
return transport;
} });
},
scene: {
showLoading() { scenes.push('loading'); }, showLogin() { scenes.push('login'); },
showLobby() { scenes.push('lobby'); }, showRoom() { scenes.push('room'); },
showReconnect() { scenes.push('reconnect'); }, showKicked() { scenes.push('kicked'); },
showServerDenial() { assert.fail('Unexpected server denial'); },
showFatal(error) { fatals.push(error); },
},
loadResources: async () => {}, waitForMinimumDisplay: async () => {}, clock,
getLoginDeviceSnapshot: () => ({
location: null, machineid: 'machine', machineroom: 'install',
deviceLogin: { enabled: false }, loginPlayerId: { enabled: false },
}),
});
return {
runtime, transports, hosts, restores, scenes, fatals, gameMessages, timers,
fire(ms: number) {
const entry = [...timers].find(([, timer]) => timer.ms === ms);
assert.ok(entry, `missing ${ms}ms timer`);
timers.delete(entry[0]);
entry[1].fn();
},
};
}
test('modern login gates business packets and retries exact login after the four-second guard', async (t) => {
const s = setup();
t.after(() => s.runtime.stop());
const routed: InboundMessage[] = [];
const dispatch = Router.prototype.dispatch;
t.mock.method(Router.prototype, 'dispatch', function (this: Router, message: InboundMessage) {
routed.push(message);
return dispatch.call(this, message);
});
await s.runtime.start();
const first = s.transports[0]!;
await first.flush();
assert.deepEqual(first.sent, []);
s.runtime.login(ACCOUNT);
assert.deepEqual(first.sent.map(frame => JSON.parse(frame)), [LOGIN]);
first.serverPush(JSON.stringify(fixture('self-join-room')));
await first.flush();
assert.equal(routed.length, 1);
assert.equal(s.runtime.state.room.kind, 'outside');
assert.equal(s.hosts.length, 0);
s.fire(4000);
assert.equal(first.closeCalls, 1);
assert.equal(s.transports.length, 1);
s.fire(10000);
const second = s.transports[1]!;
await second.flush();
assert.deepEqual(second.sent.map(frame => JSON.parse(frame)), [LOGIN]);
// The obsolete connection cannot reach Router or consume the new login gate.
first.serverPush(JSON.stringify(fixture('player-login-room')));
await first.flush();
assert.equal(routed.length, 1);
second.serverPush(JSON.stringify(fixture('player-login-success')));
await second.flush();
assert.equal(routed.length, 2);
assert.equal(s.scenes[s.scenes.length - 1], 'lobby');
assert.equal(s.runtime.state.app.phase, 'logged-in');
assert.equal([...s.timers.values()].some(timer => timer.ms === 4000), false);
second.serverPush(JSON.stringify({ com: '@serverheartbeat' }));
await second.flush();
assert.equal(routed.length, 2);
assert.deepEqual(s.fatals, []);
assert.deepEqual(s.transports.flatMap(transport => transport.sent.map(frame => JSON.parse(frame))), [LOGIN, LOGIN]);
});
for (const deskinfo of [undefined, null, false, 0, '']) {
test(`modern room login does not restore falsey deskinfo: ${String(deskinfo)}`, async (t) => {
const s = setup();
t.after(() => s.runtime.stop());
await s.runtime.start();
const transport = s.transports[0]!;
await transport.flush();
s.runtime.login(ACCOUNT);
const login = fixture('player-login-room');
if (deskinfo === undefined) delete login.data.deskinfo;
else login.data.deskinfo = deskinfo;
transport.serverPush(JSON.stringify(login));
await transport.flush();
assert.equal(s.hosts.length, 1);
assert.equal(s.scenes[s.scenes.length - 1], 'room');
assert.deepEqual(s.restores, []);
assert.deepEqual(s.fatals, []);
assert.deepEqual(transport.sent.map(frame => JSON.parse(frame)), [LOGIN]);
});
}
test('another game route throws in the sole Router dispatch and becomes one terminal runtime error', async (t) => {
const s = setup();
t.after(() => s.runtime.stop());
const thrown: unknown[] = [];
const routed: InboundMessage[] = [];
const dispatch = Router.prototype.dispatch;
t.mock.method(Router.prototype, 'dispatch', function (this: Router, message: InboundMessage) {
routed.push(message);
try { return dispatch.call(this, message); }
catch (error) { thrown.push(error); throw error; }
});
await s.runtime.start();
const transport = s.transports[0]!;
await transport.flush();
s.runtime.login(ACCOUNT);
transport.serverPush(JSON.stringify(fixture('player-login-room')));
await transport.flush();
transport.serverPush(JSON.stringify({ route: 'another-game', rpc: 'deal', data: { cards: [1] } }));
await transport.flush();
assert.equal(routed.length, 2);
assert.equal(thrown.length, 1);
assert.match(String(thrown[0]), /route.*mismatch|another-game/i);
assert.equal(s.fatals.length, 1);
assert.equal(s.fatals[0], thrown[0]);
assert.deepEqual(s.gameMessages, []);
assert.equal(transport.closeCalls, 1);
assert.equal(s.timers.size, 0);
assert.deepEqual(transport.sent.map(frame => JSON.parse(frame)), [LOGIN]);
});