fix: 특수 유저 커맨드의 Ref 호환 경계를 보강한다
수뇌 국가 설정과 NPC 정책을 actor-bound ENGINE mutation으로 옮기고, 추방·등용·점령·멸망·아이템 폐기의 특수 분기를 Ref와 맞춘다. 요청 ID를 사용자·프로필별로 격리하고 토너먼트 손상 projection을 fail-closed하며 실제 DB 및 Ref 차등 회귀를 보강한다.
This commit is contained in:
@@ -73,8 +73,9 @@ const auth: GameSessionTokenPayload = {
|
||||
const buildContext = () => {
|
||||
const changeJournal = new ChangeJournal();
|
||||
const requestCommand = vi.fn(async (command: unknown) => ({
|
||||
type: 'setNationMeta',
|
||||
type: 'setNationSetting',
|
||||
ok: true,
|
||||
nationId: 1,
|
||||
updatedAt: '2026-01-01T00:00:01.000Z',
|
||||
command,
|
||||
}));
|
||||
@@ -97,6 +98,7 @@ const buildContext = () => {
|
||||
battleSim: {} as GameApiContext['battleSim'],
|
||||
profile: { id: 'che', scenario: 'default', name: 'che:default' },
|
||||
auth,
|
||||
requestId: 'http-nation-html',
|
||||
changeJournal,
|
||||
uploadDir: 'uploads',
|
||||
uploadPath: '/uploads',
|
||||
@@ -131,29 +133,59 @@ describe('nation HTML API boundary', () => {
|
||||
});
|
||||
|
||||
expect(fixture.requestCommand).toHaveBeenCalledWith({
|
||||
type: 'setNationMeta',
|
||||
type: 'setNationSetting',
|
||||
requestId: `http-nation-html:nation.${procedure}:engine:0:setNationSetting`,
|
||||
userId: 'user-1',
|
||||
generalId: 1,
|
||||
nationId: 1,
|
||||
updates: {
|
||||
[metaKey]: msg,
|
||||
},
|
||||
expectedUpdatedAt: undefined,
|
||||
mutation: metaKey === 'notice' ? { kind: 'notice', message: msg } : { kind: 'scoutMessage', message: msg },
|
||||
});
|
||||
expect(fixture.changeJournal.snapshot()).toEqual([
|
||||
{ domain: 'dashboard.global', entityId: 0 },
|
||||
...(procedure === 'setNotice' ? [{ domain: 'front.nation' as const, entityId: 1 }] : []),
|
||||
{ domain: 'nation.content', entityId: 1 },
|
||||
]);
|
||||
expect(fixture.changeJournal.snapshot()).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(['setNotice', 'setScoutMsg'] as const)(
|
||||
'rejects an empty $procedure value like Ref required validation',
|
||||
'rejects empty and whitespace-only $procedure values like Ref required validation',
|
||||
async (procedure) => {
|
||||
await expect(buildContext().caller.nation[procedure]({ msg: '' })).rejects.toMatchObject({
|
||||
code: 'BAD_REQUEST',
|
||||
});
|
||||
for (const msg of ['', ' \t\n\v\0']) {
|
||||
const fixture = buildContext();
|
||||
await expect(fixture.caller.nation[procedure]({ msg })).rejects.toMatchObject({
|
||||
code: 'BAD_REQUEST',
|
||||
});
|
||||
expect(fixture.requestCommand).not.toHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['setNotice', 'setScoutMsg'] as const)(
|
||||
'keeps PHP trim semantics for a Unicode-only $procedure value',
|
||||
async (procedure) => {
|
||||
const fixture = buildContext();
|
||||
await expect(fixture.caller.nation[procedure]({ msg: ' ' })).resolves.toMatchObject({
|
||||
ok: true,
|
||||
msg: ' ',
|
||||
});
|
||||
expect(fixture.requestCommand).toHaveBeenCalledOnce();
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
['setNotice', 'notice'],
|
||||
['setScoutMsg', 'scoutMessage'],
|
||||
] as const)('dispatches unsafe-only $procedure HTML as the empty string Ref persists', async (procedure, kind) => {
|
||||
const fixture = buildContext();
|
||||
|
||||
await expect(fixture.caller.nation[procedure]({ msg: '<script></script>' })).resolves.toEqual({
|
||||
ok: true,
|
||||
msg: '',
|
||||
});
|
||||
expect(fixture.requestCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'setNationSetting',
|
||||
mutation: { kind, message: '' },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('purifies legacy stored values on every read resolver', () => {
|
||||
expect(
|
||||
resolveNationNotice({
|
||||
|
||||
Reference in New Issue
Block a user