2026-09-21 18:04:52 +03:00
|
|
|
package server
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Тесты шаблонов серверов (AGENT.md 7.3): встроенный шаблон создаёт структуру
|
|
|
|
|
// комнат и ролей без сообщений, неизвестный шаблон отклоняется.
|
|
|
|
|
|
|
|
|
|
func TestGuildTemplatesCatalog(t *testing.T) {
|
|
|
|
|
srv, _ := newTestServer(t)
|
|
|
|
|
user := registerAndLogin(t, srv, "tpl_user", "tpl@example.com")
|
|
|
|
|
|
|
|
|
|
rec := doJSON(t, srv, http.MethodGet, "/api/v1/guild-templates", "", user)
|
|
|
|
|
if rec.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("каталог шаблонов = %d (%s)", rec.Code, rec.Body.String())
|
|
|
|
|
}
|
|
|
|
|
payload := decodeResponse[struct {
|
|
|
|
|
Templates []struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Roles []string `json:"roles"`
|
|
|
|
|
TextCount int `json:"text_channels"`
|
|
|
|
|
VoiceCount int `json:"voice_channels"`
|
|
|
|
|
Categories []string `json:"categories"`
|
|
|
|
|
} `json:"templates"`
|
|
|
|
|
}](t, rec)
|
|
|
|
|
if len(payload.Templates) != 3 {
|
|
|
|
|
t.Fatalf("шаблонов %d, ожидалось 3", len(payload.Templates))
|
|
|
|
|
}
|
|
|
|
|
found := map[string]bool{}
|
|
|
|
|
for _, template := range payload.Templates {
|
|
|
|
|
found[template.ID] = true
|
|
|
|
|
if len(template.Roles) < 2 {
|
|
|
|
|
t.Fatalf("шаблон %s без ролей: %+v", template.ID, template.Roles)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, id := range []string{"empty", "community", "gaming"} {
|
|
|
|
|
if !found[id] {
|
|
|
|
|
t.Fatalf("шаблон %s отсутствует в каталоге", id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGuildFromTemplateCreatesStructure(t *testing.T) {
|
|
|
|
|
cases := []struct {
|
|
|
|
|
template string
|
|
|
|
|
wantRoles []string
|
|
|
|
|
wantText []string
|
|
|
|
|
wantVoice []string
|
|
|
|
|
wantCategory []string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
template: "empty",
|
|
|
|
|
wantRoles: []string{"Администратор", "Пользователь"},
|
|
|
|
|
wantText: []string{"общий"},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
template: "community",
|
|
|
|
|
wantRoles: []string{"Администратор", "Пользователь", "Модератор"},
|
|
|
|
|
wantText: []string{"правила", "объявления", "общий", "флудилка"},
|
|
|
|
|
wantVoice: []string{"Голосовой"},
|
|
|
|
|
wantCategory: []string{"Информация", "Общение"},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
template: "gaming",
|
|
|
|
|
wantRoles: []string{"Администратор", "Пользователь", "Игрок"},
|
|
|
|
|
wantText: []string{"общий", "поиск-игр"},
|
|
|
|
|
wantVoice: []string{"Игровая 1", "Игровая 2"},
|
|
|
|
|
wantCategory: []string{"Текстовые", "Голосовые"},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, item := range cases {
|
|
|
|
|
t.Run(item.template, func(t *testing.T) {
|
|
|
|
|
srv, _ := newTestServer(t)
|
|
|
|
|
owner := registerAndLogin(t, srv, "tpl_"+item.template, "tpl-"+item.template+"@example.com")
|
|
|
|
|
created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds",
|
|
|
|
|
`{"name":"Шаблон","template_id":"`+item.template+`"}`, owner)
|
|
|
|
|
if created.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("создание = %d (%s)", created.Code, created.Body.String())
|
|
|
|
|
}
|
|
|
|
|
guildID := decodeResponse[struct {
|
|
|
|
|
Guild struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
} `json:"guild"`
|
|
|
|
|
}](t, created).Guild.ID
|
|
|
|
|
|
|
|
|
|
roles := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guildID+"/roles", "", owner)
|
|
|
|
|
roleNames := make([]string, 0, 4)
|
|
|
|
|
for _, role := range decodeResponse[struct {
|
|
|
|
|
Roles []struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
} `json:"roles"`
|
|
|
|
|
}](t, roles).Roles {
|
|
|
|
|
roleNames = append(roleNames, role.Name)
|
|
|
|
|
}
|
|
|
|
|
for _, want := range item.wantRoles {
|
|
|
|
|
if !contains(roleNames, want) {
|
|
|
|
|
t.Fatalf("роль %q не создана: %v", want, roleNames)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
channels := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guildID+"/channels", "", owner)
|
|
|
|
|
list := decodeResponse[struct {
|
|
|
|
|
Channels []struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
ParentID string `json:"parent_id"`
|
|
|
|
|
} `json:"channels"`
|
|
|
|
|
}](t, channels)
|
|
|
|
|
byType := map[string][]string{}
|
|
|
|
|
for _, channel := range list.Channels {
|
|
|
|
|
byType[channel.Type] = append(byType[channel.Type], channel.Name)
|
|
|
|
|
// Комнаты шаблона лежат внутри своих категорий.
|
|
|
|
|
if len(item.wantCategory) > 0 && channel.Type != "category" && channel.ParentID == "" {
|
|
|
|
|
t.Fatalf("комната %q не привязана к категории", channel.Name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, want := range item.wantText {
|
|
|
|
|
if !contains(byType["text"], want) {
|
|
|
|
|
t.Fatalf("текстовая комната %q не создана: %v", want, byType["text"])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, want := range item.wantVoice {
|
|
|
|
|
if !contains(byType["voice"], want) {
|
|
|
|
|
t.Fatalf("голосовая комната %q не создана: %v", want, byType["voice"])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, want := range item.wantCategory {
|
|
|
|
|
if !contains(byType["category"], want) {
|
|
|
|
|
t.Fatalf("категория %q не создана: %v", want, byType["category"])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGuildWithUnknownTemplateRejected(t *testing.T) {
|
|
|
|
|
srv, _ := newTestServer(t)
|
|
|
|
|
owner := registerAndLogin(t, srv, "tpl_bad", "tpl-bad@example.com")
|
|
|
|
|
|
|
|
|
|
rec := doJSON(t, srv, http.MethodPost, "/api/v1/guilds",
|
|
|
|
|
`{"name":"Странный","template_id":"нет-такого"}`, owner)
|
|
|
|
|
if rec.Code != http.StatusUnprocessableEntity {
|
|
|
|
|
t.Fatalf("неизвестный шаблон = %d (%s)", rec.Code, rec.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// contains проверяет наличие строки в списке.
|
|
|
|
|
func contains(list []string, value string) bool {
|
|
|
|
|
for _, item := range list {
|
|
|
|
|
if item == value {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-09-21 18:16:18 +03:00
|
|
|
|
|
|
|
|
func TestGuildFromTemplateCreatesNoDuplicates(t *testing.T) {
|
|
|
|
|
srv, _ := newTestServer(t)
|
|
|
|
|
owner := registerAndLogin(t, srv, "tpl_dup", "tpl-dup@example.com")
|
|
|
|
|
created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds",
|
|
|
|
|
`{"name":"Дубли","template_id":"community"}`, owner)
|
|
|
|
|
guildID := decodeResponse[struct {
|
|
|
|
|
Guild struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
} `json:"guild"`
|
|
|
|
|
}](t, created).Guild.ID
|
|
|
|
|
|
|
|
|
|
channels := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guildID+"/channels", "", owner)
|
|
|
|
|
list := decodeResponse[struct {
|
|
|
|
|
Channels []struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
} `json:"channels"`
|
|
|
|
|
}](t, channels)
|
|
|
|
|
seen := map[string]int{}
|
|
|
|
|
for _, channel := range list.Channels {
|
|
|
|
|
seen[channel.Type+":"+channel.Name]++
|
|
|
|
|
}
|
|
|
|
|
for key, count := range seen {
|
|
|
|
|
if count > 1 {
|
|
|
|
|
t.Fatalf("комната %s создана %d раза", key, count)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// «Сообщество»: 2 категории, 4 текстовые и 1 голосовая.
|
|
|
|
|
if len(list.Channels) != 7 {
|
|
|
|
|
t.Fatalf("комнат %d, ожидалось 7", len(list.Channels))
|
|
|
|
|
}
|
|
|
|
|
}
|