fix(gateway): READY отдаёт права участника вместо пустого списка

Снапшот READY инициализировал my_permissions пустым списком и нигде его не
заполнял, поэтому у всех — включая владельца сервера — клиент выключал
композер («В эту комнату нельзя писать»), прятал шестерёнку, приглашения и
кнопку саундборда: состав прав в снапшоте не совпадал с REST
(GET /users/@me/guilds отдавал полный список).

Права считает тот же движок и тот же кэш, что REST (permissions.Calculator,
переданный в NewSnapshot), а инстанс-админ получает максимальный набор даже
на чужом сервере, где он не участник (AGENT.md 7.19, 11.6).

Тесты: TestReadyCarriesMyPermissions (непустые права роли по умолчанию,
VIEW_CHANNEL/SEND_MESSAGES и согласие с can_send комнаты) и
TestReadyGivesInstanceAdminAllPermissions.
This commit is contained in:
2026-09-22 21:52:09 +03:00
parent cffcec72b8
commit a6cec5919a
2 changed files with 79 additions and 1 deletions
+66
View File
@@ -6,6 +6,7 @@ import (
"log/slog"
"net/http/httptest"
"path/filepath"
"slices"
"strings"
"testing"
"time"
@@ -214,6 +215,71 @@ func TestHelloAndReady(t *testing.T) {
}
}
// TestReadyCarriesMyPermissions проверяет, что снапшот несёт права участника:
// пустой `my_permissions` выключал композер и управление сервером у всех,
// включая владельца (AGENT.md 11.6).
func TestReadyCarriesMyPermissions(t *testing.T) {
f := newFixture(t)
token := registerUser(t, f, "perm_user", "perm@example.com")
joinMainGuildForTest(t, f, token)
conn, _ := f.dial(t)
send(t, conn, gateway.OpIdentify, map[string]any{"token": token})
var snapshot gateway.Ready
if err := json.Unmarshal(readEnvelope(t, conn).D, &snapshot); err != nil {
t.Fatalf("decode READY: %v", err)
}
perms := snapshot.Guilds[0].MyPerms
if len(perms) == 0 {
t.Fatal("READY must carry my_permissions of the default role")
}
// Роль @user даёт эти права каждому участнику (AGENT.md 6.3).
for _, want := range []string{"VIEW_CHANNEL", "SEND_MESSAGES"} {
if !slices.Contains(perms, want) {
t.Fatalf("my_permissions = %v, want %s", perms, want)
}
}
if !snapshot.Guilds[0].Channels[0].CanSend {
t.Fatal("my_permissions must agree with channel can_send")
}
}
// TestReadyGivesInstanceAdminAllPermissions: у инстанс-админа права
// максимальные даже на чужом сервере, где он не участник (AGENT.md 7.19).
func TestReadyGivesInstanceAdminAllPermissions(t *testing.T) {
f := newFixture(t)
ctx := context.Background()
token := registerUser(t, f, "admin_user", "admin@example.com")
// Инстанс-админ — участник главного сервера: список серверов берётся из
// членства, а права на них максимальные.
joinMainGuildForTest(t, f, token)
user, _, err := f.auth.ResolveSession(ctx, token)
if err != nil {
t.Fatalf("ResolveSession: %v", err)
}
if err := f.store.SetInstanceAdmin(ctx, user.ID, true); err != nil {
t.Fatalf("SetInstanceAdmin: %v", err)
}
conn, _ := f.dial(t)
send(t, conn, gateway.OpIdentify, map[string]any{"token": token})
var snapshot gateway.Ready
if err := json.Unmarshal(readEnvelope(t, conn).D, &snapshot); err != nil {
t.Fatalf("decode READY: %v", err)
}
if len(snapshot.Guilds) != 1 {
t.Fatalf("instance admin must see the guild, got %d", len(snapshot.Guilds))
}
perms := snapshot.Guilds[0].MyPerms
for _, want := range []string{"ADMINISTRATOR", "SEND_MESSAGES", "MANAGE_ROLES", "BAN_MEMBERS"} {
if !slices.Contains(perms, want) {
t.Fatalf("my_permissions инстанс-админа = %v, want %s", perms, want)
}
}
}
func TestReadyHidesPrivateChannels(t *testing.T) {
f := newFixture(t)
ctx := context.Background()