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 API = '/api/v1';
/** Префикс имени тестового сервера: по нему убираются серверы прошлых прогонов. */ /** Префикс имени тестового сервера: по нему убираются серверы прошлых прогонов. */
const GUILD_PREFIX = 'Realtime E2E '; const GUILD_PREFIX = 'Realtime E2E ';
/** Пароль отдельного пробного владельца сервера для проверки инвайтов. */
const INVITE_PROBE_PASSWORD = 'Rt-Invite-Probe-9x';
/** Постоянные пробные аккаунты: A — владелец сервера, B — второй браузер. */ /** Постоянные пробные аккаунты: A — владелец сервера, B — второй браузер. */
const OWNER_LOGIN = { username: 'rtprobe', password: 'Rt-Probe-Owner-9x' }; const OWNER_LOGIN = { username: 'rtprobe', password: 'Rt-Probe-Owner-9x' };
const MEMBER_LOGIN = { username: 'rtprobe2', password: 'Rt-Probe-Member-9x' }; const MEMBER_LOGIN = { username: 'rtprobe2', password: 'Rt-Probe-Member-9x' };
@@ -573,23 +575,29 @@ test.describe('realtime: пункты 1–7 (сервер, роли, оформ
browser, browser,
playwright, playwright,
}) => { }) => {
// Владелец сервера — наблюдатель B: панель приглашений и квота его. // Приглашения ограничены суточной квотой — 10 на пользователя (AGENT.md
const guestApi = await apiAs(playwright, owner.state); // 8.6), поэтому сервер создаёт отдельный свежий владелец: у постоянных
const ownerApi = await apiAs(playwright, member.state); // пробных аккаунтов квота выгорает за серию прогонов.
const inviterApi = await playwright.request.newContext({ baseURL: BASE_URL });
const guestApi = await apiAs(playwright, member.state);
let session: Session | null = null; let session: Session | null = null;
let fixture: GuildFixture | null = null; let fixture: GuildFixture | null = null;
try { try {
fixture = await prepareGuild(guestApi, ownerApi, { observerOwns: true }); await ensureProbeWithWait(inviterApi, {
session = await openSession(browser, member); 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; const page = session.page;
await openWorkspace(page, fixture); await openWorkspace(page, fixture);
// Список приглашений сервера читается ручкой REST: экрана списка в // Список приглашений читается ручкой REST: экрана списка в web-клиенте
// web-клиенте нет, панель показывает только созданную ссылку (см. отчёт). // нет, панель показывает только созданную ссылку (см. отчёт).
const listInvites = async (): Promise<string[]> => { const listInvites = async (): Promise<string[]> => {
const response = await repeat429(() => 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(); expect(response.ok(), `список приглашений: ${await response.text()}`).toBeTruthy();
const payload = (await response.json()) as { invites: { code: string }[] }; const payload = (await response.json()) as { invites: { code: string }[] };
@@ -597,26 +605,40 @@ test.describe('realtime: пункты 1–7 (сервер, роли, оформ
}; };
expect(await listInvites(), 'до создания приглашений нет').toHaveLength(0); 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(); await page.getByTestId('guild-invite').click();
const linkInput = page.getByTestId('guild-invite-link'); const linkInput = page.getByTestId('guild-invite-link');
await expect(linkInput).toBeVisible({ timeout: 20_000 }); await expect(linkInput).toBeVisible({ timeout: 20_000 });
const code = inviteCodeFromUrl(await linkInput.inputValue()); const uiCode = inviteCodeFromUrl(await linkInput.inputValue());
await expect.poll(listInvites, { timeout: 20_000 }).toContain(code); 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(); expect(revoked.ok(), `отзыв приглашения: ${await revoked.text()}`).toBeTruthy();
await expect.poll(listInvites, { timeout: 20_000 }).not.toContain(code); await expect.poll(listInvites, { timeout: 20_000 }).not.toContain(uiCode);
const left = await repeat429(() => guestApi.post(`${API}/guilds/${fixture.guildId}/leave`)); const join = await guestApi.post(`${API}/invites/${uiCode}`);
expect(left.ok(), `выход из сервера: ${await left.text()}`).toBeTruthy();
const join = await repeat429(() => guestApi.post(`${API}/invites/${code}`));
expect(join.status(), 'отозванное приглашение больше не работает').toBe(404); 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 { } finally {
await session?.context.close(); await session?.context.close();
await cleanup(ownerApi, fixture, MEMBER_LOGIN.password); await cleanup(inviterApi, fixture, INVITE_PROBE_PASSWORD);
await ownerApi.dispose(); await inviterApi.dispose();
await guestApi.dispose(); await guestApi.dispose();
} }
}); });
@@ -903,6 +925,15 @@ async function openSession(browser: Browser, account: Account): Promise<Session>
return { context, page }; 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 входит в голосовую комнату: кнопка входа исчезает после * joinVoice входит в голосовую комнату: кнопка входа исчезает после
* подключения. Подключение к SFU изредка срывается с первого раза, поэтому * подключения. Подключение к SFU изредка срывается с первого раза, поэтому
+1 -1
View File
@@ -142,7 +142,7 @@ describe('диспетчер событий шлюза', () => {
it('CHANNEL_CREATE и CHANNEL_UPDATE перечитывают комнаты с сервера', async () => { it('CHANNEL_CREATE и CHANNEL_UPDATE перечитывают комнаты с сервера', async () => {
// Права на комнату персональные: тело события их не несёт, поэтому клиент // Права на комнату персональные: тело события их не несёт, поэтому клиент
// перечитывает список, а не подставляет комнату из кадра (AGENDA.md 11.6). // перечитывает список, а не подставляет комнату из кадра (AGENT.md 11.6).
installFetch([ installFetch([
{ {
match: '/api/v1/guilds/g-1/channels', match: '/api/v1/guilds/g-1/channels',