Files

266 lines
11 KiB
Go
Raw Permalink Normal View History

package server
import (
"net/http"
"testing"
)
// channelIDs забирает видимые комнаты сервера глазами конкретной сессии.
func channelIDs(t *testing.T, srv *Server, cookie *http.Cookie, guildID string) []string {
t.Helper()
rec := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guildID+"/channels", "", cookie)
if rec.Code != http.StatusOK {
t.Fatalf("GET channels = %d, body = %s", rec.Code, rec.Body.String())
}
payload := decodeResponse[struct {
Channels []struct {
ID string `json:"id"`
} `json:"channels"`
}](t, rec)
ids := make([]string, 0, len(payload.Channels))
for _, channel := range payload.Channels {
ids = append(ids, channel.ID)
}
return ids
}
func containsID(ids []string, want string) bool {
for _, id := range ids {
if id == want {
return true
}
}
return false
}
func defaultRoleID(t *testing.T, srv *Server, cookie *http.Cookie, guildID string) string {
t.Helper()
rec := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guildID+"/roles", "", cookie)
if rec.Code != http.StatusOK {
t.Fatalf("GET roles = %d, body = %s", rec.Code, rec.Body.String())
}
payload := decodeResponse[struct {
Roles []struct {
ID string `json:"id"`
IsDefault bool `json:"is_default"`
} `json:"roles"`
}](t, rec)
for _, role := range payload.Roles {
if role.IsDefault {
return role.ID
}
}
t.Fatal("default role not found")
return ""
}
// TestChannelOverwritesHideAndRevealChannel проверяет приватную комнату
// (AGENT.md 6.2, 11.6): запрет @everyone скрывает комнату у участника,
// разрешение роли — открывает, снятие оверрайдов возвращает исходное состояние.
func TestChannelOverwritesHideAndRevealChannel(t *testing.T) {
srv, _ := newTestServer(t)
ownerCookie := registerAndLogin(t, srv, "overwrite_owner", "overwrite-owner@example.com")
memberCookie := registerAndLogin(t, srv, "overwrite_member", "overwrite-member@example.com")
created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds", `{"name":"Приватный"}`, ownerCookie)
guild := decodeResponse[struct {
Guild struct {
ID string `json:"id"`
} `json:"guild"`
}](t, created)
guildID := guild.Guild.ID
channelRec := doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+guildID+"/channels",
`{"name":"тайная","type":"text"}`, ownerCookie)
channel := decodeResponse[struct {
Channel struct {
ID string `json:"id"`
} `json:"channel"`
}](t, channelRec)
channelID := channel.Channel.ID
roleRec := doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+guildID+"/roles",
`{"name":"Доверенные"}`, ownerCookie)
role := decodeResponse[struct {
Role struct {
ID string `json:"id"`
} `json:"role"`
}](t, roleRec)
roleID := role.Role.ID
inviteRec := doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+guildID+"/invites",
`{"max_uses":0,"max_age_seconds":3600}`, ownerCookie)
invite := decodeResponse[struct {
Invite struct {
Code string `json:"code"`
} `json:"invite"`
}](t, inviteRec)
if rec := doJSON(t, srv, http.MethodPost, "/api/v1/invites/"+invite.Invite.Code, "", memberCookie); rec.Code != http.StatusOK {
t.Fatalf("accept invite = %d, body = %s", rec.Code, rec.Body.String())
}
member, err := srv.auth.UserByEmail(t.Context(), "overwrite-member@example.com")
if err != nil {
t.Fatalf("UserByEmail: %v", err)
}
memberPathID := formatSnowflake(member.ID)
// Пока оверрайдов нет, комната видна всем участникам сервера.
if ids := channelIDs(t, srv, memberCookie, guildID); !containsID(ids, channelID) {
t.Fatalf("member must see the channel before overwrites: %v", ids)
}
const viewMask = "VIEW_CHANNEL"
everyoneID := defaultRoleID(t, srv, ownerCookie, guildID)
basePath := "/api/v1/guilds/" + guildID + "/channels/" + channelID + "/overwrites/"
// Участник не управляет правами комнаты.
denied := doJSON(t, srv, http.MethodPut, basePath+"role/"+everyoneID,
`{"deny":"`+viewMask+`","step_up_password":"correct-horse-battery"}`, memberCookie)
if denied.Code != http.StatusForbidden {
t.Fatalf("member overwrite = %d, want 403", denied.Code)
}
// Без подтверждения личности права не меняются.
noStepUp := doJSON(t, srv, http.MethodPut, basePath+"role/"+everyoneID,
`{"deny":"`+viewMask+`"}`, ownerCookie)
if noStepUp.Code != http.StatusForbidden {
t.Fatalf("overwrite without step-up = %d, want 403, body = %s", noStepUp.Code, noStepUp.Body.String())
}
if code := errorCodeOf(t, noStepUp); code != "auth.step_up_required" {
t.Fatalf("overwrite without step-up code = %q", code)
}
// Закрываем комнату для @everyone: участник её больше не видит.
closed := doJSON(t, srv, http.MethodPut, basePath+"role/"+everyoneID,
`{"deny":"`+viewMask+`","step_up_password":"correct-horse-battery"}`, ownerCookie)
if closed.Code != http.StatusOK {
t.Fatalf("close channel = %d, body = %s", closed.Code, closed.Body.String())
}
closedPayload := decodeResponse[struct {
Channel struct {
PermissionOverwrites []struct {
TargetType string `json:"target_type"`
TargetID string `json:"target_id"`
Deny string `json:"deny"`
} `json:"permission_overwrites"`
} `json:"channel"`
}](t, closed)
if len(closedPayload.Channel.PermissionOverwrites) != 1 ||
closedPayload.Channel.PermissionOverwrites[0].Deny != viewMask {
t.Fatalf("overwrites in payload = %+v", closedPayload.Channel.PermissionOverwrites)
}
if ids := channelIDs(t, srv, memberCookie, guildID); containsID(ids, channelID) {
t.Fatalf("private channel must be hidden from member: %v", ids)
}
// Владелец по-прежнему видит комнату (у него ADMINISTRATOR).
if ids := channelIDs(t, srv, ownerCookie, guildID); !containsID(ids, channelID) {
t.Fatalf("owner must still see the channel: %v", ids)
}
// Разрешение роли возвращает доступ: выдаём роль участнику и открываем комнату.
if rec := doJSON(t, srv, http.MethodPut,
"/api/v1/guilds/"+guildID+"/members/"+memberPathID+"/roles/"+roleID, "", ownerCookie); rec.Code != http.StatusOK {
t.Fatalf("assign role = %d, body = %s", rec.Code, rec.Body.String())
}
allow := doJSON(t, srv, http.MethodPut, basePath+"role/"+roleID,
`{"allow":"`+viewMask+`","step_up_password":"correct-horse-battery"}`, ownerCookie)
if allow.Code != http.StatusOK {
t.Fatalf("allow role = %d, body = %s", allow.Code, allow.Body.String())
}
if ids := channelIDs(t, srv, memberCookie, guildID); !containsID(ids, channelID) {
t.Fatalf("role allow must open the channel: %v", ids)
}
// Снятие оверрайда с роли снова закрывает комнату (запрет @everyone остался).
removed := doJSON(t, srv, http.MethodDelete, basePath+"role/"+roleID, "", ownerCookie)
if removed.Code != http.StatusOK {
t.Fatalf("delete role overwrite = %d, body = %s", removed.Code, removed.Body.String())
}
if ids := channelIDs(t, srv, memberCookie, guildID); containsID(ids, channelID) {
t.Fatalf("channel must be hidden again after removing the role overwrite: %v", ids)
}
// Снятие запрета @everyone делает комнату обычной.
if rec := doJSON(t, srv, http.MethodDelete, basePath+"role/"+everyoneID, "", ownerCookie); rec.Code != http.StatusOK {
t.Fatalf("delete everyone overwrite = %d, body = %s", rec.Code, rec.Body.String())
}
if ids := channelIDs(t, srv, memberCookie, guildID); !containsID(ids, channelID) {
t.Fatalf("channel must be visible after clearing overwrites: %v", ids)
}
// Аудит: изменения прав комнаты записаны.
audit := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guildID+"/audit-log", "", ownerCookie)
entries := decodeResponse[struct {
Entries []struct {
Action string `json:"action"`
TargetType string `json:"target_type"`
} `json:"entries"`
}](t, audit)
if !hasAction(entries.Entries, "channel.overwrite_set") || !hasAction(entries.Entries, "channel.overwrite_delete") {
t.Fatalf("audit has no overwrite entries: %+v", entries.Entries)
}
}
// TestChannelOverwriteTargetValidation проверяет отказ на чужую цель.
func TestChannelOverwriteTargetValidation(t *testing.T) {
srv, _ := newTestServer(t)
ownerCookie := registerAndLogin(t, srv, "target_owner", "target-owner@example.com")
registerAndLogin(t, srv, "target_outsider", "target-outsider@example.com")
created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds", `{"name":"Цели"}`, ownerCookie)
guild := decodeResponse[struct {
Guild struct {
ID string `json:"id"`
} `json:"guild"`
}](t, created)
channelRec := doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+guild.Guild.ID+"/channels",
`{"name":"общий","type":"text"}`, ownerCookie)
channel := decodeResponse[struct {
Channel struct {
ID string `json:"id"`
} `json:"channel"`
}](t, channelRec)
outsider, err := srv.auth.UserByEmail(t.Context(), "target-outsider@example.com")
if err != nil {
t.Fatalf("UserByEmail: %v", err)
}
base := "/api/v1/guilds/" + guild.Guild.ID + "/channels/" + channel.Channel.ID + "/overwrites/"
// Пользователь не состоит в сервере.
notMember := doJSON(t, srv, http.MethodPut,
base+"user/"+formatSnowflake(outsider.ID),
`{"allow":"VIEW_CHANNEL","step_up_password":"correct-horse-battery"}`, ownerCookie)
if notMember.Code != http.StatusNotFound {
t.Fatalf("overwrite for non-member = %d, want 404, body = %s", notMember.Code, notMember.Body.String())
}
// Роль из другого сервера.
otherGuild := doJSON(t, srv, http.MethodPost, "/api/v1/guilds", `{"name":"Другой"}`, ownerCookie)
other := decodeResponse[struct {
Guild struct {
ID string `json:"id"`
} `json:"guild"`
}](t, otherGuild)
otherRole := doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+other.Guild.ID+"/roles",
`{"name":"Чужая"}`, ownerCookie)
role := decodeResponse[struct {
Role struct {
ID string `json:"id"`
} `json:"role"`
}](t, otherRole)
foreign := doJSON(t, srv, http.MethodPut, base+"role/"+role.Role.ID,
`{"allow":"VIEW_CHANNEL","step_up_password":"correct-horse-battery"}`, ownerCookie)
if foreign.Code != http.StatusNotFound {
t.Fatalf("overwrite for foreign role = %d, want 404, body = %s", foreign.Code, foreign.Body.String())
}
// Неизвестный тип цели — 422 (huma отклоняет значение вне enum).
badType := doJSON(t, srv, http.MethodPut, base+"everyone/123",
`{"allow":"VIEW_CHANNEL","step_up_password":"correct-horse-battery"}`, ownerCookie)
if badType.Code != http.StatusUnprocessableEntity && badType.Code != http.StatusBadRequest {
t.Fatalf("overwrite with bad target type = %d, want 422, body = %s", badType.Code, badType.Body.String())
}
}