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 });
|
await expect(memberRow).toHaveCount(0, { timeout: 20_000 });
|
||||||
|
|
||||||
// Возврат в сервер: он на время открывается — приглашения тратят
|
// Возврат в сервер: он на время открывается — приглашения тратят
|
||||||
// суточную квоту 10/24ч (AGENT.md 8.6). Участник снова в составе.
|
// суточную квоту 10/24ч (AGENT.md 8.6). Участник снова в составе, и
|
||||||
// Плитка сервера в рейке B при этом не возвращается без перезагрузки:
|
// плитка сервера возвращается в рейку B без перезагрузки: по GUILD_CREATE
|
||||||
// по GUILD_CREATE клиент берёт список через fetchQuery и получает свежий
|
// клиент перечитывает список серверов (web/src/stores/gateway.ts,
|
||||||
// кэш myGuilds без нового сервера (web/src/stores/gateway.ts,
|
// addGuildFromRest), а не берёт устаревший кэш myGuilds.
|
||||||
// addGuildFromRest) — дефект описан в отчёте, поэтому состав проверяем
|
|
||||||
// по ответу сервера.
|
|
||||||
await setPublic(ownerApi, fixture.guildId, true);
|
await setPublic(ownerApi, fixture.guildId, true);
|
||||||
const rejoin = await memberApi.post(`${API}/guilds/${fixture.guildId}/join`);
|
const rejoin = await memberApi.post(`${API}/guilds/${fixture.guildId}/join`);
|
||||||
expect(rejoin.ok(), `возврат в сервер: ${await rejoin.text()}`).toBeTruthy();
|
expect(rejoin.ok(), `возврат в сервер: ${await rejoin.text()}`).toBeTruthy();
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => guildMemberIds(ownerApi, fixture?.guildId ?? ''), { timeout: 20_000 })
|
.poll(async () => guildMemberIds(ownerApi, fixture?.guildId ?? ''), { timeout: 20_000 })
|
||||||
.toContain(member.id);
|
.toContain(member.id);
|
||||||
|
await expect(
|
||||||
|
memberSession.page.getByRole('link', {
|
||||||
|
name: `Открыть сервер ${fixture.guildName}`,
|
||||||
|
exact: true,
|
||||||
|
}),
|
||||||
|
).toBeVisible({ timeout: 20_000 });
|
||||||
await setPublic(ownerApi, fixture.guildId, false);
|
await setPublic(ownerApi, fixture.guildId, false);
|
||||||
|
|
||||||
// Бан: участник снова исключён, попал в список банов и не может вернуться
|
// Бан: участник снова исключён, попал в список банов и не может вернуться
|
||||||
@@ -610,25 +614,17 @@ test.describe('realtime: пункты 8–13 (чат, состояния, мод
|
|||||||
// Тест идёт последним: logout-all и смена пароля отзывают подготовленные
|
// Тест идёт последним: 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 и
|
// сервер шлёт INVALID_SESSION, клиент сразу уходит на экран входа, даже
|
||||||
// уводит устройство на экран входа (AGENT.md 11.6).
|
// когда открыт сервер (AGENT.md 11.6).
|
||||||
const ownerApi = await apiAs(playwright, owner.state);
|
const ownerApi = await apiAs(playwright, owner.state);
|
||||||
const memberApi = await apiAs(playwright, member.state);
|
const memberApi = await apiAs(playwright, member.state);
|
||||||
const sessions: Session[] = [];
|
const sessions: Session[] = [];
|
||||||
let fixture: GuildFixture | null = null;
|
let fixture: GuildFixture | null = null;
|
||||||
|
// Устройство, сменившее пароль: с него возвращаем исходный в finally, чтобы
|
||||||
|
// падение теста не оставило постоянный пробный аккаунт с временным паролем.
|
||||||
|
let passwordDevice: Page | null = null;
|
||||||
|
let passwordChanged = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fixture = await prepareGuild(ownerApi, memberApi);
|
fixture = await prepareGuild(ownerApi, memberApi);
|
||||||
@@ -655,29 +651,23 @@ test.describe('realtime: пункты 8–13 (чат, состояния, мод
|
|||||||
sessions.push(third, fourth);
|
sessions.push(third, fourth);
|
||||||
await openWorkspace(third.page, fixture);
|
await openWorkspace(third.page, fixture);
|
||||||
await openWorkspace(fourth.page, fixture);
|
await openWorkspace(fourth.page, fixture);
|
||||||
|
passwordDevice = third.page;
|
||||||
await changePassword(third.page, member.password, MEMBER_TEMP_PASSWORD);
|
await changePassword(third.page, member.password, MEMBER_TEMP_PASSWORD);
|
||||||
|
passwordChanged = true;
|
||||||
const revoked = await fourth.page.evaluate(async () => {
|
const revoked = await fourth.page.evaluate(async () => {
|
||||||
const response = await fetch('/api/v1/users/@me', { credentials: 'same-origin' });
|
const response = await fetch('/api/v1/users/@me', { credentials: 'same-origin' });
|
||||||
return response.status;
|
return response.status;
|
||||||
});
|
});
|
||||||
expect(revoked, 'сессия второго устройства отозвана сменой пароля').toBe(401);
|
expect(revoked, 'сессия второго устройства отозвана сменой пароля').toBe(401);
|
||||||
await expect(fourth.page).toHaveURL(/\/login/u, { timeout: 30_000 });
|
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 {
|
} 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) {
|
for (const device of sessions) {
|
||||||
await device.context.close();
|
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 });
|
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 открывает браузерный контекст на готовой сессии аккаунта. */
|
/** sessionFrom открывает браузерный контекст на готовой сессии аккаунта. */
|
||||||
async function sessionFrom(browser: Browser, state: StorageState): Promise<Session> {
|
async function sessionFrom(browser: Browser, state: StorageState): Promise<Session> {
|
||||||
const context = await browser.newContext({ storageState: state });
|
const context = await browser.newContext({ storageState: state });
|
||||||
|
|||||||
Reference in New Issue
Block a user