fix(realtime): события ролей, комнат и отзыв сессий без перезагрузки
Дефекты, найденные e2e-матрицей AGENT.md 11.6 (пункты 1, 2, 7, 12):
- ROLE_CREATE/ROLE_UPDATE уходили телом роли без guild_id, а клиент ждал
ссылку {guild_id, role_id} — событие отбрасывалось, роли у второго
участника не обновлялись. В rolePayload добавлено guild_id, parseRoleEvent
принимает и тело роли, и старую ссылку;
- выдача роли (MEMBER_UPDATE) и правки ролей не перечитывали список комнат,
поэтому приватная комната не появлялась в сайдбаре без F5 — теперь клиент
перечитывает комнаты по этим событиям;
- CHANNEL_CREATE/CHANNEL_UPDATE уходили телом комнаты с нулевым can_view
(права персональные), и клиент прятал комнату по этому признаку. Сервер
шлёт тело события без вычисленных прав плюс GUILD_CHANNELS_SYNC, а клиент
по событию комнаты перечитывает список;
- отзыв сессий (logout-all, смена пароля, админский сброс и «выйти везде»,
бан, удаление) не доходил до Gateway: второе устройство оставалось в
приложении. Добавлен gateway.InvalidateUser — клиент получает
INVALID_SESSION и закрывает соединение; клиент дополнительно обрабатывает
событие SESSION_INVALIDATED.
Тесты: gateway (отзыв сессий закрывает соединения и не задевает чужого),
web (событие SESSION_INVALIDATED, перечитывание комнат по CHANNEL_CREATE).
This commit is contained in:
@@ -404,3 +404,42 @@ func TestResumeReplaysMissedEvents(t *testing.T) {
|
||||
t.Fatalf("stale resume = %q, want full READY", full.T)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvalidateUserClosesConnections проверяет отзыв сессий: после
|
||||
// logout-all, смены пароля или действий администратора соединения клиента
|
||||
// должны закрыться с INVALID_SESSION, а не ждать следующего запроса
|
||||
// (AGENT.md 11.6). Отдельно проверяется, что чужого пользователя это не задевает.
|
||||
func TestInvalidateUserClosesConnections(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
token := registerUser(t, f, "invalidate_user", "invalidate@example.com")
|
||||
otherToken := registerUser(t, f, "invalidate_other", "invalidate-other@example.com")
|
||||
|
||||
conn, _ := f.dial(t)
|
||||
send(t, conn, gateway.OpIdentify, map[string]any{"token": token})
|
||||
if ready := readEnvelope(t, conn); ready.T != "READY" {
|
||||
t.Fatalf("expected READY, got %q", ready.T)
|
||||
}
|
||||
|
||||
otherConn, _ := f.dial(t)
|
||||
send(t, otherConn, gateway.OpIdentify, map[string]any{"token": otherToken})
|
||||
if ready := readEnvelope(t, otherConn); ready.T != "READY" {
|
||||
t.Fatalf("expected READY for the other client, got %q", ready.T)
|
||||
}
|
||||
|
||||
user, err := f.auth.UserByEmail(context.Background(), "invalidate@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("UserByEmail: %v", err)
|
||||
}
|
||||
f.service.InvalidateUser(user.ID, "logout_all")
|
||||
|
||||
invalid := readEnvelope(t, conn)
|
||||
if invalid.Op != gateway.OpInvalidSess {
|
||||
t.Fatalf("op = %d, want INVALID_SESSION (%d)", invalid.Op, gateway.OpInvalidSess)
|
||||
}
|
||||
|
||||
// Второй пользователь продолжает работать: heartbeat отвечает.
|
||||
send(t, otherConn, gateway.OpHeartbeat, nil)
|
||||
if ack := readEnvelope(t, otherConn); ack.Op != gateway.OpHeartbeatAck {
|
||||
t.Fatalf("other client op = %d, want HEARTBEAT_ACK", ack.Op)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user