package admin import ( "context" "strings" "testing" "telesrv/internal/domain" ) type fakeGifCatalogService struct { entries []domain.GifCatalogEntry listCalls int uploadCalls int createCalls int } func (s *fakeGifCatalogService) ValidateGifUpload(_ string, data []byte) (string, bool) { return "image/gif", strings.HasPrefix(string(data), "GIF89a") } func (s *fakeGifCatalogService) AdminUploadGifMaterial(context.Context, string, []byte) (domain.Document, error) { s.uploadCalls++ return domain.Document{ID: 91}, nil } func (s *fakeGifCatalogService) AdminCreateGifCatalogEntry(context.Context, string, int64) (domain.GifCatalogEntry, error) { s.createCalls++ entry := domain.GifCatalogEntry{ID: 92, DocumentID: 91, Title: "Wave", Enabled: true} s.entries = append(s.entries, entry) return entry, nil } func (s *fakeGifCatalogService) AdminListGifCatalog(context.Context) ([]domain.GifCatalogEntry, error) { s.listCalls++ return append([]domain.GifCatalogEntry(nil), s.entries...), nil } func (*fakeGifCatalogService) AdminSetGifCatalogEnabled(context.Context, int64, bool) (bool, error) { return true, nil } func (*fakeGifCatalogService) AdminSetGifCatalogSortOrder(context.Context, int64, int) (bool, error) { return true, nil } func (*fakeGifCatalogService) AdminSetGifCatalogCategory(context.Context, int64, string) (bool, error) { return true, nil } func (*fakeGifCatalogService) AdminAutoCategorizeGifCatalog(context.Context) (int, error) { return 0, nil } func (*fakeGifCatalogService) AdminDeleteUncategorizedGifs(context.Context) (int, int, error) { return 0, 0, nil } func (*fakeGifCatalogService) AdminDeleteGifCatalogEntry(context.Context, int64) (bool, error) { return true, nil } func (*fakeGifCatalogService) GetFile(context.Context, domain.FileDownloadRequest) (domain.FileChunk, bool, error) { return domain.FileChunk{}, false, nil } func TestCreateGifCatalogEntryReplayAndContentFingerprint(t *testing.T) { ctx := context.Background() repo := newMemoryCommandRepo() gifs := &fakeGifCatalogService{} svc := NewService(Dependencies{Commands: repo, GifCatalog: gifs, Now: fixedNow}) req := CreateGifCatalogEntryRequest{ CommandMeta: CommandMeta{CommandID: "gif-create-1", Actor: "ops", Reason: "catalog"}, Title: "Wave", FileName: "wave.gif", Data: []byte("GIF89a-one"), } first, err := svc.CreateGifCatalogEntry(ctx, req) if err != nil || first.Status != string(domain.AdminCommandCompleted) { t.Fatalf("first create = %+v err=%v", first, err) } if gifs.listCalls != 1 || gifs.uploadCalls != 1 || gifs.createCalls != 1 { t.Fatalf("first calls list/upload/create=%d/%d/%d", gifs.listCalls, gifs.uploadCalls, gifs.createCalls) } // Even when the catalog has become full, the same completed command must be // replayed before capacity preflight and must not upload another blob. gifs.entries = make([]domain.GifCatalogEntry, domain.MaxGifCatalogEntries) replay, err := svc.CreateGifCatalogEntry(ctx, req) if err != nil || !replay.AlreadyExecuted { t.Fatalf("replay = %+v err=%v", replay, err) } if gifs.listCalls != 1 || gifs.uploadCalls != 1 || gifs.createCalls != 1 { t.Fatalf("replay calls list/upload/create=%d/%d/%d", gifs.listCalls, gifs.uploadCalls, gifs.createCalls) } conflict := req conflict.Data = []byte("GIF89a-two") if _, err := svc.CreateGifCatalogEntry(ctx, conflict); err == nil || err.Error() != "COMMAND_ID_CONFLICT" { t.Fatalf("different content with same command id err=%v", err) } if gifs.listCalls != 1 || gifs.uploadCalls != 1 || gifs.createCalls != 1 { t.Fatalf("conflict mutated list/upload/create=%d/%d/%d", gifs.listCalls, gifs.uploadCalls, gifs.createCalls) } }