package test import ( imageDomain "RenewCMS/internal/domain/image" "RenewCMS/internal/infrastructure/useCases" "errors" "io" "slices" "sort" "strconv" "strings" "testing" "time" ) var mockImageEpoch = time.Date(2026, time.June, 1, 12, 0, 0, 0, time.UTC) var mockImageExtensions = map[string]string{ "image/png": ".png", "image/jpeg": ".jpeg", "image/webp": ".webp", "image/svg+xml": ".svg", } type mockImageStorage struct { blobs map[string][]byte putCalls int nextName int lastContent []byte deleted []string failPut error failDelete error } func newMockImageStorage() *mockImageStorage { return &mockImageStorage{blobs: map[string][]byte{}} } func (m *mockImageStorage) Put(contentType string, content io.Reader) (string, error) { m.putCalls++ if m.failPut != nil { return "", m.failPut } data, err := io.ReadAll(content) if err != nil { return "", err } m.nextName++ name := "stored-" + strconv.Itoa(m.nextName) + mockImageExtensions[contentType] m.blobs[name] = data m.lastContent = data return name, nil } func (m *mockImageStorage) Open(name string) (imageDomain.Blob, error) { data, ok := m.blobs[name] if !ok { return imageDomain.Blob{}, imageDomain.ErrBlobNotFound } return imageDomain.Blob{Content: io.NopCloser(strings.NewReader(string(data))), Size: int64(len(data))}, nil } func (m *mockImageStorage) Delete(name string) error { m.deleted = append(m.deleted, name) if m.failDelete != nil { return m.failDelete } delete(m.blobs, name) return nil } var _ imageDomain.Storage = &mockImageStorage{} type mockImageRepo struct { images map[uint32]imageDomain.Image nextID uint32 fail failSwitch listCalls int failDeleteFor map[uint32]error deleteCalls []uint32 createCalls int onDelete func(id uint32) storedNameLookups int } func newMockImageRepo() *mockImageRepo { return &mockImageRepo{images: make(map[uint32]imageDomain.Image)} } func (m *mockImageRepo) sortedImages() []imageDomain.Image { result := make([]imageDomain.Image, 0, len(m.images)) for _, img := range m.images { result = append(result, img) } sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID }) return result } func (m *mockImageRepo) Create(input imageDomain.Image) (imageDomain.Image, error) { m.createCalls++ if err := m.fail.trigger("Create"); err != nil { return imageDomain.Image{}, err } for m.images[m.nextID+1].ID != 0 { m.nextID++ } m.nextID++ img := input img.ID = m.nextID img.CreatedAt = mockImageEpoch.Add(time.Duration(m.nextID) * time.Minute) m.images[img.ID] = img return img, nil } func (m *mockImageRepo) ListMissingMetadata() ([]imageDomain.Image, error) { if err := m.fail.trigger("ListMissingMetadata"); err != nil { return nil, err } pending := []imageDomain.Image{} for _, img := range m.sortedImages() { if img.Filename == "" { pending = append(pending, img) } } return pending, nil } func (m *mockImageRepo) UpdateMetadata(id uint32, metadata imageDomain.Metadata) error { if err := m.fail.trigger("UpdateMetadata"); err != nil { return err } img, ok := m.images[id] if !ok { return imageDomain.ErrImageNotFound } img.Filename, img.Size, img.ContentType = metadata.Filename, metadata.Size, metadata.ContentType img.Width, img.Height, img.SHA256 = metadata.Width, metadata.Height, metadata.SHA256 m.images[id] = img return nil } func (m *mockImageRepo) Get(id uint32) (imageDomain.Image, error) { if err := m.fail.trigger("Get"); err != nil { return imageDomain.Image{}, err } img, ok := m.images[id] if !ok { return imageDomain.Image{}, imageDomain.ErrImageNotFound } return img, nil } func (m *mockImageRepo) Delete(id uint32) error { m.deleteCalls = append(m.deleteCalls, id) if m.onDelete != nil { m.onDelete(id) } if err := m.fail.trigger("Delete"); err != nil { return err } if err, ok := m.failDeleteFor[id]; ok { return err } if _, ok := m.images[id]; !ok { return imageDomain.ErrImageNotFound } delete(m.images, id) return nil } func (m *mockImageRepo) List(query imageDomain.SearchQuery) ([]imageDomain.Image, error) { m.listCalls++ if err := m.fail.trigger("List"); err != nil { return nil, err } result := make([]imageDomain.Image, 0) for _, img := range m.sortedImages() { if query.Search != "" && !strings.Contains(strings.ToLower(img.Filename), strings.ToLower(query.Search)) { continue } if query.Hash != "" && img.SHA256 != query.Hash { continue } result = append(result, img) } return result, nil } func (m *mockImageRepo) FindByHash(hash string) ([]imageDomain.Image, error) { if err := m.fail.trigger("FindByHash"); err != nil { return nil, err } result := make([]imageDomain.Image, 0) for _, img := range m.sortedImages() { if hash != "" && img.SHA256 == hash { result = append(result, img) } } return result, nil } func (m *mockImageRepo) Rename(id uint32, filename string) (imageDomain.Image, error) { if err := m.fail.trigger("Rename"); err != nil { return imageDomain.Image{}, err } img, ok := m.images[id] if !ok { return imageDomain.Image{}, imageDomain.ErrImageNotFound } img.Filename = filename m.images[id] = img return img, nil } func (m *mockImageRepo) hashCounts() map[string]int { counts := map[string]int{} for _, img := range m.images { if img.SHA256 != "" { counts[img.SHA256]++ } } return counts } func (m *mockImageRepo) ListDuplicates() ([]imageDomain.Image, error) { if err := m.fail.trigger("ListDuplicates"); err != nil { return nil, err } counts := m.hashCounts() result := make([]imageDomain.Image, 0) for _, img := range m.sortedImages() { if img.SHA256 != "" && counts[img.SHA256] > 1 { result = append(result, img) } } return result, nil } func (m *mockImageRepo) Counts() (imageDomain.Counts, error) { if err := m.fail.trigger("Counts"); err != nil { return imageDomain.Counts{}, err } groups := 0 for _, count := range m.hashCounts() { if count > 1 { groups++ } } return imageDomain.Counts{All: len(m.images), DuplicateGroups: groups}, nil } func (m *mockImageRepo) ExistsByStoredName(storedName string) (bool, error) { if err := m.fail.trigger("ExistsByStoredName"); err != nil { return false, err } for _, img := range m.images { if imageDomain.StoredName(img.Path) == storedName { return true, nil } } return false, nil } func (m *mockImageRepo) ListByStoredNames(storedNames []string) ([]imageDomain.Image, error) { if err := m.fail.trigger("ListByStoredNames"); err != nil { return nil, err } m.storedNameLookups++ result := make([]imageDomain.Image, 0) for _, img := range m.sortedImages() { if slices.Contains(storedNames, imageDomain.StoredName(img.Path)) { result = append(result, img) } } return result, nil } var _ imageDomain.Repository = &mockImageRepo{} func TestImageUseCases(t *testing.T) { repo := newMockImageRepo() createUc := useCases.NewCreateImageUseCase(repo, newMockImageStorage(), discardRecorder) deleteUc := useCases.NewDeleteMediaUseCase(repo, newMockImageStorage(), newMockVariantFileRepo(), discardRecorder, plainSites(), plainAuthors()) t.Run("Create Image", func(t *testing.T) { input := imageDomain.ImageInput{ Content: strings.NewReader(string(pngBytes(t, 3, 2))), Filename: "test.png", ContentType: "image/png", } res, err := createUc.CreateImage(testCtx, input) if err != nil { t.Fatalf("CreateImage failed: %v", err) } if res.ID == 0 { t.Errorf("Expected valid ID, got 0") } if res.Filename != "test.png" { t.Errorf("Filename mismatch: %s", res.Filename) } }) t.Run("Delete Image", func(t *testing.T) { repo.images[99] = imageDomain.Image{ID: 99, Path: "/static/delete-me.png"} err := deleteUc.Delete(testCtx, 99) if err != nil { t.Fatalf("Delete failed: %v", err) } if _, exists := repo.images[99]; exists { t.Errorf("Image 99 should have been deleted") } }) t.Run("Delete unknown image", func(t *testing.T) { if err := deleteUc.Delete(testCtx, 12345); !errors.Is(err, imageDomain.ErrImageNotFound) { t.Errorf("expected ErrImageNotFound, got %v", err) } }) }