fix(invites): картинки оформления отдаются без сессии для страницы приглашения

- GET /files/{id} без сессии отдаёт только оформление сервера и картинку
  приглашения (guild_icon, guild_banner, guild_splash, invite_background):
  их показывают до входа (AGENT.md 7.9), остальное по-прежнему требует сессии
- optionalUser резолвит сессию без ответа об ошибке; путь файла проверяется на
  принадлежность каталогу данных (AGENT.md 9.2)
- аватар пригласившего показывается только вошедшим
- тесты: Go (splash анонимно, вложение закрыто) и правки живого скрипта
This commit is contained in:
2026-09-21 19:30:25 +03:00
parent 566e1cf981
commit 72537e9ff9
4 changed files with 185 additions and 17 deletions
+88
View File
@@ -697,3 +697,91 @@ func TestInviteBackgroundAndPreview(t *testing.T) {
t.Fatalf("переопределение не снято: %s", cleared.Body.String())
}
}
func TestPresentationFilesArePublic(t *testing.T) {
srv, owner, _, guildID, _ := cosmeticsFixture(t)
// Оформление сервера: splash загружается как обычно.
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
part, err := writer.CreateFormFile("file", "splash.png")
if err != nil {
t.Fatalf("multipart: %v", err)
}
if _, err := part.Write(pngBytes()); err != nil {
t.Fatalf("multipart write: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("multipart close: %v", err)
}
request := httptest.NewRequestWithContext(t.Context(), http.MethodPost,
"/api/v1/guilds/"+guildID+"/appearance/splash", &buf)
request.Header.Set("Content-Type", writer.FormDataContentType())
request.AddCookie(owner)
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, request)
if rec.Code != http.StatusOK {
t.Fatalf("загрузка splash = %d (%s)", rec.Code, rec.Body.String())
}
splashID := decodeResponse[struct {
Guild struct {
SplashFileID string `json:"splash_file_id"`
} `json:"guild"`
}](t, rec).Guild.SplashFileID
// Без сессии картинка оформления отдаётся: её показывают до входа.
anonymous := doJSON(t, srv, http.MethodGet, "/files/"+splashID, "")
if anonymous.Code != http.StatusOK {
t.Fatalf("splash без сессии = %d, ожидался 200", anonymous.Code)
}
// Обычное вложение без сессии по-прежнему закрыто.
var fileBuf bytes.Buffer
fileWriter := multipart.NewWriter(&fileBuf)
filePart, err := fileWriter.CreateFormFile("file", "secret.txt")
if err != nil {
t.Fatalf("multipart: %v", err)
}
if _, err := filePart.Write([]byte("секрет")); err != nil {
t.Fatalf("multipart write: %v", err)
}
if err := fileWriter.Close(); err != nil {
t.Fatalf("multipart close: %v", err)
}
uploadReq := httptest.NewRequestWithContext(t.Context(), http.MethodPost,
"/api/v1/channels/"+guildChannelID(t, srv, guildID, owner)+"/files", &fileBuf)
uploadReq.Header.Set("Content-Type", fileWriter.FormDataContentType())
uploadReq.AddCookie(owner)
uploadRec := httptest.NewRecorder()
srv.Handler().ServeHTTP(uploadRec, uploadReq)
if uploadRec.Code != http.StatusOK {
t.Fatalf("загрузка вложения = %d (%s)", uploadRec.Code, uploadRec.Body.String())
}
attachmentID := decodeResponse[struct {
File struct {
FileID string `json:"file_id"`
} `json:"file"`
}](t, uploadRec).File.FileID
if rec := doJSON(t, srv, http.MethodGet, "/files/"+attachmentID, ""); rec.Code != http.StatusUnauthorized {
t.Fatalf("вложение без сессии = %d, ожидался 401", rec.Code)
}
}
// guildChannelID возвращает первую текстовую комнату сервера.
func guildChannelID(t *testing.T, srv *Server, guildID string, cookie *http.Cookie) string {
t.Helper()
list := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guildID+"/channels", "", cookie)
channels := decodeResponse[struct {
Channels []struct {
ID string `json:"id"`
Type string `json:"type"`
} `json:"channels"`
}](t, list)
for _, channel := range channels.Channels {
if channel.Type == "text" {
return channel.ID
}
}
t.Fatal("текстовая комната не найдена")
return ""
}