177 lines
6.8 KiB
TypeScript
177 lines
6.8 KiB
TypeScript
import { remoteFixture } from '../fixtures/remote-config.ts';
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
assertLocalConnectionTarget,
|
|
assertLocalStartupContext,
|
|
resolveLocalStartup,
|
|
type LocalStartupContext,
|
|
} from '../../YouleNexus/assets/framework/config/local-startup.ts';
|
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
|
import type { ResolveRuntimeConfigOptions, RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts';
|
|
import type { ConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
|
import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts';
|
|
|
|
function options(search = ''): ResolveRuntimeConfigOptions & {
|
|
fetcher: ConfigFetcher & { calls: number };
|
|
} {
|
|
const fetcher = {
|
|
calls: 0,
|
|
async fetch() {
|
|
this.calls += 1;
|
|
return { player_server_tcp: 'remote.example:3088' };
|
|
},
|
|
};
|
|
return { hostKind: 'h5', win: {}, search, fetcher };
|
|
}
|
|
|
|
test('local debug fetches remote config before choosing authoritative local servers', async () => {
|
|
const input = options();
|
|
const context = await resolveLocalStartup(input);
|
|
|
|
assert.equal(context.hostKind, 'h5');
|
|
assert.equal(context.profile, 'local');
|
|
assert.equal(context.config.identity.gameid, BUILD_IDENTITY.gameid);
|
|
assert.deepEqual(context.config.servers, STARTUP_CONFIG.localServers);
|
|
assert.equal(input.fetcher.calls, 1);
|
|
assert.equal(context.config.source, 'remote');
|
|
assert.equal(Object.isFrozen(context), true);
|
|
});
|
|
|
|
test('rejects non-local startup modes and hosts before fetching', async () => {
|
|
const cases: Array<[Partial<ResolveRuntimeConfigOptions>, RegExp]> = [
|
|
[{ startupConfig: {...STARTUP_CONFIG, mode:'release'} }, /mode.*debug/i],
|
|
[{ startupConfig: {...STARTUP_CONFIG, useLocalServer:false} }, /useLocalServer/i],
|
|
[{ hostKind: 'native-settings' }, /hostKind.*h5/i],
|
|
[{ hostKind: 'uAgent_3' }, /hostKind.*h5/i],
|
|
];
|
|
for (const [override, expected] of cases) {
|
|
const base = options();
|
|
const input = { ...base, ...override };
|
|
await assert.rejects(resolveLocalStartup(input), expected);
|
|
assert.equal(base.fetcher.calls, 0);
|
|
}
|
|
});
|
|
|
|
test('ignores conflicting and duplicate mode/profile queries', async () => {
|
|
for (const search of ['', '?profile=prod', '?profile=local&profile=unknown&mode=release&mode=other']) {
|
|
const input=options(search); const result=await resolveLocalStartup(input);
|
|
assert.deepEqual(result.config.servers,STARTUP_CONFIG.localServers); assert.equal(input.fetcher.calls,1);
|
|
}
|
|
});
|
|
|
|
test('rejects gameconfig and ambiguous identity controls before fetching', async () => {
|
|
const cases: Array<[string, RegExp]> = [
|
|
['?profile=local&gameconfig=remote-host', /gameconfig/i],
|
|
['?profile=local&agentid=A&agentid=B', /agentid/i],
|
|
['?profile=local&version=1&version=2', /version/i],
|
|
['?profile=local&gameid=other', /gameid/i],
|
|
];
|
|
for (const [search, expected] of cases) {
|
|
const input = options(search);
|
|
await assert.rejects(resolveLocalStartup(input), expected);
|
|
assert.equal(input.fetcher.calls, 0);
|
|
}
|
|
});
|
|
|
|
test('permits query identity overrides except gameid and validates numeric version', async () => {
|
|
const context = await resolveLocalStartup(options(
|
|
'?profile=local&agentid=A&channelid=C&marketid=9&version=12',
|
|
));
|
|
assert.deepEqual(context.config.identity, {
|
|
...BUILD_IDENTITY,
|
|
agentid: 'A',
|
|
channelid: 'C',
|
|
marketid: '9',
|
|
version: 12,
|
|
});
|
|
|
|
const invalid = options('?profile=local&version=not-a-number');
|
|
await assert.rejects(resolveLocalStartup(invalid), /identity.*version.*finite/i);
|
|
assert.equal(invalid.fetcher.calls, 0);
|
|
});
|
|
|
|
function validContext(): LocalStartupContext {
|
|
return {
|
|
hostKind: 'h5',
|
|
profile: 'local',
|
|
config: {
|
|
startupConfig: STARTUP_CONFIG,
|
|
mode: 'debug',
|
|
isDebugger: true,
|
|
identity: BUILD_IDENTITY,
|
|
servers: STARTUP_CONFIG.localServers,
|
|
source: 'remote',
|
|
gameserver: STARTUP_CONFIG.gameserver,
|
|
...remoteFixture(BUILD_IDENTITY),
|
|
},
|
|
};
|
|
}
|
|
|
|
test('context validator rejects forged config fields and malformed identity', () => {
|
|
const base = validContext();
|
|
const configCases: Array<[keyof RuntimeConfig, unknown]> = [
|
|
['mode', 'release'],
|
|
['isDebugger', false],
|
|
['source', 'direct'],
|
|
['gameserver', 'https://example.com/config.txt'],
|
|
['rawConfig', {}],
|
|
['servers', ['ws://127.0.0.1:9999']],
|
|
['servers', ['https://127.0.0.1:3088']],
|
|
['servers', ['ws://user:pass@127.0.0.1:3088']],
|
|
];
|
|
for (const [field, value] of configCases) {
|
|
assert.throws(
|
|
() => assertLocalStartupContext({ ...base, config: { ...base.config, [field]: value } } as LocalStartupContext),
|
|
new RegExp(field, 'i'),
|
|
);
|
|
}
|
|
assert.throws(
|
|
() => assertLocalStartupContext({
|
|
...base,
|
|
config: { ...base.config, identity: { ...BUILD_IDENTITY, version: Number.NaN } },
|
|
}),
|
|
/identity.*version.*finite/i,
|
|
);
|
|
});
|
|
|
|
test('context validator names identity when a forged context omits the identity object', () => {
|
|
const base = validContext();
|
|
for (const identity of [null, undefined]) {
|
|
assert.throws(
|
|
() => assertLocalStartupContext({
|
|
...base,
|
|
config: { ...base.config, identity },
|
|
} as unknown as LocalStartupContext),
|
|
(error: unknown) => {
|
|
assert.ok(error instanceof Error);
|
|
assert.match(error.message, /identity/i);
|
|
assert.ok((error as Error & { cause?: unknown }).cause instanceof Error);
|
|
return true;
|
|
},
|
|
);
|
|
}
|
|
});
|
|
|
|
test('connection target must be a valid URL from the authoritative local profile', () => {
|
|
const context = validContext();
|
|
assert.doesNotThrow(() => assertLocalConnectionTarget(context, 'ws://127.0.0.1:3088'));
|
|
for (const [url, expected] of [
|
|
['ws://127.0.0.1:9999', /url|target|servers/i],
|
|
['ws://example.com:3088', /url|target|servers|hostname/i],
|
|
['not-a-url', /url/i],
|
|
] as const) {
|
|
assert.throws(() => assertLocalConnectionTarget(context, url), expected);
|
|
}
|
|
});
|
|
|
|
test('local consumers validate the injected source snapshot and its exact targets', async () => {
|
|
const startupConfig={...STARTUP_CONFIG,gameserver:'https://fixture.example/config.txt',localServers:['ws://[::1]:4300']};
|
|
const context=await resolveLocalStartup({...options(),startupConfig});
|
|
assertLocalConnectionTarget(context,'ws://[::1]:4300');
|
|
assert.throws(()=>assertLocalConnectionTarget(context,STARTUP_CONFIG.localServers[0]),/target/);
|
|
for (const patch of [{mode:'release'}, {useLocalServer:false}, {localServers:['ws://elsewhere:3088']}, {gameserver:'https://other.example/config.txt'}]) {
|
|
assert.throws(()=>assertLocalStartupContext({...context,config:{...context.config,startupConfig:{...context.config.startupConfig,...patch}}} as any),/mode|useLocalServer|localServers|gameserver/);
|
|
}
|
|
});
|