fix: 국호와 예약 인자의 불필요한 앰퍼샌드 escape를 제거한다
This commit is contained in:
1 parent
6a23e4ca08
commit
da79013226
5 files changed
+50
-13
No files matched your search
@@ -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('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replace(/^[\p{Z}\p{C}]+|[\p{Z}\p{C}]+$/gu, '');
|
||||
|
||||
@@ -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', '&', '<img>'] };
|
||||
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자 판정에 &의 저장 길이가 끼어들지 않는다.
|
||||
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: ' <신-국># ',
|
||||
|
||||
@@ -1110,7 +1110,10 @@ describe('appRouter', () => {
|
||||
expect(nationWrites).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('stores Ref-sanitized nation command strings in the reserved queue', async () => {
|
||||
it.each([
|
||||
[' <신-국># ', '<신국>'],
|
||||
['풍년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: '<신국>' });
|
||||
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: '<신국>' } }),
|
||||
]),
|
||||
data: expect.arrayContaining([expect.objectContaining({ turnIdx: 0, arg: { nationName: stored } })]),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ const history = [
|
||||
id: 3,
|
||||
text: '이번 수입은 금 <C>158</>입니다.',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
text: '<D><b>풍년F&B</b></> <img src=x onerror=globalThis.__legacyLogXss=4>',
|
||||
},
|
||||
];
|
||||
|
||||
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('<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=');
|
||||
|
||||
@@ -396,7 +396,7 @@ describe('Nation Actions', () => {
|
||||
expect(changeNationNameArgsSchema.safeParse({ nationName: '가나다라마바사아자차' }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('changes nation name', () => {
|
||||
it.each(['NewName', '풍년F&B'])('changes nation name to %s without HTML encoding', (name) => {
|
||||
const nation = buildNation(1, 'OldName');
|
||||
const general = buildGeneral(1, 1, 1);
|
||||
const definition = new ChangeNationNameAction();
|
||||
@@ -408,12 +408,12 @@ describe('Nation Actions', () => {
|
||||
addLog: () => {},
|
||||
};
|
||||
|
||||
const resolution = definition.resolve(context as any, { nationName: 'NewName' });
|
||||
const resolution = definition.resolve(context as any, { nationName: name });
|
||||
|
||||
expect(resolution.effects).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'nation:patch',
|
||||
patch: expect.objectContaining({ name: 'NewName' }),
|
||||
patch: expect.objectContaining({ name }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user