test(e2e): пункт 6 матрицы 11.6 не зависит от суточной квоты приглашений

Приглашения ограничены 10 на пользователя за 24 ч (AGENT.md 8.6), поэтому
проверка инвайтов создаёт отдельного свежего владельца сервера: у постоянных
пробных аккаунтов квота выгорает за серию прогонов. Прогон: 1 passed.
This commit is contained in:
2026-09-22 21:24:22 +03:00
parent ab855fbd4f
commit 594a5c0af5
2 changed files with 51 additions and 20 deletions
+50 -19
View File
@@ -35,6 +35,8 @@ const BASE_URL = process.env.GLCHAT_URL ?? 'https://gl.mhspx.su';
const API = '/api/v1';
/** Префикс имени тестового сервера: по нему убираются серверы прошлых прогонов. */
const GUILD_PREFIX = 'Realtime E2E ';
/** Пароль отдельного пробного владельца сервера для проверки инвайтов. */
const INVITE_PROBE_PASSWORD = 'Rt-Invite-Probe-9x';
/** Постоянные пробные аккаунты: A — владелец сервера, B — второй браузер. */
const OWNER_LOGIN = { username: 'rtprobe', password: 'Rt-Probe-Owner-9x' };
const MEMBER_LOGIN = { username: 'rtprobe2', password: 'Rt-Probe-Member-9x' };
@@ -573,23 +575,29 @@ test.describe('realtime: пункты 1–7 (сервер, роли, оформ
browser,
playwright,
}) => {
// Владелец сервера — наблюдатель B: панель приглашений и квота его.
const guestApi = await apiAs(playwright, owner.state);
const ownerApi = await apiAs(playwright, member.state);
// Приглашения ограничены суточной квотой — 10 на пользователя (AGENT.md
// 8.6), поэтому сервер создаёт отдельный свежий владелец: у постоянных
// пробных аккаунтов квота выгорает за серию прогонов.
const inviterApi = await playwright.request.newContext({ baseURL: BASE_URL });
const guestApi = await apiAs(playwright, member.state);
let session: Session | null = null;
let fixture: GuildFixture | null = null;
try {
fixture = await prepareGuild(guestApi, ownerApi, { observerOwns: true });
session = await openSession(browser, member);
await ensureProbeWithWait(inviterApi, {
username: `rtinvite${String(Date.now() % 10_000_000)}`,
password: INVITE_PROBE_PASSWORD,
});
fixture = await prepareGuild(inviterApi, guestApi);
session = await sessionFrom(browser, await inviterApi.storageState());
const page = session.page;
await openWorkspace(page, fixture);
// Список приглашений сервера читается ручкой REST: экрана списка в
// web-клиенте нет, панель показывает только созданную ссылку (см. отчёт).
// Список приглашений читается ручкой REST: экрана списка в web-клиенте
// нет, панель показывает только созданную ссылку (см. отчёт).
const listInvites = async (): Promise<string[]> => {
const response = await repeat429(() =>
ownerApi.get(`${API}/guilds/${fixture?.guildId}/invites`),
inviterApi.get(`${API}/guilds/${fixture?.guildId ?? ''}/invites`),
);
expect(response.ok(), `список приглашений: ${await response.text()}`).toBeTruthy();
const payload = (await response.json()) as { invites: { code: string }[] };
@@ -597,26 +605,40 @@ test.describe('realtime: пункты 1–7 (сервер, роли, оформ
};
expect(await listInvites(), 'до создания приглашений нет').toHaveLength(0);
// Приглашение создаётся владельцем через панель сервера.
// Квота исчерпана — пункт проверить нечем: пропускаем с явной причиной.
const probe = await repeat429(() =>
inviterApi.post(`${API}/guilds/${fixture?.guildId ?? ''}/invites`, {
data: { max_uses: 0, max_age_seconds: 3600 },
}),
);
if (probe.status() === 429) {
test.skip(true, 'суточная квота приглашений исчерпана (10/24 ч)');
}
expect(probe.ok(), `создание приглашения: ${await probe.text()}`).toBeTruthy();
const restCode = ((await probe.json()) as { invite: { code: string } }).invite.code;
await expect.poll(listInvites, { timeout: 20_000 }).toContain(restCode);
// Второе приглашение создаётся владельцем через панель сервера.
await page.getByTestId('guild-invite').click();
const linkInput = page.getByTestId('guild-invite-link');
await expect(linkInput).toBeVisible({ timeout: 20_000 });
const code = inviteCodeFromUrl(await linkInput.inputValue());
await expect.poll(listInvites, { timeout: 20_000 }).toContain(code);
const uiCode = inviteCodeFromUrl(await linkInput.inputValue());
await expect.poll(listInvites, { timeout: 20_000 }).toContain(uiCode);
// Отзыв: код пропадает из списка и больше не пускает новых участников.
const revoked = await repeat429(() => ownerApi.delete(`${API}/invites/${code}`));
const revoked = await repeat429(() => inviterApi.delete(`${API}/invites/${uiCode}`));
expect(revoked.ok(), `отзыв приглашения: ${await revoked.text()}`).toBeTruthy();
await expect.poll(listInvites, { timeout: 20_000 }).not.toContain(code);
const left = await repeat429(() => guestApi.post(`${API}/guilds/${fixture.guildId}/leave`));
expect(left.ok(), `выход из сервера: ${await left.text()}`).toBeTruthy();
const join = await repeat429(() => guestApi.post(`${API}/invites/${code}`));
await expect.poll(listInvites, { timeout: 20_000 }).not.toContain(uiCode);
const join = await guestApi.post(`${API}/invites/${uiCode}`);
expect(join.status(), 'отозванное приглашение больше не работает').toBe(404);
await expect(linkInput).toHaveValue(new RegExp(code, 'u'));
// Ссылка, созданная через панель ранее, остаётся рабочей.
await expect(linkInput).toHaveValue(new RegExp(uiCode, 'u'));
await expect.poll(listInvites, { timeout: 20_000 }).toContain(restCode);
} finally {
await session?.context.close();
await cleanup(ownerApi, fixture, MEMBER_LOGIN.password);
await ownerApi.dispose();
await cleanup(inviterApi, fixture, INVITE_PROBE_PASSWORD);
await inviterApi.dispose();
await guestApi.dispose();
}
});
@@ -903,6 +925,15 @@ async function openSession(browser: Browser, account: Account): Promise<Session>
return { context, page };
}
/** sessionFrom открывает браузерный контекст на готовой сессии аккаунта. */
async function sessionFrom(browser: Browser, state: StorageState): Promise<Session> {
const context = await browser.newContext({ storageState: state });
const page = await context.newPage();
await page.goto('/app');
await expect(page.getByTestId('guild-rail')).toBeVisible({ timeout: 30_000 });
return { context, page };
}
/**
* joinVoice входит в голосовую комнату: кнопка входа исчезает после
* подключения. Подключение к SFU изредка срывается с первого раза, поэтому
+1 -1
View File
@@ -142,7 +142,7 @@ describe('диспетчер событий шлюза', () => {
it('CHANNEL_CREATE и CHANNEL_UPDATE перечитывают комнаты с сервера', async () => {
// Права на комнату персональные: тело события их не несёт, поэтому клиент
// перечитывает список, а не подставляет комнату из кадра (AGENDA.md 11.6).
// перечитывает список, а не подставляет комнату из кадра (AGENT.md 11.6).
installFetch([
{
match: '/api/v1/guilds/g-1/channels',