import { test } from 'node:test'; import assert from 'node:assert/strict'; import { resolveLocalStartup } from '../../YouleNexus/assets/framework/config/local-startup.ts'; import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts'; import { localAccountKey, type LocalStoragePort } from '../../YouleNexus/assets/framework/adapters/local/local-storage.ts'; import { createLocalLoginSources } from '../../YouleNexus/assets/framework/adapters/local/login-sources.ts'; import { buildLoginRequest } from '../../YouleNexus/assets/framework/protocol/contracts/index.ts'; class MapStorage implements LocalStoragePort { readonly values = new Map(); getItem(key: string): string | null { return this.values.get(key) ?? null; } setItem(key: string, value: string): void { this.values.set(key, value); } } function resolve(search = '?profile=local') { return resolveLocalStartup({ mode: 'debug', hostKind: 'h5', win: {}, search, fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } } }); } function sourcesFor(context: Awaited>, storage: LocalStoragePort, overrides: Partial[0]> = {}) { return createLocalLoginSources({ context, storage, random: () => 0.5, now: () => 123456, sampleHost: () => ({ location: null }), isSessionActive: () => false, ...overrides }); } test('real local resolution and visitor sources build the login envelope with query identity and omitted optional fields', async () => { const context = await resolve('?profile=local&version=731&agentid=integration-agent&channelid=integration-channel&marketid=009'); const sources = sourcesFor(context, new MapStorage()); const account = await sources.prepareLogin(); const envelope = buildLoginRequest(context.config, account, sources.getLoginDeviceSnapshot()); assert.equal(envelope.app, 'youle'); assert.equal(envelope.route, 'agent'); assert.equal(envelope.rpc, 'player_login'); assert.equal(envelope.data.gameid, BUILD_IDENTITY.gameid); assert.equal(envelope.data.version, 731); assert.equal(typeof envelope.data.version, 'number'); assert.equal(envelope.data.agentid, 'integration-agent'); assert.equal(envelope.data.channelid, 'integration-channel'); assert.equal(envelope.data.marketid, '009'); assert.equal(envelope.data.openid, 'testopenid_50000'); assert.equal(envelope.data.unionid, 'ylgame50000'); assert.equal(envelope.data.province, 'jiangxi'); assert.equal(envelope.data.city, 'nanchang'); assert.equal(envelope.data.location, null); assert.equal(envelope.data.machineid, '123456-5499'); for (const field of ['playerid', 'telphone', 'telphoneAuto', 'ip']) { assert.equal(Object.prototype.hasOwnProperty.call(envelope.data, field), false, field); } }); test('one host sample overrides province and city in the real envelope without changing the visitor cache', async () => { const context = await resolve(); const storage = new MapStorage(); const location = { longitude: 115.9, latitude: 28.7 }; const city = { ip: '10.0.0.2', province: 'P', city: 'C' }; let samples = 0; const sources = sourcesFor(context, storage, { sampleHost: () => { samples++; return { location, returnCitySN: city }; }, }); const account = await sources.prepareLogin(); city.ip = 'changed'; city.province = 'changed'; city.city = 'changed'; const envelope = buildLoginRequest(context.config, account, sources.getLoginDeviceSnapshot()); assert.equal(samples, 1); assert.equal(envelope.data.province, 'P'); assert.equal(envelope.data.city, 'C'); assert.equal(envelope.data.ip, '10.0.0.2'); assert.strictEqual(envelope.data.location, location); const cached = JSON.parse(storage.getItem(localAccountKey(context))!); assert.equal(cached.account.province, 'jiangxi'); assert.equal(cached.account.city, 'nanchang'); }); test('a version upgrade reuses stored account and machine but sends the newly resolved numeric version', async () => { const storage = new MapStorage(); const firstContext = await resolve('?profile=local&version=731'); const firstSources = sourcesFor(firstContext, storage); const firstAccount = await firstSources.prepareLogin(); const first = buildLoginRequest(firstContext.config, firstAccount, firstSources.getLoginDeviceSnapshot()); const originalCache = new Map(storage.values); const nextContext = await resolve('?profile=local&version=732'); const nextSources = sourcesFor(nextContext, storage, { random: () => { throw new Error('cached login must not regenerate identity'); }, now: () => { throw new Error('cached login must not regenerate machine'); }, }); const nextAccount = await nextSources.prepareLogin(); const next = buildLoginRequest(nextContext.config, nextAccount, nextSources.getLoginDeviceSnapshot()); assert.equal(first.data.version, 731); assert.equal(next.data.version, 732); assert.deepEqual(nextAccount, firstAccount); assert.equal(next.data.openid, first.data.openid); assert.equal(next.data.machineid, first.data.machineid); assert.deepEqual(storage.values, originalCache); }); test('corrupt and unknown cached account records stop before the real builder and invalidate the prepared device', async (t) => { const context = await resolve(); const validAccount = { openid: 'cached-user', nickname: 'Visitor', avatar: '', sex: 1, province: 'P', city: 'C', unionid: 'cached-union' }; for (const [name, raw, error] of [ ['corrupt JSON', '{broken', /invalid JSON/], ['unknown schema', JSON.stringify({ schemaVersion: 2, account: validAccount }), /schemaVersion/], ['unknown account field', JSON.stringify({ schemaVersion: 1, account: { ...validAccount, headimgurl: 'legacy' } }), /headimgurl/], ['missing identity', JSON.stringify({ schemaVersion: 1, account: { ...validAccount, openid: '' } }), /openid/], ] as const) { await t.test(name, async () => { const storage = new MapStorage(); const sources = sourcesFor(context, storage); await sources.prepareLogin(); storage.setItem(localAccountKey(context), raw); let builderReached = false; await assert.rejects(async () => { const account = await sources.prepareLogin(); builderReached = true; return buildLoginRequest(context.config, account, sources.getLoginDeviceSnapshot()); }, error); assert.equal(builderReached, false); assert.throws(() => sources.getLoginDeviceSnapshot(), /prepare/i); assert.equal(storage.getItem(localAccountKey(context)), raw); }); } });