feat(invites): страница приглашения со splash и переопределением
- миграция 00015: переопределение splash у приглашения (AGENT.md 7.9)
- GET /invites/{code} доступен без сессии: гость по ссылке видит карточку
сервера, описание, число участников и пригласившего
- POST/DELETE /invites/{code}/background: своя картинка страницы под правом
создателя или MANAGE_GUILD, файл удаляется при замене и снятии
- клиент: маршрут /invite/:code с фоном (переопределение → splash → баннер),
карточкой сервера, кнопками «Присоединиться»/«Открыть сервер» и входом с
возвратом на страницу (?next=)
- тесты: Go (карточка, загрузка, права, снятие) и Vitest (splash, гость,
присоединение, недействительная ссылка)
This commit is contained in:
@@ -602,3 +602,98 @@ func TestBlockUserStopsDMsAndRequests(t *testing.T) {
|
||||
}
|
||||
_ = ownerID
|
||||
}
|
||||
|
||||
func TestInviteBackgroundAndPreview(t *testing.T) {
|
||||
srv, owner, member, guildID, _ := cosmeticsFixture(t)
|
||||
created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+guildID+"/invites",
|
||||
`{"max_uses":5,"max_age_seconds":600}`, owner)
|
||||
code := decodeResponse[struct {
|
||||
Invite struct {
|
||||
Code string `json:"code"`
|
||||
} `json:"invite"`
|
||||
}](t, created).Invite.Code
|
||||
if code == "" {
|
||||
t.Fatalf("приглашение не создано: %s", created.Body.String())
|
||||
}
|
||||
|
||||
// Предпросмотр: карточка сервера и пригласивший.
|
||||
preview := doJSON(t, srv, http.MethodGet, "/api/v1/invites/"+code, "", member)
|
||||
body := decodeResponse[struct {
|
||||
Invite struct {
|
||||
BackgroundFileID string `json:"background_file_id"`
|
||||
} `json:"invite"`
|
||||
Guild struct {
|
||||
Name string `json:"name"`
|
||||
IsMember bool `json:"is_member"`
|
||||
Inviter struct {
|
||||
Username string `json:"username"`
|
||||
} `json:"inviter"`
|
||||
} `json:"guild"`
|
||||
}](t, preview)
|
||||
if body.Guild.Name == "" || body.Guild.Inviter.Username == "" {
|
||||
t.Fatalf("предпросмотр без карточки: %s", preview.Body.String())
|
||||
}
|
||||
|
||||
// Загрузка переопределения splash: multipart, картинка.
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
part, err := writer.CreateFormFile("file", "invite.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/invites/"+code+"/background", &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("загрузка картинки приглашения = %d (%s)", rec.Code, rec.Body.String())
|
||||
}
|
||||
uploaded := decodeResponse[struct {
|
||||
Invite struct {
|
||||
BackgroundFileID string `json:"background_file_id"`
|
||||
} `json:"invite"`
|
||||
}](t, rec)
|
||||
if uploaded.Invite.BackgroundFileID == "" {
|
||||
t.Fatalf("переопределение не сохранилось: %s", rec.Body.String())
|
||||
}
|
||||
|
||||
// Карточка приглашения отдаёт переопределение и без сессии не работает.
|
||||
after := doJSON(t, srv, http.MethodGet, "/api/v1/invites/"+code, "", member)
|
||||
fileID := decodeResponse[struct {
|
||||
Invite struct {
|
||||
BackgroundFileID string `json:"background_file_id"`
|
||||
} `json:"invite"`
|
||||
}](t, after).Invite.BackgroundFileID
|
||||
if fileID != uploaded.Invite.BackgroundFileID {
|
||||
t.Fatalf("переопределение не видно в предпросмотре: %s", after.Body.String())
|
||||
}
|
||||
|
||||
// Посторонний не может менять оформление чужого приглашения.
|
||||
outsider := registerAndLogin(t, srv, "invite_guest", "invite-guest@example.com")
|
||||
denied := doJSON(t, srv, http.MethodDelete, "/api/v1/invites/"+code+"/background", "", outsider)
|
||||
if denied.Code != http.StatusForbidden && denied.Code != http.StatusNotFound {
|
||||
t.Fatalf("снятие чужим = %d, ожидался отказ (%s)", denied.Code, denied.Body.String())
|
||||
}
|
||||
|
||||
// Снятие переопределения создателем.
|
||||
cleared := doJSON(t, srv, http.MethodDelete, "/api/v1/invites/"+code+"/background", "", owner)
|
||||
if cleared.Code != http.StatusOK {
|
||||
t.Fatalf("снятие переопределения = %d (%s)", cleared.Code, cleared.Body.String())
|
||||
}
|
||||
final := decodeResponse[struct {
|
||||
Invite struct {
|
||||
BackgroundFileID string `json:"background_file_id"`
|
||||
} `json:"invite"`
|
||||
}](t, cleared)
|
||||
if final.Invite.BackgroundFileID != "" {
|
||||
t.Fatalf("переопределение не снято: %s", cleared.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user