From dcafb100469ee665905df1adcf27e9a5d7f7597f Mon Sep 17 00:00:00 2001 From: grendervill Date: Tue, 22 Sep 2026 22:28:10 +0300 Subject: [PATCH] =?UTF-8?q?test(e2e):=20=D0=BC=D0=B0=D1=82=D1=80=D0=B8?= =?UTF-8?q?=D1=86=D0=B0=2011.6=20=E2=80=94=20=D0=BF=D1=83=D0=BD=D0=BA?= =?UTF-8?q?=D1=82=2012=20=D0=B1=D0=B5=D0=B7=20fixme=20=D0=B8=20=D0=B2?= =?UTF-8?q?=D0=BE=D0=B7=D0=B2=D1=80=D0=B0=D1=82=20=D1=81=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B0=20=D0=B2=20=D0=BF=D1=83=D0=BD=D0=BA=D1=82?= =?UTF-8?q?=D0=B5=2011?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пункт 12 включён: клиент уходит на экран входа при отзыве сессий, в том числе когда открыт сервер (дефект исправлен в 751ecc6), а смена пароля на сервере больше не рвёт соединение текущего устройства (05409d0). Пункт 11 теперь проверяет то, ради чего он написан: после кика и повторного входа плитка сервера возвращается в рейку без перезагрузки (правка клиента в ea1b4a7). Возврат пароля пробного участника перенесён в finally: падение теста не оставляет постоянный аккаунт с временным паролем, а неудачный возврат сообщается явной проверкой. Прогон: 13 passed (2,9 мин), лог — build/e2e-realtime-run4.log. --- web/e2e/realtime-social.spec.ts | 81 +++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/web/e2e/realtime-social.spec.ts b/web/e2e/realtime-social.spec.ts index 64c8a64..be26692 100644 --- a/web/e2e/realtime-social.spec.ts +++ b/web/e2e/realtime-social.spec.ts @@ -472,18 +472,22 @@ test.describe('realtime: пункты 8–13 (чат, состояния, мод await expect(memberRow).toHaveCount(0, { timeout: 20_000 }); // Возврат в сервер: он на время открывается — приглашения тратят - // суточную квоту 10/24ч (AGENT.md 8.6). Участник снова в составе. - // Плитка сервера в рейке B при этом не возвращается без перезагрузки: - // по GUILD_CREATE клиент берёт список через fetchQuery и получает свежий - // кэш myGuilds без нового сервера (web/src/stores/gateway.ts, - // addGuildFromRest) — дефект описан в отчёте, поэтому состав проверяем - // по ответу сервера. + // суточную квоту 10/24ч (AGENT.md 8.6). Участник снова в составе, и + // плитка сервера возвращается в рейку B без перезагрузки: по GUILD_CREATE + // клиент перечитывает список серверов (web/src/stores/gateway.ts, + // addGuildFromRest), а не берёт устаревший кэш myGuilds. await setPublic(ownerApi, fixture.guildId, true); const rejoin = await memberApi.post(`${API}/guilds/${fixture.guildId}/join`); expect(rejoin.ok(), `возврат в сервер: ${await rejoin.text()}`).toBeTruthy(); await expect .poll(async () => guildMemberIds(ownerApi, fixture?.guildId ?? ''), { timeout: 20_000 }) .toContain(member.id); + await expect( + memberSession.page.getByRole('link', { + name: `Открыть сервер ${fixture.guildName}`, + exact: true, + }), + ).toBeVisible({ timeout: 20_000 }); await setPublic(ownerApi, fixture.guildId, false); // Бан: участник снова исключён, попал в список банов и не может вернуться @@ -610,25 +614,17 @@ test.describe('realtime: пункты 8–13 (чат, состояния, мод // Тест идёт последним: logout-all и смена пароля отзывают подготовленные // сессии файла, поэтому остальным тестам они должны остаться живыми. // - // Дефект: сервер отзывает сессии и присылает второму устройству - // {"op":4,"d":{"reason":"password_changed","resumable":false}} + CLOSE 1000 - // (проверено перехватом WS на стенде), но устройство, находящееся внутри - // сервера (/app/<сервер>/<комната>), остаётся на месте: запроса - // GET /users/@me после отзыва не происходит, экран входа не появляется. - // Скрипт build/verify-session-invalidation.mjs показывает уход на /login, - // когда второе устройство находится на /app без открытого сервера, поэтому - // дело в ветке клиента, которая срабатывает при пустом снапшоте серверов. - test.fixme( - true, - 'второе устройство с открытым сервером не уходит на /login после отзыва сессии (профиль не перечитывается)', - ); // Оба действия подтверждаются на втором устройстве без перезагрузки — - // сервер шлёт INVALID_SESSION, клиент перечитывает профиль, получает 401 и - // уводит устройство на экран входа (AGENT.md 11.6). + // сервер шлёт INVALID_SESSION, клиент сразу уходит на экран входа, даже + // когда открыт сервер (AGENT.md 11.6). const ownerApi = await apiAs(playwright, owner.state); const memberApi = await apiAs(playwright, member.state); const sessions: Session[] = []; let fixture: GuildFixture | null = null; + // Устройство, сменившее пароль: с него возвращаем исходный в finally, чтобы + // падение теста не оставило постоянный пробный аккаунт с временным паролем. + let passwordDevice: Page | null = null; + let passwordChanged = false; try { fixture = await prepareGuild(ownerApi, memberApi); @@ -655,29 +651,23 @@ test.describe('realtime: пункты 8–13 (чат, состояния, мод sessions.push(third, fourth); await openWorkspace(third.page, fixture); await openWorkspace(fourth.page, fixture); + passwordDevice = third.page; await changePassword(third.page, member.password, MEMBER_TEMP_PASSWORD); + passwordChanged = true; const revoked = await fourth.page.evaluate(async () => { const response = await fetch('/api/v1/users/@me', { credentials: 'same-origin' }); return response.status; }); expect(revoked, 'сессия второго устройства отозвана сменой пароля').toBe(401); await expect(fourth.page).toHaveURL(/\/login/u, { timeout: 30_000 }); - - // Пароль возвращаем исходный из уцелевшей сессии устройства 3. - const restored = await third.page.evaluate(async (password: string) => { - const response = await fetch('/api/v1/users/@me/password', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - credentials: 'same-origin', - body: JSON.stringify({ - current_password: 'Rt-Probe-Member-Temp-9x', - new_password: password, - }), - }); - return response.status; - }, member.password); - expect(restored, 'пароль пробного аккаунта возвращён').toBe(200); } finally { + if (passwordChanged && passwordDevice !== null) { + const restored = await restoreMemberPassword(passwordDevice, member.password); + expect( + restored, + `пароль пробного аккаунта ${MEMBER_LOGIN.username} возвращён (${restored})`, + ).toBe(200); + } for (const device of sessions) { await device.context.close(); } @@ -938,6 +928,27 @@ async function changePassword(page: Page, current: string, next: string): Promis await expect(content.getByRole('status')).toContainText('Пароль изменён', { timeout: 20_000 }); } +/** + * restoreMemberPassword возвращает пробному участнику исходный пароль с + * устройства, которое его сменило. Возврат идёт из finally: если он не удался, + * постоянный аккаунт останется с временным паролем и следующие прогоны файла + * не смогут войти — поэтому тест обязан сообщить об этом явно. + */ +async function restoreMemberPassword(page: Page, password: string): Promise { + return page.evaluate( + async (payload: { current: string; next: string }) => { + const response = await fetch('/api/v1/users/@me/password', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ current_password: payload.current, new_password: payload.next }), + }); + return response.status; + }, + { current: MEMBER_TEMP_PASSWORD, next: password }, + ); +} + /** sessionFrom открывает браузерный контекст на готовой сессии аккаунта. */ async function sessionFrom(browser: Browser, state: StorageState): Promise { const context = await browser.newContext({ storageState: state });