fix(message): 수뇌의 재야 광고 발송 대상을 복원
Ref와 같이 외교 권한이 있는 장수에게 재야 국가 우편함을 노출한다. 서버 권한 강제와 재야 수신 경로, 실제 Chromium 발송 흐름을 회귀 테스트로 고정한다.
This commit is contained in:
@@ -260,6 +260,116 @@ describe('messages router missing-flow compatibility', () => {
|
|||||||
expect(queryRaw.mock.calls[0]?.slice(1)).toEqual(expect.arrayContaining([9002, 'diplomacy']));
|
expect(queryRaw.mock.calls[0]?.slice(1)).toEqual(expect.arrayContaining([9002, 'diplomacy']));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows a ruler to send a recruitment advertisement to the wanderer mailbox', async () => {
|
||||||
|
const ruler = { ...general, officerLevel: 12 } as GeneralRow;
|
||||||
|
const queryRaw = vi.fn(async () => [{ id: 53 }]);
|
||||||
|
const changeJournal = new ChangeJournal();
|
||||||
|
const { caller } = buildContext(
|
||||||
|
{
|
||||||
|
$queryRaw: queryRaw,
|
||||||
|
general: {
|
||||||
|
findUnique: vi.fn(async () => ruler),
|
||||||
|
findMany: vi.fn(async () => []),
|
||||||
|
},
|
||||||
|
nation: {
|
||||||
|
findMany: vi.fn(async () => []),
|
||||||
|
findUnique: vi.fn(async () => ({ id: 1, name: '위', color: '#112233', meta: {} })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ changeJournal }
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await caller.messages.send({
|
||||||
|
generalId: ruler.id,
|
||||||
|
mailbox: 9000,
|
||||||
|
text: '우리 나라로 와주세요',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.msgType).toBe('diplomacy');
|
||||||
|
expect(queryRaw.mock.calls[0]?.slice(1)).toEqual(expect.arrayContaining([9000, 'diplomacy']));
|
||||||
|
expect(queryRaw).toHaveBeenCalledTimes(2);
|
||||||
|
expect(changeJournal.snapshot()).toEqual([
|
||||||
|
{ domain: 'messages.mailbox', entityId: 9000 },
|
||||||
|
{ domain: 'messages.mailbox', entityId: 9001 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the wanderer mailbox unavailable to a non-diplomat on the server', async () => {
|
||||||
|
const queryRaw = vi.fn(async () => [{ id: 54 }]);
|
||||||
|
const { caller } = buildContext({
|
||||||
|
$queryRaw: queryRaw,
|
||||||
|
nation: {
|
||||||
|
findMany: vi.fn(async () => []),
|
||||||
|
findUnique: vi.fn(async () => ({ id: 1, name: '위', color: '#112233', meta: {} })),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await caller.messages.send({
|
||||||
|
generalId: general.id,
|
||||||
|
mailbox: 9000,
|
||||||
|
text: '권한 없는 재야 광고',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.msgType).toBe('national');
|
||||||
|
expect(queryRaw).toHaveBeenCalledTimes(1);
|
||||||
|
expect(queryRaw.mock.calls[0]?.slice(1)).toEqual(expect.arrayContaining([9001, 'national']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a wanderer the advertisement stored in mailbox 9000 without diplomacy redaction', async () => {
|
||||||
|
const wanderer = { ...general, nationId: 0, officerLevel: 0 } as GeneralRow;
|
||||||
|
const advertisementRow = {
|
||||||
|
id: 55,
|
||||||
|
mailbox: 9000,
|
||||||
|
type: 'diplomacy',
|
||||||
|
src: 9001,
|
||||||
|
dest: 9000,
|
||||||
|
time: new Date(),
|
||||||
|
valid_until: new Date('9999-12-31T00:00:00Z'),
|
||||||
|
message: {
|
||||||
|
src: {
|
||||||
|
generalId: 1,
|
||||||
|
generalName: '위왕',
|
||||||
|
nationId: 1,
|
||||||
|
nationName: '위',
|
||||||
|
color: '#112233',
|
||||||
|
icon: '',
|
||||||
|
},
|
||||||
|
dest: {
|
||||||
|
generalId: 0,
|
||||||
|
generalName: '',
|
||||||
|
nationId: 0,
|
||||||
|
nationName: '재야',
|
||||||
|
color: '#000000',
|
||||||
|
icon: '',
|
||||||
|
},
|
||||||
|
text: '우리 나라로 와주세요',
|
||||||
|
option: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const queryRaw = vi.fn(async (...args: unknown[]) => {
|
||||||
|
const values = args.slice(1);
|
||||||
|
return values.includes(9000) && values.includes('diplomacy') ? [advertisementRow] : [];
|
||||||
|
});
|
||||||
|
const { caller } = buildContext({
|
||||||
|
$queryRaw: queryRaw,
|
||||||
|
general: {
|
||||||
|
findUnique: vi.fn(async () => wanderer),
|
||||||
|
findMany: vi.fn(async () => []),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await caller.messages.getRecent({ generalId: wanderer.id });
|
||||||
|
|
||||||
|
expect(result.permission).toBe(-1);
|
||||||
|
expect(result.diplomacy).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
text: '우리 나라로 와주세요',
|
||||||
|
dest: expect.objectContaining({ nationId: 0, nationName: '재야' }),
|
||||||
|
option: {},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('blocks private messages between foreign ambassadors', async () => {
|
it('blocks private messages between foreign ambassadors', async () => {
|
||||||
const ambassador = {
|
const ambassador = {
|
||||||
...general,
|
...general,
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
label: '외교메시지',
|
label: '외교메시지',
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
options: contacts
|
options: contacts
|
||||||
.filter((nation) => nation.mailbox !== ownMailbox && nation.nationId > 0)
|
.filter((nation) => nation.mailbox !== ownMailbox)
|
||||||
.map((nation) => ({
|
.map((nation) => ({
|
||||||
label: nation.name,
|
label: nation.name,
|
||||||
value: nation.mailbox,
|
value: nation.mailbox,
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||||
|
import { mkdir } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
import { canonicalFrontendFixture as fixture } from './fixtures/canonical.js';
|
import { canonicalFrontendFixture as fixture } from './fixtures/canonical.js';
|
||||||
|
|
||||||
const gamePort = process.env.FRONTEND_PARITY_GAME_PORT ?? '15102';
|
const gamePort = process.env.FRONTEND_PARITY_GAME_PORT ?? '15102';
|
||||||
|
const artifactRoot = process.env.FRONTEND_PARITY_ARTIFACT_DIR;
|
||||||
const response = (data: unknown) => ({ result: { data } });
|
const response = (data: unknown) => ({ result: { data } });
|
||||||
const errorResponse = (path: string, message: string) => ({
|
const errorResponse = (path: string, message: string) => ({
|
||||||
error: {
|
error: {
|
||||||
@@ -42,7 +45,35 @@ const general = {
|
|||||||
const generalContext = {
|
const generalContext = {
|
||||||
general,
|
general,
|
||||||
city: null,
|
city: null,
|
||||||
nation: null,
|
nation: {
|
||||||
|
id: 1,
|
||||||
|
name: '테스트국',
|
||||||
|
color: '#d32f2f',
|
||||||
|
level: 1,
|
||||||
|
levelName: '군벌',
|
||||||
|
gold: 1000,
|
||||||
|
rice: 1000,
|
||||||
|
tech: 1000,
|
||||||
|
typeCode: 'test',
|
||||||
|
typeName: '테스트',
|
||||||
|
typePros: '-',
|
||||||
|
typeCons: '-',
|
||||||
|
capitalCityId: 1,
|
||||||
|
capitalCityName: '낙양',
|
||||||
|
population: { cityCount: 1, current: 10000, max: 20000 },
|
||||||
|
crew: { generalCount: 2, current: 1000, max: 16000 },
|
||||||
|
power: 100,
|
||||||
|
bill: 10,
|
||||||
|
taxRate: 10,
|
||||||
|
strategicCommandLimit: 0,
|
||||||
|
diplomaticLimit: 0,
|
||||||
|
prohibitScout: false,
|
||||||
|
prohibitWar: false,
|
||||||
|
techLevel: 1,
|
||||||
|
techLimited: false,
|
||||||
|
topChiefs: { 12: null, 11: null },
|
||||||
|
impossibleStrategicCommands: [],
|
||||||
|
},
|
||||||
settings: {},
|
settings: {},
|
||||||
penalties: {},
|
penalties: {},
|
||||||
};
|
};
|
||||||
@@ -373,14 +404,25 @@ for (const viewport of [
|
|||||||
boxShadow: getComputedStyle(element).boxShadow,
|
boxShadow: getComputedStyle(element).boxShadow,
|
||||||
}))
|
}))
|
||||||
).toEqual({ outlineWidth: '0px', boxShadow: 'none' });
|
).toEqual({ outlineWidth: '0px', boxShadow: 'none' });
|
||||||
|
|
||||||
|
if (artifactRoot) {
|
||||||
|
await mkdir(artifactRoot, { recursive: true });
|
||||||
|
await page.locator('.MessagePanel').screenshot({
|
||||||
|
path: resolve(artifactRoot, `message-panel-${viewport.width}.png`),
|
||||||
|
animations: 'disabled',
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test('exposes ambassador targets, reply, read, delete, and successful send interactions', async ({ page }) => {
|
test('exposes nation targets including wanderers, reply, read, delete, and successful send interactions', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
const mutations = await installFixture(page, { permission: 4 });
|
const mutations = await installFixture(page, { permission: 4 });
|
||||||
await openMessages(page, { width: 500, height: 900 });
|
await openMessages(page, { width: 500, height: 900 });
|
||||||
|
|
||||||
const select = page.getByLabel('메시지 수신 대상');
|
const select = page.getByLabel('메시지 수신 대상');
|
||||||
|
await expect(select.locator('optgroup[label="외교메시지"] option[value="9000"]')).toHaveText('재야');
|
||||||
await expect(select.locator('option[value="9002"]')).toHaveCount(1);
|
await expect(select.locator('option[value="9002"]')).toHaveCount(1);
|
||||||
await expect(select.locator('option[value="8"]')).toBeDisabled();
|
await expect(select.locator('option[value="8"]')).toBeDisabled();
|
||||||
await expect(select.locator('option[value="9"]')).toBeEnabled();
|
await expect(select.locator('option[value="9"]')).toBeEnabled();
|
||||||
@@ -396,11 +438,21 @@ test('exposes ambassador targets, reply, read, delete, and successful send inter
|
|||||||
await deleteButton.click();
|
await deleteButton.click();
|
||||||
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.delete').length).toBe(1);
|
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.delete').length).toBe(1);
|
||||||
|
|
||||||
await select.selectOption('9999');
|
await select.selectOption('9000');
|
||||||
await page.getByLabel('메시지 입력').fill('전송 성공');
|
await page.getByLabel('메시지 입력').fill('우리 나라로 와주세요');
|
||||||
|
if (artifactRoot) {
|
||||||
|
await mkdir(artifactRoot, { recursive: true });
|
||||||
|
await page.locator('.MessageInputForm').screenshot({
|
||||||
|
path: resolve(artifactRoot, 'wanderer-recruitment-target-500.png'),
|
||||||
|
animations: 'disabled',
|
||||||
|
});
|
||||||
|
}
|
||||||
await page.getByRole('button', { name: '서신전달&갱신' }).click();
|
await page.getByRole('button', { name: '서신전달&갱신' }).click();
|
||||||
await expect(page.getByLabel('메시지 입력')).toHaveValue('');
|
await expect(page.getByLabel('메시지 입력')).toHaveValue('');
|
||||||
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.send').length).toBe(1);
|
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.send').length).toBe(1);
|
||||||
|
expect(JSON.stringify(mutations.find((entry) => entry.operation === 'messages.send')?.body)).toContain(
|
||||||
|
'"mailbox":9000'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('accepts recruitment and declines invader prompts through private-message controls', async ({ page }) => {
|
test('accepts recruitment and declines invader prompts through private-message controls', async ({ page }) => {
|
||||||
@@ -442,6 +494,7 @@ test('redacts diplomacy for a low-permission general and preserves the failed-se
|
|||||||
await openMessages(page, { width: 500, height: 900 });
|
await openMessages(page, { width: 500, height: 900 });
|
||||||
|
|
||||||
const select = page.getByLabel('메시지 수신 대상');
|
const select = page.getByLabel('메시지 수신 대상');
|
||||||
|
await expect(select.locator('option[value="9000"]')).toHaveCount(0);
|
||||||
await expect(select.locator('option[value="9002"]')).toHaveCount(0);
|
await expect(select.locator('option[value="9002"]')).toHaveCount(0);
|
||||||
await expect(page.locator('.DiplomacyTalk')).toContainText('삭제된 메시지입니다');
|
await expect(page.locator('.DiplomacyTalk')).toContainText('삭제된 메시지입니다');
|
||||||
await expect(page.locator('.DiplomacyTalk')).not.toContainText('외교 메시지 본문');
|
await expect(page.locator('.DiplomacyTalk')).not.toContainText('외교 메시지 본문');
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ const measure = async (browser, name, viewport) => {
|
|||||||
await ensureGeneral(page);
|
await ensureGeneral(page);
|
||||||
await page.locator('.MessagePanel').waitFor({ state: 'visible', timeout: 30_000 });
|
await page.locator('.MessagePanel').waitFor({ state: 'visible', timeout: 30_000 });
|
||||||
await page.locator('.BoardHeader').first().waitFor({ state: 'visible' });
|
await page.locator('.BoardHeader').first().waitFor({ state: 'visible' });
|
||||||
|
const mailboxOptions = await page.locator('.MessageInputForm select option').evaluateAll((options) =>
|
||||||
|
options.map((option) => ({
|
||||||
|
value: option.value,
|
||||||
|
label: option.textContent?.trim() ?? '',
|
||||||
|
group: option.parentElement?.tagName === 'OPTGROUP' ? option.parentElement.label : '',
|
||||||
|
disabled: option.disabled,
|
||||||
|
}))
|
||||||
|
);
|
||||||
const marker = `computed-dom-${name}-${Date.now()}`;
|
const marker = `computed-dom-${name}-${Date.now()}`;
|
||||||
await page.locator('.MessageInputForm select').selectOption('9999');
|
await page.locator('.MessageInputForm select').selectOption('9999');
|
||||||
await page.locator('.MessageInputForm input').fill(marker);
|
await page.locator('.MessageInputForm input').fill(marker);
|
||||||
@@ -160,7 +168,11 @@ const measure = async (browser, name, viewport) => {
|
|||||||
page.once('dialog', (dialog) => dialog.accept());
|
page.once('dialog', (dialog) => dialog.accept());
|
||||||
await deleteButton.click();
|
await deleteButton.click();
|
||||||
}
|
}
|
||||||
return { ...result, interaction: { hover, focus } };
|
return {
|
||||||
|
...result,
|
||||||
|
mailboxOptions,
|
||||||
|
interaction: { hover, focus },
|
||||||
|
};
|
||||||
} finally {
|
} finally {
|
||||||
await context.close();
|
await context.close();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user