test(e2e): матрица 11.6 — пункт 12 без fixme и возврат сервера в пункте 11
Пункт 12 включён: клиент уходит на экран входа при отзыве сессий, в том числе когда открыт сервер (дефект исправлен в751ecc6), а смена пароля на сервере больше не рвёт соединение текущего устройства (05409d0). Пункт 11 теперь проверяет то, ради чего он написан: после кика и повторного входа плитка сервера возвращается в рейку без перезагрузки (правка клиента вea1b4a7). Возврат пароля пробного участника перенесён в finally: падение теста не оставляет постоянный аккаунт с временным паролем, а неудачный возврат сообщается явной проверкой. Прогон: 13 passed (2,9 мин), лог — build/e2e-realtime-run4.log.
This commit is contained in:
@@ -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<number> {
|
||||
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<Session> {
|
||||
const context = await browser.newContext({ storageState: state });
|
||||
|
||||
Reference in New Issue
Block a user