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) } } }