import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../../YouleNexus/assets/framework/config/sources/defaults.ts'; import { PROFILES } from '../../../YouleNexus/assets/framework/config/profiles.ts'; import type { LocalStartupContext } from '../../../YouleNexus/assets/framework/config/local-startup.ts'; import { LOCAL_MACHINE_KEY, localAccountKey, type LocalStoragePort } from '../../../YouleNexus/assets/framework/adapters/local/local-storage.ts'; import { createLocalLoginSources, type LocalHostSnapshot } from '../../../YouleNexus/assets/framework/adapters/local/login-sources.ts'; const context: LocalStartupContext = { hostKind: 'h5', profile: 'local', config: { mode: 'debug', isDebugger: true, source: 'direct', gameserver: null, rawConfig: null, servers: PROFILES.local!.servers!, identity: BUILD_IDENTITY, } }; 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 fixture(overrides: Partial[0]> = {}) { return createLocalLoginSources({ context, storage: new MapStorage(), random: () => 0.5, now: () => 123456, sampleHost: () => ({ location: null }), isSessionActive: () => false, ...overrides }); } test('one host sample supplies frozen account and device without rewriting account cache', async () => { const storage = new MapStorage(); let samples = 0; const city = { ip: '10.0.0.2', province: 'P', city: 'C' }; const location = { unknownProtocolField: [1] }; const sources = fixture({ storage, sampleHost: () => { samples++; return { location, returnCitySN: city }; } }); assert.throws(() => sources.getLoginDeviceSnapshot(), /prepare/i); const account = await sources.prepareLogin(); const device = sources.getLoginDeviceSnapshot(); assert.equal(samples, 1); assert.equal(account.province, 'P'); assert.equal(account.city, 'C'); assert.equal(device.machineid, '123456-5499'); assert.strictEqual(device.location, location); assert.equal(device.ip, '10.0.0.2'); assert.equal(device.machineroom, ''); assert.deepEqual(device.deviceLogin, { enabled: false }); assert.deepEqual(device.loginPlayerId, { enabled: false }); for (const value of [account, device, device.deviceLogin, device.loginPlayerId]) assert.ok(Object.isFrozen(value)); city.ip = 'changed'; city.province = 'changed'; city.city = 'changed'; assert.equal(device.ip, '10.0.0.2'); assert.equal(account.province, 'P'); assert.equal(account.city, 'C'); assert.equal(JSON.parse(storage.values.get(localAccountKey(context))!).account.province, 'jiangxi'); }); test('undefined city omits ip and retains generated province and city', async () => { const sources = fixture(); const account = await sources.prepareLogin(); assert.equal(account.province, 'jiangxi'); assert.equal(account.city, 'nanchang'); assert.equal(Object.prototype.hasOwnProperty.call(sources.getLoginDeviceSnapshot(), 'ip'), false); }); test('malformed host input fails and leaves no device', async () => { for (const host of [{}, { location: undefined }, { location: [] }, { location: 0 }, { location: null, returnCitySN: null }, { location: null, returnCitySN: [] }, ...['ip', 'province', 'city'].map(field => ({ location: null, returnCitySN: { ip: '', province: '', city: '', [field]: 3 } }))]) { const sources = fixture({ sampleHost: () => host as LocalHostSnapshot }); await assert.rejects(sources.prepareLogin(), /location|returnCitySN/); assert.throws(() => sources.getLoginDeviceSnapshot(), /prepare/i); } }); test('machine cache is persisted and reused across source instances without now or random', async () => { const storage = new MapStorage(); await fixture({ storage }).prepareLogin(); assert.deepEqual(JSON.parse(storage.values.get(LOCAL_MACHINE_KEY)!), { schemaVersion: 1, machineid: '123456-5499' }); const sources = fixture({ storage, now: () => { throw Error('unexpected now'); }, random: () => { throw Error('unexpected random'); } }); await sources.prepareLogin(); assert.equal(sources.getLoginDeviceSnapshot().machineid, '123456-5499'); }); test('damaged machine records fail without replacing the stored value', async () => { for (const raw of ['bad json', 'null', '[]', '{}', ...[ { schemaVersion: 2, machineid: '1-1000' }, { schemaVersion: 1, machineid: '' }, { schemaVersion: 1, machineid: 3 }, { schemaVersion: 1, machineid: 'bad' }, { schemaVersion: 1, machineid: '-1-1000' }, { schemaVersion: 1, machineid: '1-999' }, { schemaVersion: 1, machineid: '1-9999' }, { schemaVersion: 1, machineid: '9007199254740992-1000' }, { schemaVersion: 1, machineid: '1-1000', extra: true }, ].map(value => JSON.stringify(value))]) { const storage = new MapStorage(); storage.values.set(LOCAL_MACHINE_KEY, raw); const sources = fixture({ storage }); await assert.rejects(sources.prepareLogin(), /youle\.local-test\.machine\.v1/); assert.equal(storage.values.get(LOCAL_MACHINE_KEY), raw); assert.throws(() => sources.getLoginDeviceSnapshot(), /prepare/i); } }); test('machine generation validates timestamp and random boundaries', async () => { const storage = new MapStorage(); await fixture({ storage }).prepareLogin(); storage.values.delete(LOCAL_MACHINE_KEY); for (const now of [-1, 0.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) { await assert.rejects(fixture({ storage, now: () => now }).prepareLogin(), /now|timestamp/); assert.equal(storage.values.has(LOCAL_MACHINE_KEY), false); } for (const random of [-0.1, 1, NaN, Infinity]) { await assert.rejects(fixture({ storage, random: () => random }).prepareLogin(), /random/); assert.equal(storage.values.has(LOCAL_MACHINE_KEY), false); } for (const [random, expected] of [[0, '0-1000'], [0.999999, '0-9998']] as const) { storage.values.delete(LOCAL_MACHINE_KEY); const sources = fixture({ storage, random: () => random, now: () => 0 }); await sources.prepareLogin(); assert.equal(sources.getLoginDeviceSnapshot().machineid, expected); } }); test('machine storage and generation failures preserve the original cause', async () => { for (const operation of ['read', 'write', 'now', 'random'] as const) { const stored = new MapStorage(); await fixture({ storage: stored }).prepareLogin(); stored.values.delete(LOCAL_MACHINE_KEY); const cause = new Error(operation); const sources = fixture({ storage: { getItem(key) { if (key === LOCAL_MACHINE_KEY && operation === 'read') throw cause; return stored.getItem(key); }, setItem(key, value) { if (key === LOCAL_MACHINE_KEY && operation === 'write') throw cause; stored.setItem(key, value); }, }, now: () => { if (operation === 'now') throw cause; return 0; }, random: () => { if (operation === 'random') throw cause; return 0; } }); await assert.rejects(sources.prepareLogin(), (error: unknown) => { assert.ok(error instanceof Error); assert.match(error.message, /youle\.local-test\.machine\.v1/); assert.strictEqual((error as Error & { cause?: unknown }).cause, cause); return true; }); assert.throws(() => sources.getLoginDeviceSnapshot(), /prepare/i); } }); test('concurrent prepares merge and reset conflicts in both directions', async () => { let samples = 0; const sources = fixture({ sampleHost: () => { samples++; return { location: null }; } }); const first = sources.prepareLogin(); const second = sources.prepareLogin(); await assert.rejects(sources.resetAccount(), /pending|prepare/i); const [a, b] = await Promise.all([first, second]); assert.strictEqual(a, b); assert.equal(samples, 1); const reset = sources.resetAccount(); assert.throws(() => sources.getLoginDeviceSnapshot(), /prepare/i); const conflictingPrepare = sources.prepareLogin(); const conflictingReset = sources.resetAccount(); await assert.rejects(conflictingPrepare, /reset/i); await assert.rejects(conflictingReset, /pending|reset/i); await reset; }); test('reset invalidates device and changes account while preserving the machine key', async () => { const storage = new MapStorage(); let random = 0.5; const sources = fixture({ storage, random: () => random }); const old = await sources.prepareLogin(); const machine = storage.values.get(LOCAL_MACHINE_KEY); random = 0; const account = await sources.resetAccount(); assert.notEqual(account.openid, old.openid); assert.equal(storage.values.get(LOCAL_MACHINE_KEY), machine); assert.throws(() => sources.getLoginDeviceSnapshot(), /prepare/i); assert.equal((await sources.prepareLogin()).openid, account.openid); }); test('prepare starts by invalidating prior device and failure cannot expose it', async () => { let fail = false; const sources = fixture({ sampleHost: () => { if (fail) throw Error('host failed'); return { location: null }; } }); await sources.prepareLogin(); fail = true; const pending = sources.prepareLogin(); assert.throws(() => sources.getLoginDeviceSnapshot(), /prepare/i); await assert.rejects(pending, /host failed/); assert.throws(() => sources.getLoginDeviceSnapshot(), /prepare/i); }); test('active-session reset rejects and invalidates the device without overwriting account', async () => { const storage = new MapStorage(); const sources = fixture({ storage, isSessionActive: () => true }); await sources.prepareLogin(); const old = storage.values.get(localAccountKey(context)); await assert.rejects(sources.resetAccount(), /active session/i); assert.equal(storage.values.get(localAccountKey(context)), old); assert.throws(() => sources.getLoginDeviceSnapshot(), /prepare/i); });