feat(config): add scoped local visitor accounts
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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 { createLocalVisitorProvider } from '../../../YouleNexus/assets/framework/adapters/local/visitor-account.ts';
|
||||
|
||||
class MapStorage implements LocalStoragePort {
|
||||
readonly values = new Map<string, string>();
|
||||
readonly writes: Array<[string, string]> = [];
|
||||
getItem(key: string): string | null { return this.values.get(key) ?? null; }
|
||||
setItem(key: string, value: string): void { this.writes.push([key, value]); this.values.set(key, value); }
|
||||
}
|
||||
|
||||
function context(overrides: Partial<typeof BUILD_IDENTITY> = {}): LocalStartupContext {
|
||||
return { hostKind: 'h5', profile: 'local', config: {
|
||||
mode: 'debug', isDebugger: true, source: 'direct', gameserver: null, rawConfig: null,
|
||||
servers: PROFILES.local!.servers!, identity: { ...BUILD_IDENTITY, ...overrides },
|
||||
} };
|
||||
}
|
||||
|
||||
test('concurrent account reads share one generated frozen identity and one write', async () => {
|
||||
const storage = new MapStorage();
|
||||
let randomCalls = 0;
|
||||
const provider = createLocalVisitorProvider({ context: context(), storage,
|
||||
random: () => { randomCalls += 1; return 0.5; }, isSessionActive: () => false });
|
||||
const [first, second] = await Promise.all([provider.getAccount(), provider.getAccount()]);
|
||||
assert.strictEqual(first, second);
|
||||
assert.deepEqual(first, { openid: 'testopenid_50000', unionid: 'ylgame50000', nickname: '50000_游客',
|
||||
avatar: 'https://img2.baidu.com/it/u=3081347414,2363887197&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
sex: 2, province: 'jiangxi', city: 'nanchang' });
|
||||
assert.equal(Object.isFrozen(first), true);
|
||||
assert.equal(randomCalls, 3);
|
||||
assert.equal(storage.writes.length, 1);
|
||||
});
|
||||
|
||||
test('random boundaries select first and last valid values', async () => {
|
||||
for (const [samples, expected] of [
|
||||
[[0, 0, 0], ['testopenid_0', 1, 'u=1823726962,2692568839']],
|
||||
[[0.999999, 0.999999, 0.999999], ['testopenid_99999', 2, 'u=1801052125,1596537112']],
|
||||
] as const) {
|
||||
const queue = [...samples];
|
||||
const account = await createLocalVisitorProvider({ context: context(), storage: new MapStorage(),
|
||||
random: () => queue.shift()!, isSessionActive: () => false }).getAccount();
|
||||
assert.equal(account.openid, expected[0]);
|
||||
assert.equal(account.sex, expected[1]);
|
||||
assert.match(account.avatar, new RegExp(expected[2]));
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects every invalid random sample without writing', async () => {
|
||||
for (const invalid of [Number.NaN, Number.POSITIVE_INFINITY, -0.01, 1]) {
|
||||
const storage = new MapStorage();
|
||||
await assert.rejects(createLocalVisitorProvider({ context: context(), storage,
|
||||
random: () => invalid, isSessionActive: () => false }).getAccount(), /random.*finite|random.*range/i);
|
||||
assert.equal(storage.writes.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('reuses only an exact schema record and never randomizes it', async () => {
|
||||
const storage = new MapStorage();
|
||||
const account = { openid: 'cached', nickname: '', avatar: 'avatar', sex: 1, province: '', city: '', unionid: 'u' };
|
||||
storage.values.set(localAccountKey(context()), JSON.stringify({ schemaVersion: 1, account }));
|
||||
const loaded = await createLocalVisitorProvider({ context: context(), storage,
|
||||
random: () => { throw new Error('random forbidden'); }, isSessionActive: () => false }).getAccount();
|
||||
assert.deepEqual(loaded, account);
|
||||
assert.equal(Object.isFrozen(loaded), true);
|
||||
assert.equal(storage.writes.length, 0);
|
||||
});
|
||||
|
||||
test('rejects malformed cached records with key and field path without rewriting or leaking text', async () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
['{secret-account-text', '$'],
|
||||
[JSON.stringify({ schemaVersion: 2, account: {} }), 'schemaVersion'],
|
||||
[JSON.stringify({ schemaVersion: 1, account: { openid: 'x' } }), 'account.nickname'],
|
||||
[JSON.stringify({ schemaVersion: 1, account: { openid: 'x', nickname: '', avatar: '', sex: 1, province: '', city: '', unionid: 'u' } }), 'nickname'],
|
||||
[JSON.stringify({ schemaVersion: 1, account: { openid: 'x', nickname: 'n', avatar: '', sex: 1, province: '', city: '', unionid: 'u', headimgurl: 'old' } }), 'headimgurl'],
|
||||
[JSON.stringify({ schemaVersion: 1, account: { openid: 'x', nickname: 'n', avatar: '', sex: 1, province: '', city: '', unionid: 'u', Province: 'old' } }), 'Province'],
|
||||
];
|
||||
for (const [raw, path] of cases) {
|
||||
const storage = new MapStorage();
|
||||
const key = localAccountKey(context());
|
||||
storage.values.set(key, raw);
|
||||
await assert.rejects(createLocalVisitorProvider({ context: context(), storage,
|
||||
random: () => 0, isSessionActive: () => false }).getAccount(), (error: unknown) => {
|
||||
assert.ok(error instanceof Error);
|
||||
assert.match(error.message, new RegExp(path.replace('.', '\\.')));
|
||||
assert.match(error.message, /youle\.local-test\.account\.v1:/);
|
||||
assert.doesNotMatch(error.message, /secret-account-text/);
|
||||
return true;
|
||||
});
|
||||
assert.equal(storage.writes.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('preserves dependency failures as causes and keeps old storage on generation failure', async () => {
|
||||
const readFailure = new Error('read broke');
|
||||
await assert.rejects(createLocalVisitorProvider({ context: context(),
|
||||
storage: { getItem() { throw readFailure; }, setItem() {} }, random: () => 0, isSessionActive: () => false,
|
||||
}).getAccount(), (error: unknown) => error instanceof Error && (error as Error & { cause?: unknown }).cause === readFailure);
|
||||
const storage = new MapStorage();
|
||||
const key = localAccountKey(context());
|
||||
storage.values.set(key, 'old-value');
|
||||
const failure = new Error('generation broke');
|
||||
await assert.rejects(createLocalVisitorProvider({ context: context(), storage,
|
||||
random: () => { throw failure; }, isSessionActive: () => false }).resetAccount(),
|
||||
(error: unknown) => error instanceof Error && (error as Error & { cause?: unknown }).cause === failure);
|
||||
assert.equal(storage.values.get(key), 'old-value');
|
||||
|
||||
const writeFailure = new Error('write broke');
|
||||
await assert.rejects(createLocalVisitorProvider({ context: context(),
|
||||
storage: { getItem() { return null; }, setItem() { throw writeFailure; } },
|
||||
random: () => 0, isSessionActive: () => false,
|
||||
}).getAccount(), (error: unknown) => error instanceof Error && (error as Error & { cause?: unknown }).cause === writeFailure);
|
||||
});
|
||||
|
||||
test('scope isolates identity types, ignores version, and serializes the server configuration', () => {
|
||||
const base = context({ marketid: 7, version: 1 });
|
||||
assert.notEqual(localAccountKey(base), localAccountKey(context({ marketid: '7', version: 1 })));
|
||||
assert.equal(localAccountKey(base), localAccountKey(context({ marketid: 7, version: 999 })));
|
||||
assert.equal(localAccountKey(base),
|
||||
'youle.local-test.account.v1:["local",["ws://127.0.0.1:3088"],"veRa0qrBf0df2K1G4de2tgfmVxB2jxpv","J9r4022t80pxlb01zOK76DN1WD4QewFl","FtJf073aa0d6rI1xD8J1Y42fINTm0ziK",7]');
|
||||
});
|
||||
|
||||
test('reset is isolated, refuses active sessions, and refuses overlap with getAccount', async () => {
|
||||
const storage = new MapStorage();
|
||||
storage.values.set('visitorinfo', 'legacy');
|
||||
storage.values.set(LOCAL_MACHINE_KEY, 'machine');
|
||||
await assert.rejects(createLocalVisitorProvider({ context: context(), storage,
|
||||
random: () => 0, isSessionActive: () => true }).resetAccount(), /active session/i);
|
||||
assert.equal(storage.writes.length, 0);
|
||||
const provider = createLocalVisitorProvider({ context: context(), storage: new MapStorage(),
|
||||
random: () => 0, isSessionActive: () => false });
|
||||
const pending = provider.getAccount();
|
||||
await assert.rejects(provider.resetAccount(), /pending|getAccount/i);
|
||||
await pending;
|
||||
const reset = await createLocalVisitorProvider({ context: context(), storage,
|
||||
random: () => 0, isSessionActive: () => false }).resetAccount();
|
||||
assert.equal(reset.openid, 'testopenid_0');
|
||||
assert.deepEqual(storage.writes.map(([key]) => key), [localAccountKey(context())]);
|
||||
assert.equal(storage.values.get('visitorinfo'), 'legacy');
|
||||
assert.equal(storage.values.get(LOCAL_MACHINE_KEY), 'machine');
|
||||
});
|
||||
Reference in New Issue
Block a user