package store import ( "context" "encoding/json" "errors" "path/filepath" "strings" "testing" "glchat/internal/database" ) // newUserGuildTemplateTestStore открывает временную БД со всеми миграциями и // заводит двух пользователей: шаблоны ссылаются на владельца внешним ключом, // а приватность проверяется на втором аккаунте. func newUserGuildTemplateTestStore(t *testing.T) (*Store, uint64, uint64) { t.Helper() ctx := context.Background() db, err := database.Open(ctx, database.Options{ Path: filepath.Join(t.TempDir(), "glchat.db"), ReadPool: 2, Migrate: true, }) if err != nil { t.Fatalf("открыть тестовую БД: %v", err) } t.Cleanup(func() { if err := db.Close(); err != nil { t.Errorf("закрыть тестовую БД: %v", err) } }) st := New(db) owner, err := st.CreateUser(ctx, CreateUserParams{ Username: "tpl_owner", EmailEnc: "enc-owner", EmailIndex: "idx-owner", PasswordHash: "hash", }) if err != nil { t.Fatalf("создать владельца: %v", err) } other, err := st.CreateUser(ctx, CreateUserParams{ Username: "tpl_other", EmailEnc: "enc-other", EmailIndex: "idx-other", PasswordHash: "hash", }) if err != nil { t.Fatalf("создать второго пользователя: %v", err) } return st, owner.ID, other.ID } func TestUserGuildTemplateLifecycle(t *testing.T) { ctx := context.Background() st, ownerID, _ := newUserGuildTemplateTestStore(t) guild, err := st.CreateGuild(ctx, CreateGuildParams{Name: "Источник", OwnerID: ownerID}) if err != nil { t.Fatalf("CreateGuild: %v", err) } structure := json.RawMessage(`{"roles":[{"name":"Модератор","color":16711680,"permissions":"KICK_MEMBERS","position":50,"hoist":true,"mentionable":true}],"channels":[{"name":"Информация","type":"category","position":0}]}`) created, err := st.CreateUserGuildTemplate(ctx, CreateUserGuildTemplateParams{ OwnerID: ownerID, Name: "Мой шаблон", Description: "Структура", Structure: structure, SourceGuildID: &guild.ID, }) if err != nil { t.Fatalf("CreateUserGuildTemplate: %v", err) } if created.ID == 0 || created.OwnerID != ownerID || created.Name != "Мой шаблон" { t.Fatalf("неожиданный шаблон: %+v", created) } if created.SourceGuildID == nil || *created.SourceGuildID != guild.ID { t.Fatalf("source_guild_id = %v, ожидался %d", created.SourceGuildID, guild.ID) } if string(created.Structure) != string(structure) { t.Fatalf("structure_json = %s", created.Structure) } if created.CreatedAt.IsZero() { t.Fatal("created_at не заполнен") } fetched, err := st.GetUserGuildTemplate(ctx, created.ID) if err != nil { t.Fatalf("GetUserGuildTemplate: %v", err) } if fetched.Name != created.Name || fetched.Description != created.Description { t.Fatalf("прочитан другой шаблон: %+v", fetched) } second, err := st.CreateUserGuildTemplate(ctx, CreateUserGuildTemplateParams{ OwnerID: ownerID, Name: "Второй", Structure: json.RawMessage(`{"roles":[],"channels":[]}`), }) if err != nil { t.Fatalf("CreateUserGuildTemplate (второй): %v", err) } list, err := st.ListUserGuildTemplates(ctx, ownerID) if err != nil { t.Fatalf("ListUserGuildTemplates: %v", err) } if len(list) != 2 { t.Fatalf("шаблонов %d, ожидалось 2", len(list)) } // Свежие — первыми: список сортируется по id DESC. if list[0].ID != second.ID || list[1].ID != created.ID { t.Fatalf("порядок шаблонов неверен: %d, %d", list[0].ID, list[1].ID) } count, err := st.CountUserGuildTemplates(ctx, ownerID) if err != nil { t.Fatalf("CountUserGuildTemplates: %v", err) } if count != 2 { t.Fatalf("CountUserGuildTemplates = %d, ожидалось 2", count) } deleted, err := st.DeleteUserGuildTemplate(ctx, created.ID, ownerID) if err != nil { t.Fatalf("DeleteUserGuildTemplate: %v", err) } if !deleted { t.Fatal("свой шаблон не удалён") } if _, err := st.GetUserGuildTemplate(ctx, created.ID); !errors.Is(err, ErrNotFound) { t.Fatalf("удалённый шаблон читается: %v", err) } deleted, err = st.DeleteUserGuildTemplate(ctx, created.ID, ownerID) if err != nil { t.Fatalf("повторное удаление: %v", err) } if deleted { t.Fatal("повторное удаление вернуло true") } } // Приватность: чужой шаблон не виден в списке и не удаляется, его владелец // продолжает им пользоваться. func TestUserGuildTemplatePrivacy(t *testing.T) { ctx := context.Background() st, ownerID, otherID := newUserGuildTemplateTestStore(t) created, err := st.CreateUserGuildTemplate(ctx, CreateUserGuildTemplateParams{ OwnerID: ownerID, Name: "Секретный", Structure: json.RawMessage(`{"roles":[],"channels":[]}`), }) if err != nil { t.Fatalf("CreateUserGuildTemplate: %v", err) } list, err := st.ListUserGuildTemplates(ctx, otherID) if err != nil { t.Fatalf("ListUserGuildTemplates: %v", err) } if len(list) != 0 { t.Fatalf("чужой шаблон виден в списке: %+v", list) } count, err := st.CountUserGuildTemplates(ctx, otherID) if err != nil { t.Fatalf("CountUserGuildTemplates: %v", err) } if count != 0 { t.Fatalf("чужой шаблон посчитан: %d", count) } deleted, err := st.DeleteUserGuildTemplate(ctx, created.ID, otherID) if err != nil { t.Fatalf("DeleteUserGuildTemplate: %v", err) } if deleted { t.Fatal("чужой шаблон удалён") } if _, err := st.GetUserGuildTemplate(ctx, created.ID); err != nil { t.Fatalf("шаблон владельца пропал: %v", err) } } func TestUserGuildTemplateLimit(t *testing.T) { ctx := context.Background() st, ownerID, otherID := newUserGuildTemplateTestStore(t) for i := 0; i < MaxUserGuildTemplates; i++ { if _, err := st.CreateUserGuildTemplate(ctx, CreateUserGuildTemplateParams{ OwnerID: ownerID, Name: "Шаблон", Structure: json.RawMessage(`{"roles":[],"channels":[]}`), }); err != nil { t.Fatalf("шаблон %d: %v", i, err) } } _, err := st.CreateUserGuildTemplate(ctx, CreateUserGuildTemplateParams{ OwnerID: ownerID, Name: "Лишний", Structure: json.RawMessage(`{"roles":[],"channels":[]}`), }) if !errors.Is(err, ErrTemplateLimitReached) { t.Fatalf("превышение лимита = %v, ожидалось ErrTemplateLimitReached", err) } count, err := st.CountUserGuildTemplates(ctx, ownerID) if err != nil { t.Fatalf("CountUserGuildTemplates: %v", err) } if count != MaxUserGuildTemplates { t.Fatalf("после отказа шаблонов %d, ожидалось %d", count, MaxUserGuildTemplates) } // Лимит считается по владельцу: у второго пользователя место свободно. if _, err := st.CreateUserGuildTemplate(ctx, CreateUserGuildTemplateParams{ OwnerID: otherID, Name: "Свой", Structure: json.RawMessage(`{"roles":[],"channels":[]}`), }); err != nil { t.Fatalf("шаблон второго пользователя: %v", err) } } func TestUserGuildTemplateTooLarge(t *testing.T) { ctx := context.Background() st, ownerID, _ := newUserGuildTemplateTestStore(t) oversized := json.RawMessage(`{"note":"` + strings.Repeat("x", MaxUserGuildTemplateBytes) + `"}`) if _, err := st.CreateUserGuildTemplate(ctx, CreateUserGuildTemplateParams{ OwnerID: ownerID, Name: "Большой", Structure: oversized, }); !errors.Is(err, ErrTemplateTooLarge) { t.Fatalf("слишком большая структура = %v, ожидалось ErrTemplateTooLarge", err) } count, err := st.CountUserGuildTemplates(ctx, ownerID) if err != nil { t.Fatalf("CountUserGuildTemplates: %v", err) } if count != 0 { t.Fatalf("отклонённый шаблон сохранён: %d", count) } }