fix: 국호와 예약 인자의 불필요한 앰퍼샌드 escape를 제거한다

This commit is contained in:
Hide_D committed 2026-10-29 23:35:17 +09:00
1 parent 6a23e4ca08
commit da79013226
5 files changed
+50 -13

No files matched your search

+3 -1
View File
@@ -472,8 +472,10 @@ const sanitizeLegacyTurnArgString = (value: string): string => {
if (stripped === '' || stripped === '0') {
return '';
}
// Core의 Vue text와 formatLegacyLogHtml이 출력 시 &를 escape한다.
// Ref의 입력 단계 & 변환까지 적용하면 국호/예약 인자에 entity가 저장된다.
// <, > 중화는 로그의 legacy markup과 사용자 입력을 구분하기 위해 유지한다.
return stripped
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replace(/^[\p{Z}\p{C}]+|[\p{Z}\p{C}]+$/gu, '');
+23 -1
View File
@@ -152,7 +152,29 @@ describe('turn command argument input', () => {
);
});
it('recursively sanitizes reserved command strings like Ref before command parsing', async () => {
it('preserves ampersands through reservation parsing and repeated sanitization', async () => {
const args = { nationName: '풍년F&B', nested: ['A&B', '&amp;', '&lt;img&gt;'] };
expect(sanitizeReservedTurnArgs(args)).toEqual(args);
expect(sanitizeReservedTurnArgs(sanitizeReservedTurnArgs(args))).toEqual(args);
await expect(parseReservedTurnArgs('nation', 'che_국호변경', args)).resolves.toEqual({
nationName: '풍년F&B',
});
await expect(
parseReservedTurnArgs('general', 'che_건국', {
nationName: '풍년F&B',
nationType: 'che_도적',
colorType: 0,
})
).resolves.toMatchObject({ nationName: '풍년F&B' });
// 반각 18자 판정에 &amp;의 저장 길이가 끼어들지 않는다.
const maxWidthName = '가나다라마바사아&B';
expect(() => assertReservedTurnArgsPassLegacyBasicValidation({ nationName: maxWidthName })).not.toThrow();
await expect(parseReservedTurnArgs('nation', 'che_국호변경', { nationName: maxWidthName })).resolves.toEqual({
nationName: maxWidthName,
});
});
it('retains Ref character removal and angle-bracket neutralization before command parsing', async () => {
expect(
sanitizeReservedTurnArgs({
nationName: ' <신-국># ',
+7 -6
View File
@@ -1110,7 +1110,10 @@ describe('appRouter', () => {
expect(nationWrites).toHaveLength(2);
});
it('stores Ref-sanitized nation command strings in the reserved queue', async () => {
it.each([
[' <신-국># ', '&lt;신국&gt;'],
['풍년F&B', '풍년F&B'],
])('stores the sanitized nation name %s in the reserved queue as %s', async (input, stored) => {
const general = buildGeneralRow({ id: 19, nationId: 3, officerLevel: 12 });
const nationWrites: unknown[] = [];
const caller = appRouter.createCaller(
@@ -1121,16 +1124,14 @@ describe('appRouter', () => {
generalId: general.id,
turnIndex: 0,
action: 'che_국호변경',
args: { nationName: ' <신-국># ' },
args: { nationName: input },
expectedRevision: 0,
});
expect(response.turns[0]?.args).toEqual({ nationName: '&lt;신국&gt;' });
expect(response.turns[0]?.args).toEqual({ nationName: stored });
expect(nationWrites).toHaveLength(1);
expect(nationWrites[0]).toMatchObject({
data: expect.arrayContaining([
expect.objectContaining({ turnIdx: 0, arg: { nationName: '&lt;신국&gt;' } }),
]),
data: expect.arrayContaining([expect.objectContaining({ turnIdx: 0, arg: { nationName: stored } })]),
});
});
+14 -2
View File
@@ -27,6 +27,10 @@ const history = [
id: 3,
text: '이번 수입은 금 <C>158</>입니다.',
},
{
id: 4,
text: '<D><b>풍년F&B</b></> &#60;img src=x onerror=globalThis.__legacyLogXss=4&#62;',
},
];
const publicResponse = (operation: string): unknown => {
@@ -36,7 +40,12 @@ const publicResponse = (operation: string): unknown => {
return response({ year: 200, month: 1, cityList: [], nationList: [], history });
}
if (operation === 'public.getWorldTrend') return response({ year: 200, month: 1, turnTerm: 10 });
if (operation === 'public.getNationList' || operation === 'public.getGeneralList') return response([]);
if (operation === 'public.getNationList') {
return response([
{ id: 13, name: '풍년F&B', color: '#330000', level: 1, capitalCityId: 0, generalCount: 1, cityCount: 0 },
]);
}
if (operation === 'public.getGeneralList') return response([]);
throw new Error(`Unhandled legacy log fixture operation: ${operation}`);
};
@@ -64,12 +73,15 @@ for (const viewport of [
await page.goto('public');
const lines = page.locator('.recent-log-line');
await expect(lines).toHaveCount(3);
await expect(lines).toHaveCount(4);
await expect(lines.nth(0).locator('b')).toHaveText('안전 강조');
await expect(lines.nth(1).locator('.small_war_log .war_type_attack')).toHaveText('→');
await expect(lines.nth(1).locator('.ev_highlight')).toHaveText('강조');
await expect(lines.nth(2)).toHaveText('이번 수입은 금 158입니다.');
await expect(lines.nth(2)).not.toContainText('157.5');
await expect(lines.nth(3).locator('b')).toHaveText('풍년F&B');
await expect(lines.nth(3)).toContainText('&#60;img');
await expect(page.getByRole('cell', { name: '풍년F&B', exact: true })).toBeVisible();
await expect(lines.locator('script, img, svg, a, [onerror], [onclick], [style*="url"]')).toHaveCount(0);
await expect(lines.nth(0)).toContainText('<img src=x onerror=');
await expect(lines.nth(1)).toContainText('<span class="name" onclick=');