fix: align legacy general access call boundaries
This commit is contained in:
@@ -161,9 +161,9 @@ const install = async (
|
||||
);
|
||||
await page.route('**/che/api/trpc/**', async (route) => {
|
||||
const requestBody = route.request().postDataJSON() as
|
||||
| Record<string, { json?: { page?: unknown }; page?: unknown }>
|
||||
| undefined;
|
||||
Record<string, { json?: { page?: unknown }; page?: unknown }> | undefined;
|
||||
const results = operationNames(route).map((operation, operationIndex) => {
|
||||
if (operation === 'auth.status') return response({ ok: true });
|
||||
if (operation === 'lobby.info') {
|
||||
return response({ myGeneral: mode === 'no-general' ? null : { id: 1, name: '조회자' } });
|
||||
}
|
||||
@@ -191,7 +191,8 @@ const install = async (
|
||||
};
|
||||
}
|
||||
const sort = parseSort(route);
|
||||
const rows = sort === 8 ? [...generals].sort((left, right) => left.killturn - right.killturn) : generals;
|
||||
const rows =
|
||||
sort === 8 ? [...generals].sort((left, right) => left.killturn - right.killturn) : generals;
|
||||
return response({ sort, generals: rows });
|
||||
}
|
||||
return { error: { message: `unhandled ${operation}`, data: { code: 'BAD_REQUEST' } } };
|
||||
@@ -200,6 +201,30 @@ const install = async (
|
||||
});
|
||||
};
|
||||
|
||||
const installAccessBoundary = async (page: Page, accessPages: string[]) => {
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('sammo-game-token', 'ga_access_boundary');
|
||||
localStorage.setItem('sammo-game-profile', 'che:default');
|
||||
});
|
||||
await page.route('**/image/**', (route) => route.abort('failed'));
|
||||
await page.route('**/che/api/trpc/**', async (route) => {
|
||||
const requestBody = route.request().postDataJSON() as
|
||||
Record<string, { json?: { page?: unknown }; page?: unknown }> | undefined;
|
||||
const results = operationNames(route).map((operation, operationIndex) => {
|
||||
if (operation === 'auth.status') return response({ ok: true });
|
||||
if (operation === 'lobby.info') return response({ myGeneral: { id: 1, name: '조회자' } });
|
||||
if (operation === 'public.recordAccess') {
|
||||
const payload = requestBody?.[String(operationIndex)];
|
||||
const pageName = payload?.json?.page ?? payload?.page;
|
||||
if (typeof pageName === 'string') accessPages.push(pageName);
|
||||
return response({ recorded: true });
|
||||
}
|
||||
return response({});
|
||||
});
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results) });
|
||||
});
|
||||
};
|
||||
|
||||
test('nation and general directories preserve the fixed legacy Chromium geometry', async ({ page }) => {
|
||||
const accessPages: string[] = [];
|
||||
await install(page, 'general', accessPages);
|
||||
@@ -283,7 +308,8 @@ test('nation and general directories preserve the fixed legacy Chromium geometry
|
||||
).toBe(65);
|
||||
}
|
||||
}
|
||||
await expect.poll(() => accessPages).toEqual(expect.arrayContaining(['nation-list', 'general-list']));
|
||||
await expect.poll(() => accessPages).toContain('nation-list');
|
||||
expect(accessPages).not.toContain('general-list');
|
||||
|
||||
const header = page.locator('.general-table thead td').first();
|
||||
expect(await header.evaluate((element) => getComputedStyle(element).backgroundImage)).toContain('back_green.jpg');
|
||||
@@ -345,3 +371,51 @@ test('an authenticated account without a general is redirected away from both di
|
||||
await expect(page).toHaveURL(/\/che\/join$/);
|
||||
}
|
||||
});
|
||||
|
||||
test('route access belongs only to the eight Ref page boundaries', async ({ page }) => {
|
||||
const accessPages: string[] = [];
|
||||
await installAccessBoundary(page, accessPages);
|
||||
const retained = [
|
||||
['nation/info', 'nation-info'],
|
||||
['nation/cities', 'nation-cities'],
|
||||
['nation-list', 'nation-list'],
|
||||
['current-city', 'current-city'],
|
||||
['dynasty', 'dynasty'],
|
||||
['dynasty/1', 'dynasty'],
|
||||
['traffic', 'traffic'],
|
||||
['npc-control', 'npc-control'],
|
||||
] as const;
|
||||
for (const [path, pageName] of retained) {
|
||||
const before = accessPages.length;
|
||||
await page.goto(path);
|
||||
await expect.poll(() => accessPages.length).toBe(before + 1);
|
||||
expect(accessPages.at(-1)).toBe(pageName);
|
||||
}
|
||||
|
||||
const endpointOwned = [
|
||||
'./',
|
||||
'global-info',
|
||||
'general-list',
|
||||
'diplomacy',
|
||||
'nation/generals',
|
||||
'nation/personnel',
|
||||
'nation/finance',
|
||||
'battle-center',
|
||||
'board',
|
||||
'board/secret',
|
||||
'best-general',
|
||||
'hall-of-fame',
|
||||
'yearbook',
|
||||
'nation-betting',
|
||||
'npc-list',
|
||||
'my-page',
|
||||
'tournament',
|
||||
'betting',
|
||||
];
|
||||
for (const path of endpointOwned) {
|
||||
const before = accessPages.length;
|
||||
await page.goto(path);
|
||||
await page.waitForTimeout(50);
|
||||
expect(accessPages).toHaveLength(before);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,12 +26,16 @@ type FixtureState = {
|
||||
instantRetreatEnabled?: boolean;
|
||||
instantRetreatAttempts?: number;
|
||||
instantRetreatInputs?: Array<Record<string, unknown>>;
|
||||
buildNationCandidateEnabled?: boolean;
|
||||
buildNationCandidateAttempts?: number;
|
||||
buildNationCandidateInputs?: Array<Record<string, unknown>>;
|
||||
dieOnPrestartShow?: boolean;
|
||||
dieOnPrestartAvailableAt?: string;
|
||||
dieOnPrestartAttempts?: number;
|
||||
dieOnPrestartInputs?: Array<Record<string, unknown>>;
|
||||
generalMeQueries?: number;
|
||||
generalLogQueries?: number;
|
||||
ensurePrestartQueries?: number;
|
||||
nationNoticeInput?: string;
|
||||
settingMutations: Array<Record<string, unknown>>;
|
||||
accessPages: string[];
|
||||
@@ -47,7 +51,7 @@ const myGeneral = (state: FixtureState) => ({
|
||||
id: 7,
|
||||
name: '검증장수',
|
||||
npcState: 0,
|
||||
nationId: 1,
|
||||
nationId: state.buildNationCandidateEnabled ? 0 : 1,
|
||||
cityId: 1,
|
||||
troopId: 0,
|
||||
picture: null,
|
||||
@@ -165,12 +169,14 @@ const install = async (page: Page, state: FixtureState) => {
|
||||
state.generalMeQueries = (state.generalMeQueries ?? 0) + 1;
|
||||
return response(myGeneral(state));
|
||||
}
|
||||
if (operation === 'general.ensureDieOnPrestartStatus')
|
||||
if (operation === 'general.ensureDieOnPrestartStatus') {
|
||||
state.ensurePrestartQueries = (state.ensurePrestartQueries ?? 0) + 1;
|
||||
return response({
|
||||
show: state.dieOnPrestartShow ?? false,
|
||||
available: false,
|
||||
availableAt: state.dieOnPrestartAvailableAt ?? null,
|
||||
});
|
||||
}
|
||||
if (operation === 'general.getFrontStatus')
|
||||
return response({
|
||||
onlineUserCount: 1,
|
||||
@@ -196,7 +202,9 @@ const install = async (page: Page, state: FixtureState) => {
|
||||
},
|
||||
meta: {
|
||||
turntime: '2026-01-01T00:00:00.000Z',
|
||||
opentime: '2025-12-01T00:00:00.000Z',
|
||||
opentime: state.buildNationCandidateEnabled
|
||||
? '2026-02-01T00:00:00.000Z'
|
||||
: '2025-12-01T00:00:00.000Z',
|
||||
autorun_user: {},
|
||||
},
|
||||
});
|
||||
@@ -261,6 +269,20 @@ const install = async (page: Page, state: FixtureState) => {
|
||||
}
|
||||
return response({ ok: true });
|
||||
}
|
||||
if (operation === 'general.buildNationCandidate') {
|
||||
state.buildNationCandidateInputs?.push(jsonInput);
|
||||
state.buildNationCandidateAttempts = (state.buildNationCandidateAttempts ?? 0) + 1;
|
||||
if (state.buildNationCandidateAttempts === 1) {
|
||||
return {
|
||||
error: {
|
||||
message: '요청 처리 결과를 확인하지 못했습니다.',
|
||||
code: -32000,
|
||||
data: { code: 'TIMEOUT', httpStatus: 408, path: operation },
|
||||
},
|
||||
};
|
||||
}
|
||||
return response({ ok: true });
|
||||
}
|
||||
if (operation === 'general.dieOnPrestart') {
|
||||
state.dieOnPrestartInputs?.push(jsonInput);
|
||||
state.dieOnPrestartAttempts = (state.dieOnPrestartAttempts ?? 0) + 1;
|
||||
@@ -395,7 +417,8 @@ test('내 정보&설정 keeps the legacy 1000px/500px geometry and saves in plac
|
||||
await page.goto('my-page');
|
||||
await expect(page.locator('.title-row')).toContainText('내 정 보');
|
||||
await expect(page.locator('#set_my_setting')).toBeVisible();
|
||||
await expect.poll(() => state.accessPages).toContain('my-page');
|
||||
await expect.poll(() => state.generalMeQueries).toBeGreaterThan(0);
|
||||
expect(state.accessPages).not.toContain('my-page');
|
||||
const noDefenceOption = page.locator('option[value="999"]');
|
||||
await expect(noDefenceOption).toHaveText('× [훈련 -3,사기 -6]');
|
||||
await expect(page.locator('#defence_train option')).toHaveText([
|
||||
@@ -533,6 +556,7 @@ test('내 정보 즉시행동은 timeout 재시도 ID를 유지하고 성공 후
|
||||
instantRetreatInputs: [],
|
||||
generalMeQueries: 0,
|
||||
generalLogQueries: 0,
|
||||
ensurePrestartQueries: 0,
|
||||
settingMutations: [],
|
||||
accessPages: [],
|
||||
};
|
||||
@@ -547,18 +571,21 @@ test('내 정보 즉시행동은 timeout 재시도 ID를 유지하고 성공 후
|
||||
const instantRetreatButton = page.getByRole('button', { name: '접경 귀환' });
|
||||
await expect(instantRetreatButton).toBeVisible();
|
||||
await expect.poll(() => state.generalMeQueries).toBe(1);
|
||||
await expect.poll(() => state.ensurePrestartQueries).toBe(1);
|
||||
|
||||
await instantRetreatButton.click();
|
||||
await expect.poll(() => state.instantRetreatInputs?.length).toBe(1);
|
||||
await expect
|
||||
.poll(() => dialogs.some((message) => message.includes('요청 처리 결과를 확인하지 못했습니다.')))
|
||||
.toBe(true);
|
||||
expect(state.generalMeQueries).toBe(1);
|
||||
await expect.poll(() => state.generalMeQueries).toBe(2);
|
||||
await expect.poll(() => state.ensurePrestartQueries).toBe(2);
|
||||
|
||||
await instantRetreatButton.click();
|
||||
await expect.poll(() => state.instantRetreatInputs?.length).toBe(2);
|
||||
await expect.poll(() => state.generalMeQueries).toBe(2);
|
||||
await expect.poll(() => state.generalLogQueries).toBe(8);
|
||||
await expect.poll(() => state.generalMeQueries).toBe(3);
|
||||
await expect.poll(() => state.ensurePrestartQueries).toBe(3);
|
||||
await expect.poll(() => state.generalLogQueries).toBe(12);
|
||||
await page.evaluate(() => new Promise<void>((resolveFrame) => requestAnimationFrame(() => resolveFrame())));
|
||||
|
||||
await instantRetreatButton.click();
|
||||
@@ -690,6 +717,40 @@ test('가오픈 장수 삭제는 레거시 표시와 확인을 보존하고 time
|
||||
});
|
||||
});
|
||||
|
||||
test('사전 거병은 timeout reload 뒤 같은 ID를 재시도하고 성공 reload 뒤 새 ID를 만든다', async ({ page }) => {
|
||||
const state: FixtureState = {
|
||||
permission: 'head',
|
||||
myset: 3,
|
||||
buildNationCandidateEnabled: true,
|
||||
buildNationCandidateAttempts: 0,
|
||||
buildNationCandidateInputs: [],
|
||||
ensurePrestartQueries: 0,
|
||||
settingMutations: [],
|
||||
accessPages: [],
|
||||
};
|
||||
page.on('dialog', (dialog) => dialog.accept());
|
||||
await install(page, state);
|
||||
await page.goto('my-page');
|
||||
|
||||
const build = page.getByRole('button', { name: '사전 거병' });
|
||||
await expect(build).toBeVisible();
|
||||
await expect.poll(() => state.ensurePrestartQueries).toBe(1);
|
||||
|
||||
await build.click();
|
||||
await expect.poll(() => state.buildNationCandidateInputs?.length).toBe(1);
|
||||
await expect.poll(() => state.ensurePrestartQueries).toBe(2);
|
||||
|
||||
await build.click();
|
||||
await expect.poll(() => state.buildNationCandidateInputs?.length).toBe(2);
|
||||
await expect.poll(() => state.ensurePrestartQueries).toBe(3);
|
||||
|
||||
await build.click();
|
||||
await expect.poll(() => state.buildNationCandidateInputs?.length).toBe(3);
|
||||
const requestIds = state.buildNationCandidateInputs?.map((input) => input.clientRequestId);
|
||||
expect(requestIds?.[1]).toBe(requestIds?.[0]);
|
||||
expect(requestIds?.[2]).not.toBe(requestIds?.[1]);
|
||||
});
|
||||
|
||||
test('감찰부 keeps the selector interaction and shows the permission error path', async ({ page }) => {
|
||||
const head: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
|
||||
await install(page, head);
|
||||
|
||||
@@ -41,32 +41,14 @@ import { useSessionStore } from '../stores/session';
|
||||
import { trpc } from '../utils/trpc';
|
||||
|
||||
const accessPageByRouteName = {
|
||||
home: 'front-info',
|
||||
'nation-info': 'nation-info',
|
||||
'nation-cities': 'nation-cities',
|
||||
'global-info': 'global-info',
|
||||
'nation-list': 'nation-list',
|
||||
'general-list': 'general-list',
|
||||
'current-city': 'current-city',
|
||||
diplomacy: 'diplomacy',
|
||||
'nation-generals': 'nation-generals',
|
||||
'nation-personnel': 'nation-personnel',
|
||||
'nation-finance': 'nation-finance',
|
||||
'battle-center': 'battle-center',
|
||||
board: 'board',
|
||||
'board-secret': 'board',
|
||||
'best-general': 'best-general',
|
||||
'hall-of-fame': 'hall-of-fame',
|
||||
'dynasty-list': 'dynasty',
|
||||
'dynasty-detail': 'dynasty',
|
||||
yearbook: 'yearbook',
|
||||
'nation-betting': 'nation-betting',
|
||||
traffic: 'traffic',
|
||||
'npc-list': 'npc-list',
|
||||
'my-page': 'my-page',
|
||||
'npc-control': 'npc-control',
|
||||
tournament: 'tournament',
|
||||
betting: 'betting',
|
||||
} as const;
|
||||
|
||||
const routes = [
|
||||
|
||||
@@ -86,9 +86,10 @@ const placeBet = async (targetId: number) => {
|
||||
try {
|
||||
await trpc.tournament.placeBet.mutate({ targetId, amount });
|
||||
message.value = '베팅이 등록되었습니다.';
|
||||
await load();
|
||||
} catch (value) {
|
||||
message.value = errorText(value);
|
||||
} finally {
|
||||
await load();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -106,10 +106,7 @@ watch([selectedSeason, selectedScenario], () => {
|
||||
void loadHall();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadOptions();
|
||||
await loadHall();
|
||||
});
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -187,7 +187,7 @@ const loadLog = async (type: LogType, beforeId?: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadPage = async () => {
|
||||
const loadPage = async (resetImmediateActionIds = true) => {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
@@ -206,7 +206,9 @@ const loadPage = async () => {
|
||||
Object.assign(form, general.settings);
|
||||
}
|
||||
await Promise.all(logTypes.map((type) => loadLog(type)));
|
||||
resetImmediateActionRequestIds();
|
||||
if (resetImmediateActionIds) {
|
||||
resetImmediateActionRequestIds();
|
||||
}
|
||||
} catch (cause) {
|
||||
error.value = errorText(cause);
|
||||
} finally {
|
||||
@@ -224,13 +226,17 @@ const saveSettings = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const confirmMutation = async (message: string, mutation: () => Promise<unknown>) => {
|
||||
const confirmMutation = async (message: string, mutation: () => Promise<unknown>, reloadAfterFailure = false) => {
|
||||
if (!confirm(message)) return;
|
||||
try {
|
||||
await mutation();
|
||||
await loadPage();
|
||||
} catch (cause) {
|
||||
alert(`실패했습니다: ${errorText(cause)}`);
|
||||
if (reloadAfterFailure) {
|
||||
const code = asRecord(asRecord(cause).data).code;
|
||||
await loadPage(code !== 'TIMEOUT');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -288,7 +294,7 @@ onMounted(() => {
|
||||
<span>내 정 보</span>
|
||||
<RouterLink class="legacy-button" to="/past-plays">지난 플레이</RouterLink>
|
||||
<RouterLink class="legacy-button" to="/">돌아가기</RouterLink>
|
||||
<button class="legacy-button" type="button" @click="loadPage">새로고침</button>
|
||||
<button class="legacy-button" type="button" @click="() => loadPage()">새로고침</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-row">{{ error }}</div>
|
||||
@@ -430,10 +436,13 @@ onMounted(() => {
|
||||
<button
|
||||
class="action-button"
|
||||
@click="
|
||||
confirmMutation('거병 이후 장수를 삭제할 수 없게됩니다. 거병하시겠습니까?', () =>
|
||||
trpc.general.buildNationCandidate.mutate({
|
||||
clientRequestId: immediateActionRequestIds.buildNationCandidate,
|
||||
})
|
||||
confirmMutation(
|
||||
'거병 이후 장수를 삭제할 수 없게됩니다. 거병하시겠습니까?',
|
||||
() =>
|
||||
trpc.general.buildNationCandidate.mutate({
|
||||
clientRequestId: immediateActionRequestIds.buildNationCandidate,
|
||||
}),
|
||||
true
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -445,10 +454,13 @@ onMounted(() => {
|
||||
<button
|
||||
class="action-button"
|
||||
@click="
|
||||
confirmMutation('아군 접경으로 이동할까요?', () =>
|
||||
trpc.general.instantRetreat.mutate({
|
||||
clientRequestId: immediateActionRequestIds.instantRetreat,
|
||||
})
|
||||
confirmMutation(
|
||||
'아군 접경으로 이동할까요?',
|
||||
() =>
|
||||
trpc.general.instantRetreat.mutate({
|
||||
clientRequestId: immediateActionRequestIds.instantRetreat,
|
||||
}),
|
||||
true
|
||||
)
|
||||
"
|
||||
>
|
||||
|
||||
@@ -102,9 +102,10 @@ const join = async () => {
|
||||
try {
|
||||
await trpc.tournament.join.mutate();
|
||||
actionMessage.value = '참가 신청이 반영되었습니다.';
|
||||
await load();
|
||||
} catch (value) {
|
||||
actionMessage.value = errorText(value);
|
||||
} finally {
|
||||
await load();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user