188 lines
7.0 KiB
Go
188 lines
7.0 KiB
Go
|
|
package server
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"mime/multipart"
|
|||
|
|
"net/http"
|
|||
|
|
"net/http/httptest"
|
|||
|
|
"testing"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Тесты оформления сервера (AGENT.md 7.4): иконка, баннер и splash доступны
|
|||
|
|
// только с MANAGE_GUILD, принимают изображения и удаляют предыдущий файл.
|
|||
|
|
|
|||
|
|
// pngBytes — минимальный PNG: сигнатура и служебный блок (для DetectContentType).
|
|||
|
|
func pngBytes() []byte {
|
|||
|
|
return []byte{
|
|||
|
|
0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a,
|
|||
|
|
0x00, 0x00, 0x00, 0x0d, 'I', 'H', 'D', 'R',
|
|||
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
|||
|
|
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// uploadAppearance отправляет multipart-запрос загрузки оформления.
|
|||
|
|
func uploadAppearance(t *testing.T, srv *Server, guildID, kind, filename string, content []byte, cookie *http.Cookie) *httptest.ResponseRecorder {
|
|||
|
|
t.Helper()
|
|||
|
|
var body bytes.Buffer
|
|||
|
|
writer := multipart.NewWriter(&body)
|
|||
|
|
part, err := writer.CreateFormFile("file", filename)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("multipart: %v", err)
|
|||
|
|
}
|
|||
|
|
if _, err := part.Write(content); err != nil {
|
|||
|
|
t.Fatalf("multipart write: %v", err)
|
|||
|
|
}
|
|||
|
|
if err := writer.Close(); err != nil {
|
|||
|
|
t.Fatalf("multipart close: %v", err)
|
|||
|
|
}
|
|||
|
|
rec := httptest.NewRecorder()
|
|||
|
|
request := httptest.NewRequestWithContext(t.Context(), http.MethodPost,
|
|||
|
|
"/api/v1/guilds/"+guildID+"/appearance/"+kind, &body)
|
|||
|
|
request.Header.Set("Content-Type", writer.FormDataContentType())
|
|||
|
|
if cookie != nil {
|
|||
|
|
request.AddCookie(cookie)
|
|||
|
|
}
|
|||
|
|
srv.Handler().ServeHTTP(rec, request)
|
|||
|
|
return rec
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func appearanceGuild(t *testing.T) (srv *Server, owner *http.Cookie, guildID string) {
|
|||
|
|
t.Helper()
|
|||
|
|
srv, _ = newTestServer(t)
|
|||
|
|
owner = registerAndLogin(t, srv, "look_owner", "look-owner@example.com")
|
|||
|
|
created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds", `{"name":"Оформление"}`, owner)
|
|||
|
|
guild := decodeResponse[struct {
|
|||
|
|
Guild struct {
|
|||
|
|
ID string `json:"id"`
|
|||
|
|
} `json:"guild"`
|
|||
|
|
}](t, created)
|
|||
|
|
return srv, owner, guild.Guild.ID
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestGuildAppearanceUploadAndClear(t *testing.T) {
|
|||
|
|
srv, owner, guildID := appearanceGuild(t)
|
|||
|
|
|
|||
|
|
for _, kind := range []string{"icon", "banner", "splash"} {
|
|||
|
|
rec := uploadAppearance(t, srv, guildID, kind, kind+".png", pngBytes(), owner)
|
|||
|
|
if rec.Code != http.StatusOK {
|
|||
|
|
t.Fatalf("%s: статус %d (%s)", kind, rec.Code, rec.Body.String())
|
|||
|
|
}
|
|||
|
|
payload := decodeResponse[struct {
|
|||
|
|
Guild struct {
|
|||
|
|
IconFileID string `json:"icon_file_id"`
|
|||
|
|
BannerFileID string `json:"banner_file_id"`
|
|||
|
|
SplashFileID string `json:"splash_file_id"`
|
|||
|
|
} `json:"guild"`
|
|||
|
|
}](t, rec)
|
|||
|
|
switch kind {
|
|||
|
|
case "icon":
|
|||
|
|
if payload.Guild.IconFileID == "" {
|
|||
|
|
t.Fatalf("иконка не вернулась: %s", rec.Body.String())
|
|||
|
|
}
|
|||
|
|
case "banner":
|
|||
|
|
if payload.Guild.BannerFileID == "" {
|
|||
|
|
t.Fatal("баннер не вернулся")
|
|||
|
|
}
|
|||
|
|
case "splash":
|
|||
|
|
if payload.Guild.SplashFileID == "" {
|
|||
|
|
t.Fatal("splash не вернулся")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Повторная загрузка заменяет файл, а не копит их.
|
|||
|
|
second := uploadAppearance(t, srv, guildID, kind, kind+"2.png", pngBytes(), owner)
|
|||
|
|
if second.Code != http.StatusOK {
|
|||
|
|
t.Fatalf("%s (повтор): статус %d", kind, second.Code)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Удаление очищает поле.
|
|||
|
|
remove := doJSON(t, srv, http.MethodDelete, "/api/v1/guilds/"+guildID+"/appearance/"+kind, "", owner)
|
|||
|
|
if remove.Code != http.StatusOK {
|
|||
|
|
t.Fatalf("%s (удаление): статус %d (%s)", kind, remove.Code, remove.Body.String())
|
|||
|
|
}
|
|||
|
|
cleared := decodeResponse[struct {
|
|||
|
|
Guild struct {
|
|||
|
|
IconFileID string `json:"icon_file_id"`
|
|||
|
|
BannerFileID string `json:"banner_file_id"`
|
|||
|
|
SplashFileID string `json:"splash_file_id"`
|
|||
|
|
} `json:"guild"`
|
|||
|
|
}](t, remove)
|
|||
|
|
if cleared.Guild.IconFileID != "" || cleared.Guild.BannerFileID != "" || cleared.Guild.SplashFileID != "" {
|
|||
|
|
t.Fatalf("%s: после удаления остались файлы: %+v", kind, cleared.Guild)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestGuildAppearanceRequiresPermissionAndImage(t *testing.T) {
|
|||
|
|
srv, owner, guildID := appearanceGuild(t)
|
|||
|
|
member := registerAndLogin(t, srv, "look_member", "look-member@example.com")
|
|||
|
|
// Участник без прав: сервер не скрывает от него существование (404), потому
|
|||
|
|
// что он уже состоит в сервере.
|
|||
|
|
invite := doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+guildID+"/invites",
|
|||
|
|
`{"max_uses":1,"max_age_seconds":600}`, owner)
|
|||
|
|
code := decodeResponse[struct {
|
|||
|
|
Invite struct {
|
|||
|
|
Code string `json:"code"`
|
|||
|
|
} `json:"invite"`
|
|||
|
|
}](t, invite).Invite.Code
|
|||
|
|
if rec := doJSON(t, srv, http.MethodPost, "/api/v1/invites/"+code, "", member); rec.Code != http.StatusOK {
|
|||
|
|
t.Fatalf("вступление участника = %d (%s)", rec.Code, rec.Body.String())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Без MANAGE_GUILD загрузка запрещена.
|
|||
|
|
denied := uploadAppearance(t, srv, guildID, "icon", "icon.png", pngBytes(), member)
|
|||
|
|
if denied.Code != http.StatusForbidden {
|
|||
|
|
t.Fatalf("без прав: статус %d, ожидался 403", denied.Code)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Не изображение отклоняется.
|
|||
|
|
text := uploadAppearance(t, srv, guildID, "icon", "note.txt", []byte("просто текст"), owner)
|
|||
|
|
if text.Code != http.StatusUnprocessableEntity {
|
|||
|
|
t.Fatalf("текстовый файл: статус %d, ожидался 422", text.Code)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Неизвестный вид оформления — 404.
|
|||
|
|
unknown := uploadAppearance(t, srv, guildID, "cover", "cover.png", pngBytes(), owner)
|
|||
|
|
if unknown.Code != http.StatusNotFound {
|
|||
|
|
t.Fatalf("неизвестный вид: статус %d, ожидался 404", unknown.Code)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Без сессии — 401.
|
|||
|
|
anonymous := uploadAppearance(t, srv, guildID, "icon", "icon.png", pngBytes(), nil)
|
|||
|
|
if anonymous.Code != http.StatusUnauthorized {
|
|||
|
|
t.Fatalf("без сессии: статус %d, ожидался 401", anonymous.Code)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestGuildAppearanceAccentColor(t *testing.T) {
|
|||
|
|
srv, owner, guildID := appearanceGuild(t)
|
|||
|
|
|
|||
|
|
updated := doJSON(t, srv, http.MethodPatch, "/api/v1/guilds/"+guildID,
|
|||
|
|
`{"accent_color":13369344}`, owner)
|
|||
|
|
if updated.Code != http.StatusOK {
|
|||
|
|
t.Fatalf("акцент: статус %d (%s)", updated.Code, updated.Body.String())
|
|||
|
|
}
|
|||
|
|
payload := decodeResponse[struct {
|
|||
|
|
Guild struct {
|
|||
|
|
AccentColor int64 `json:"accent_color"`
|
|||
|
|
} `json:"guild"`
|
|||
|
|
}](t, updated)
|
|||
|
|
if payload.Guild.AccentColor != 13369344 {
|
|||
|
|
t.Fatalf("акцент = %d", payload.Guild.AccentColor)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Сброс: цвет возвращается к значению темы.
|
|||
|
|
cleared := doJSON(t, srv, http.MethodPatch, "/api/v1/guilds/"+guildID, `{"accent_color":0}`, owner)
|
|||
|
|
if cleared.Code != http.StatusOK {
|
|||
|
|
t.Fatalf("сброс акцента: статус %d", cleared.Code)
|
|||
|
|
}
|
|||
|
|
if value := decodeResponse[struct {
|
|||
|
|
Guild struct {
|
|||
|
|
AccentColor int64 `json:"accent_color"`
|
|||
|
|
} `json:"guild"`
|
|||
|
|
}](t, cleared).Guild.AccentColor; value != 0 {
|
|||
|
|
t.Fatalf("после сброса акцент = %d", value)
|
|||
|
|
}
|
|||
|
|
}
|