Files

138 lines
5.9 KiB
Go
Raw Permalink Normal View History

package server
import (
"net/http"
"testing"
)
// postMessage отправляет сообщение от имени владельца и возвращает его id.
func postMessage(t *testing.T, f *messagingFixture, content string) string {
t.Helper()
rec := doJSON(t, f.srv, http.MethodPost, "/api/v1/channels/"+f.openChannel+"/messages",
`{"content":"`+content+`"}`, f.ownerCookie)
if rec.Code != http.StatusOK {
t.Fatalf("отправка сообщения = %d, body = %s", rec.Code, rec.Body.String())
}
return decodeResponse[struct {
Message struct {
ID string `json:"id"`
} `json:"message"`
}](t, rec).Message.ID
}
// TestAuditCoversMessageDeleteAndPins проверяет AGENT.md 7.10 и §11.5:
// удаление и закрепление/открепление сообщения попадают в журнал сервера, а
// действия инстанс-админа на чужом сервере помечены actor_instance_admin.
// Правки сообщения в аудит не пишутся: история не хранится (7.6), а действие
// автора не является модерационным.
func TestAuditCoversMessageDeleteAndPins(t *testing.T) {
f := newMessagingFixture(t)
adminCookie := registerAndLogin(t, f.srv, "audit_msg_admin", "audit-msg-admin@example.com")
promoteAdmin(t, f.srv, "audit-msg-admin@example.com")
toDelete := postMessage(t, f, "сообщение под удаление")
toPin := postMessage(t, f, "сообщение под закрепление")
// Инстанс-админ не участник сервера, но права ADMINISTRATOR обходят иерархию.
if rec := doJSON(t, f.srv, http.MethodDelete,
"/api/v1/channels/"+f.openChannel+"/messages/"+toDelete, "", adminCookie); rec.Code != http.StatusOK {
t.Fatalf("удаление сообщения админом = %d, body = %s", rec.Code, rec.Body.String())
}
if rec := doJSON(t, f.srv, http.MethodPut,
"/api/v1/channels/"+f.openChannel+"/pins/"+toPin, "", adminCookie); rec.Code != http.StatusOK {
t.Fatalf("закрепление сообщения админом = %d, body = %s", rec.Code, rec.Body.String())
}
if rec := doJSON(t, f.srv, http.MethodDelete,
"/api/v1/channels/"+f.openChannel+"/pins/"+toPin, "", adminCookie); rec.Code != http.StatusOK {
t.Fatalf("открепление сообщения админом = %d, body = %s", rec.Code, rec.Body.String())
}
entries := auditActions(t, f.srv, f.guildID, f.ownerCookie)
want := map[string]string{
"message.delete": toDelete,
"message.pin": toPin,
"message.unpin": toPin,
}
for action, target := range want {
found := false
unflagged := false
for _, entry := range entries {
if entry.Action != action || entry.TargetID != target {
continue
}
if entry.TargetType != "message" {
t.Fatalf("%s: тип цели = %q, ожидали message", action, entry.TargetType)
}
if entry.ActorInstanceAdmin {
found = true
continue
}
unflagged = true
}
if !found {
if unflagged {
t.Fatalf("%s по цели %s записан без actor_instance_admin: %+v", action, target, entries)
}
t.Fatalf("в журнале нет действия %s по цели %s: %+v", action, target, entries)
}
}
// Действия владельца в журнале есть, но без флага инстанс-админа: пины
// обычного модератора не должны выглядеть как действия администратора.
ownPin := postMessage(t, f, "сообщение владельца")
if rec := doJSON(t, f.srv, http.MethodPut,
"/api/v1/channels/"+f.openChannel+"/pins/"+ownPin, "", f.ownerCookie); rec.Code != http.StatusOK {
t.Fatalf("закрепление сообщения владельцем = %d, body = %s", rec.Code, rec.Body.String())
}
if rec := doJSON(t, f.srv, http.MethodDelete,
"/api/v1/channels/"+f.openChannel+"/messages/"+ownPin, "", f.ownerCookie); rec.Code != http.StatusOK {
t.Fatalf("удаление своего сообщения владельцем = %d, body = %s", rec.Code, rec.Body.String())
}
ownerFlagged := false
for _, entry := range auditActions(t, f.srv, f.guildID, f.ownerCookie) {
if entry.Action == "message.delete" && entry.TargetID == ownPin && !entry.ActorInstanceAdmin {
ownerFlagged = true
}
}
if !ownerFlagged {
t.Fatalf("в журнале нет удаления сообщения владельцем без флага админа")
}
}
// TestAuditSkipsDirectMessages проверяет, что личные беседы не попадают в аудит
// сервера: у них нет сервера, а журнал ведётся по серверам (AGENT.md 7.10).
func TestAuditSkipsDirectMessages(t *testing.T) {
f := newMessagingFixture(t)
dm := doJSON(t, f.srv, http.MethodPost, "/api/v1/users/@me/channels",
`{"recipient_id":"`+f.memberID+`"}`, f.ownerCookie)
dmChannelID := decodeResponse[struct {
Channel struct {
ID string `json:"id"`
} `json:"channel"`
}](t, dm).Channel.ID
if dmChannelID == "" {
t.Fatalf("личная беседа не создана: %s", dm.Body.String())
}
sent := doJSON(t, f.srv, http.MethodPost, "/api/v1/channels/"+dmChannelID+"/messages",
`{"content":"личное"}`, f.ownerCookie)
messageID := decodeResponse[struct {
Message struct {
ID string `json:"id"`
} `json:"message"`
}](t, sent).Message.ID
if rec := doJSON(t, f.srv, http.MethodDelete,
"/api/v1/channels/"+dmChannelID+"/messages/"+messageID, "", f.ownerCookie); rec.Code != http.StatusOK {
t.Fatalf("удаление личного сообщения = %d, body = %s", rec.Code, rec.Body.String())
}
for _, entry := range auditActions(t, f.srv, f.guildID, f.ownerCookie) {
if entry.Action == "message.delete" {
t.Fatalf("личное сообщение попало в аудит сервера: %+v", entry)
}
}
}