171 lines
6.4 KiB
Go
171 lines
6.4 KiB
Go
|
|
package store
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"path/filepath"
|
|||
|
|
"testing"
|
|||
|
|
|
|||
|
|
"glchat/internal/database"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// newDMIconStore открывает временную БД и создаёт трёх пользователей: владельца
|
|||
|
|
// группы и двух участников (AGENT.md 7.8).
|
|||
|
|
func newDMIconStore(t *testing.T) (*Store, []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)
|
|||
|
|
users := make([]uint64, 0, 3)
|
|||
|
|
for index, name := range []string{"icon_owner", "icon_bob", "icon_carol"} {
|
|||
|
|
user, err := st.CreateUser(ctx, CreateUserParams{
|
|||
|
|
Username: name,
|
|||
|
|
EmailEnc: "enc",
|
|||
|
|
EmailIndex: name + "-idx",
|
|||
|
|
PasswordHash: "hash",
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("создать пользователя %d: %v", index, err)
|
|||
|
|
}
|
|||
|
|
users = append(users, user.ID)
|
|||
|
|
}
|
|||
|
|
return st, users
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// dmIconFile регистрирует файл иконки так же, как это делает сервер: обычная
|
|||
|
|
// загрузка с назначением dm_icon, привязанная к беседе.
|
|||
|
|
func dmIconFile(ctx context.Context, t *testing.T, st *Store, uploaderID, channelID uint64, name string) *File {
|
|||
|
|
t.Helper()
|
|||
|
|
file, err := st.CreateFile(ctx, CreateFileParams{
|
|||
|
|
UploaderID: uploaderID,
|
|||
|
|
ChannelID: &channelID,
|
|||
|
|
Filename: name,
|
|||
|
|
ContentType: "image/png",
|
|||
|
|
SizeBytes: 128,
|
|||
|
|
StoragePath: filepath.Join(t.TempDir(), name),
|
|||
|
|
SHA256: name,
|
|||
|
|
Purpose: "dm_icon",
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("создать файл иконки: %v", err)
|
|||
|
|
}
|
|||
|
|
return file
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// groupIconSummary ищет беседу в списке пользователя.
|
|||
|
|
func groupIconSummary(ctx context.Context, t *testing.T, st *Store, userID, channelID uint64) DMChannelSummary {
|
|||
|
|
t.Helper()
|
|||
|
|
channels, err := st.ListDMChannels(ctx, userID)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("ListDMChannels: %v", err)
|
|||
|
|
}
|
|||
|
|
for _, summary := range channels {
|
|||
|
|
if summary.Channel.ID == channelID {
|
|||
|
|
return summary
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
t.Fatalf("беседа %d не найдена в списке пользователя %d", channelID, userID)
|
|||
|
|
return DMChannelSummary{}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestDMChannelIconLifecycle проверяет иконку групповой беседы (D-070):
|
|||
|
|
// установку и снятие через UpdateChannel, видимость в списке бесед у всех
|
|||
|
|
// участников и ON DELETE SET NULL при удалении файла.
|
|||
|
|
func TestDMChannelIconLifecycle(t *testing.T) {
|
|||
|
|
ctx := context.Background()
|
|||
|
|
st, users := newDMIconStore(t)
|
|||
|
|
channel, err := st.CreateGroupDMChannel(ctx, users[0], "Игровая", users[1:])
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("CreateGroupDMChannel: %v", err)
|
|||
|
|
}
|
|||
|
|
file := dmIconFile(ctx, t, st, users[0], channel.ID, "icon.png")
|
|||
|
|
|
|||
|
|
updated, err := st.UpdateChannel(ctx, channel.ID, UpdateChannelParams{IconFileID: &file.ID})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("UpdateChannel(IconFileID): %v", err)
|
|||
|
|
}
|
|||
|
|
if updated.IconFileID == nil || *updated.IconFileID != file.ID {
|
|||
|
|
t.Fatalf("icon_file_id = %v, ожидался %d", updated.IconFileID, file.ID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Иконка видна каждому участнику: список бесед читает ту же колонку.
|
|||
|
|
for _, userID := range users {
|
|||
|
|
summary := groupIconSummary(ctx, t, st, userID, channel.ID)
|
|||
|
|
if !summary.IsGroup {
|
|||
|
|
t.Fatalf("беседа не групповая для пользователя %d: %+v", userID, summary)
|
|||
|
|
}
|
|||
|
|
if summary.Channel.IconFileID == nil || *summary.Channel.IconFileID != file.ID {
|
|||
|
|
t.Fatalf("иконка в списке пользователя %d = %v", userID, summary.Channel.IconFileID)
|
|||
|
|
}
|
|||
|
|
// У группы нет собеседника: аватар 1:1 остаётся пустым.
|
|||
|
|
if summary.AvatarFileID != nil {
|
|||
|
|
t.Fatalf("у группы появился аватар собеседника: %v", *summary.AvatarFileID)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Снятие иконки очищает колонку.
|
|||
|
|
cleared, err := st.UpdateChannel(ctx, channel.ID, UpdateChannelParams{ClearIcon: true})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("UpdateChannel(ClearIcon): %v", err)
|
|||
|
|
}
|
|||
|
|
if cleared.IconFileID != nil {
|
|||
|
|
t.Fatalf("после снятия icon_file_id = %v, ожидался NULL", *cleared.IconFileID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Удаление файла обнуляет ссылку (ON DELETE SET NULL), а не ломает беседу.
|
|||
|
|
if _, err := st.UpdateChannel(ctx, channel.ID, UpdateChannelParams{IconFileID: &file.ID}); err != nil {
|
|||
|
|
t.Fatalf("повторная установка иконки: %v", err)
|
|||
|
|
}
|
|||
|
|
if err := st.DeleteFile(ctx, file.ID); err != nil {
|
|||
|
|
t.Fatalf("DeleteFile: %v", err)
|
|||
|
|
}
|
|||
|
|
after, err := st.GetChannel(ctx, channel.ID)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("GetChannel после удаления файла: %v", err)
|
|||
|
|
}
|
|||
|
|
if after.IconFileID != nil {
|
|||
|
|
t.Fatalf("после удаления файла icon_file_id = %v, ожидался NULL", *after.IconFileID)
|
|||
|
|
}
|
|||
|
|
if after.Name != "Игровая" || after.DMOwnerID == nil {
|
|||
|
|
t.Fatalf("беседа пострадала при удалении файла: %+v", after)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestDMChannelIconResetOnDemotion: когда в беседе остаётся два участника, она
|
|||
|
|
// снова обычная личная — иконка группы сбрасывается вместе с именем и
|
|||
|
|
// владельцем (AGENT.md 7.8, D-070).
|
|||
|
|
func TestDMChannelIconResetOnDemotion(t *testing.T) {
|
|||
|
|
ctx := context.Background()
|
|||
|
|
st, users := newDMIconStore(t)
|
|||
|
|
channel, err := st.CreateGroupDMChannel(ctx, users[0], "Игровая", users[1:])
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("CreateGroupDMChannel: %v", err)
|
|||
|
|
}
|
|||
|
|
file := dmIconFile(ctx, t, st, users[0], channel.ID, "icon.png")
|
|||
|
|
if _, err := st.UpdateChannel(ctx, channel.ID, UpdateChannelParams{IconFileID: &file.ID}); err != nil {
|
|||
|
|
t.Fatalf("UpdateChannel(IconFileID): %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := st.RemoveDMParticipant(ctx, channel.ID, users[2]); err != nil {
|
|||
|
|
t.Fatalf("RemoveDMParticipant: %v", err)
|
|||
|
|
}
|
|||
|
|
demoted, err := st.GetChannel(ctx, channel.ID)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("GetChannel: %v", err)
|
|||
|
|
}
|
|||
|
|
if demoted.IconFileID != nil || demoted.DMOwnerID != nil || demoted.Name != "" {
|
|||
|
|
t.Fatalf("беседа с двумя участниками не сброшена: %+v", demoted)
|
|||
|
|
}
|
|||
|
|
}
|