119 lines
5.3 KiB
TypeScript
119 lines
5.3 KiB
TypeScript
|
|
import { expect, test } from '@playwright/test';
|
||
|
|
|
||
|
|
import {
|
||
|
|
apiAs,
|
||
|
|
createGuild,
|
||
|
|
displayName,
|
||
|
|
dropStaleGuilds,
|
||
|
|
ensureProbe,
|
||
|
|
openApp,
|
||
|
|
openGuild,
|
||
|
|
stepUp,
|
||
|
|
type Account,
|
||
|
|
type ProbeLogin,
|
||
|
|
type Session,
|
||
|
|
} from './support';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Лимит частоты API (AGENT.md 8.6): запрос, получивший 429, клиент повторяет
|
||
|
|
* сам, выждав паузу `retry_after_ms` из ответа сервера. До этой правки раздел
|
||
|
|
* «Участники» под нагрузкой оставался пустым с кнопкой «Повторить» — именно
|
||
|
|
* так падал пункт 3 матрицы 11.6 в финальном прогоне 26.09.
|
||
|
|
*
|
||
|
|
* Проверка живая: запрос участников перехватывается в браузере и дважды
|
||
|
|
* получает 429 с настоящим конвертом ошибки, после чего уходит на стенд.
|
||
|
|
* Раздел обязан показать участника без действий пользователя.
|
||
|
|
*
|
||
|
|
* GLCHAT_E2E_REALTIME=1 GLCHAT_URL=https://gl.mhspx.su \
|
||
|
|
* PLAYWRIGHT_BROWSERS_PATH=../.cache/ms-playwright \
|
||
|
|
* npx playwright test e2e/query-retry.spec.ts
|
||
|
|
*/
|
||
|
|
const enabled = process.env.GLCHAT_E2E_REALTIME === '1';
|
||
|
|
const API = '/api/v1';
|
||
|
|
const PREFIX = 'Retry E2E ';
|
||
|
|
const OWNER_LOGIN: ProbeLogin = { username: 'rtprobe', password: 'Rt-Probe-Owner-9x' };
|
||
|
|
|
||
|
|
let owner: Account;
|
||
|
|
|
||
|
|
test.describe('лимит частоты: клиент повторяет запрос без действий пользователя', () => {
|
||
|
|
test.skip(!enabled, 'нужен развёрнутый инстанс: GLCHAT_E2E_REALTIME=1');
|
||
|
|
|
||
|
|
test.beforeAll(async ({ playwright }) => {
|
||
|
|
const ownerApi = await playwright.request.newContext({ baseURL: process.env.GLCHAT_URL });
|
||
|
|
try {
|
||
|
|
const probe = await ensureProbe(ownerApi, OWNER_LOGIN.username, OWNER_LOGIN.password);
|
||
|
|
const name = await displayName(ownerApi);
|
||
|
|
await stepUp(ownerApi, OWNER_LOGIN.password);
|
||
|
|
await dropStaleGuilds(ownerApi, PREFIX);
|
||
|
|
owner = {
|
||
|
|
...probe,
|
||
|
|
displayName: name,
|
||
|
|
state: await ownerApi.storageState(),
|
||
|
|
browserState: await ownerApi.storageState(),
|
||
|
|
};
|
||
|
|
} finally {
|
||
|
|
await ownerApi.dispose();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('429 на списке участников: раздел заполняется сам', async ({ browser, playwright }) => {
|
||
|
|
const ownerApi = await apiAs(playwright, owner.state);
|
||
|
|
let session: Session | null = null;
|
||
|
|
let guildId = '';
|
||
|
|
|
||
|
|
try {
|
||
|
|
const guild = await createGuild(ownerApi, `${PREFIX}${String(Date.now() % 100_000)}`);
|
||
|
|
guildId = guild.id;
|
||
|
|
session = await openApp(browser, owner.browserState);
|
||
|
|
const page = session.page;
|
||
|
|
|
||
|
|
// Перехват ставим до первого запроса участников: его делает уже список
|
||
|
|
// участников в ленте сервера, и без перехвата данные легли бы в кэш
|
||
|
|
// раньше проверки. Запрос обязан получить 429 дважды и уйти на стенд
|
||
|
|
// только с третьей попытки.
|
||
|
|
let blocked = 0;
|
||
|
|
await page.route('**/api/v1/guilds/*/members*', async (route) => {
|
||
|
|
if (blocked >= 2) {
|
||
|
|
await route.continue();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
blocked += 1;
|
||
|
|
await route.fulfill({
|
||
|
|
status: 429,
|
||
|
|
contentType: 'application/json',
|
||
|
|
body: JSON.stringify({
|
||
|
|
error: {
|
||
|
|
code: 'rate_limited',
|
||
|
|
message: 'Слишком много запросов. Подождите немного.',
|
||
|
|
retry_after_ms: 400,
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Открываем настройки сервера так же, как пользователь: выбрать сервер в
|
||
|
|
// рейке и нажать ⚙. Адресный переход `/settings/servers/<id>` здесь не
|
||
|
|
// годится: он на мгновение монтирует вкладку «Мои серверы», та заводит
|
||
|
|
// запрос участников по тому же ключу и, размонтируясь, обрывает его
|
||
|
|
// повтор — запрос остаётся в состоянии ошибки.
|
||
|
|
await openGuild(page, guild.name);
|
||
|
|
await page.getByRole('button', { name: 'Настройки сервера' }).click();
|
||
|
|
await expect(page.getByTestId('settings-content')).toBeVisible({ timeout: 30_000 });
|
||
|
|
|
||
|
|
// Кнопка «Повторить» не нажимается: строку даёт автоматический повтор.
|
||
|
|
const row = page
|
||
|
|
.getByTestId('guild-members')
|
||
|
|
.locator('li', { hasText: `@${owner.username}` });
|
||
|
|
await expect(row).toContainText(owner.displayName, { timeout: 30_000 });
|
||
|
|
expect(blocked, 'оба ответа 429 были отданы перехватчиком').toBe(2);
|
||
|
|
} finally {
|
||
|
|
await session?.context.close();
|
||
|
|
if (guildId !== '') {
|
||
|
|
await stepUp(ownerApi, OWNER_LOGIN.password).catch(() => undefined);
|
||
|
|
await ownerApi.delete(`${API}/guilds/${guildId}`).catch(() => undefined);
|
||
|
|
}
|
||
|
|
await ownerApi.dispose();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|