package test import ( domainArticle "RenewCMS/internal/domain/article" domainImage "RenewCMS/internal/domain/image" "RenewCMS/internal/infrastructure/persistence/models" persistence "RenewCMS/internal/infrastructure/persistence/repositories" "RenewCMS/internal/infrastructure/storage" "RenewCMS/internal/infrastructure/useCases" "errors" "os" "path/filepath" "testing" "gorm.io/gorm" ) func newMediaPersistence(t *testing.T) (*gorm.DB, *persistence.ImageRepository, string) { t.Helper() db := openInMemoryDb(t) if err := db.AutoMigrate(&models.Article{}, &models.Author{}, &models.ArticleAuthor{}, &models.Image{}); err != nil { t.Fatalf("failed to migrate: %v", err) } uploadDir := t.TempDir() return db, persistence.NewImageRepository(db), uploadDir } func createStoredImage(t *testing.T, repo *persistence.ImageRepository, filename string, content []byte, contentType string) domainImage.Image { t.Helper() created, err := repo.Create(domainImage.Image{ Path: domainImage.PublicPath(filename), Filename: filename, Size: int64(len(content)), ContentType: contentType, Width: 3, Height: 2, SHA256: sha256Hex(content), }) if err != nil { t.Fatalf("Create failed: %v", err) } return created } func TestMediaImageRepository(t *testing.T) { t.Run("Create stores the metadata and returns the assigned id", func(t *testing.T) { _, repo, _ := newMediaPersistence(t) content := pngBytes(t, 3, 2) created := createStoredImage(t, repo, "a.png", content, "image/png") if created.ID == 0 || created.Filename != "a.png" || created.SHA256 != sha256Hex(content) || created.Width != 3 || created.Size != int64(len(content)) { t.Errorf("unexpected image %+v", created) } got, err := repo.Get(created.ID) if err != nil || got.SHA256 != created.SHA256 { t.Errorf("Get returned %+v, %v", got, err) } }) t.Run("Get reports a missing image as ErrImageNotFound", func(t *testing.T) { _, repo, _ := newMediaPersistence(t) if _, err := repo.Get(9); !errors.Is(err, domainImage.ErrImageNotFound) { t.Errorf("expected ErrImageNotFound, got %v", err) } }) t.Run("Delete removes the row", func(t *testing.T) { _, repo, _ := newMediaPersistence(t) created := createStoredImage(t, repo, "a.png", pngBytes(t, 3, 2), "image/png") if err := repo.Delete(created.ID); err != nil { t.Fatalf("Delete failed: %v", err) } if _, err := repo.Get(created.ID); !errors.Is(err, domainImage.ErrImageNotFound) { t.Errorf("expected the row to be gone, got %v", err) } if err := repo.Delete(created.ID); !errors.Is(err, domainImage.ErrImageNotFound) { t.Errorf("expected ErrImageNotFound on a second delete, got %v", err) } }) t.Run("List, FindByHash, Rename, ListDuplicates and Counts", func(t *testing.T) { _, repo, _ := newMediaPersistence(t) same := pngBytes(t, 3, 2) first := createStoredImage(t, repo, "North-Wall.png", same, "image/png") second := createStoredImage(t, repo, "copy.png", same, "image/png") createStoredImage(t, repo, "other.png", pngBytes(t, 4, 4), "image/png") found, err := repo.List(domainImage.SearchQuery{Search: "NORTH"}) if err != nil || len(found) != 1 || found[0].ID != first.ID { t.Errorf("unexpected search result %+v, %v", found, err) } byHash, err := repo.List(domainImage.SearchQuery{Hash: sha256Hex(same)}) if err != nil || len(byHash) != 2 { t.Errorf("unexpected hash result %+v, %v", byHash, err) } sameContent, err := repo.FindByHash(sha256Hex(same)) if err != nil || len(sameContent) != 2 || sameContent[1].ID != second.ID { t.Errorf("unexpected FindByHash result %+v, %v", sameContent, err) } if none, err := repo.FindByHash(""); err != nil || len(none) != 0 { t.Errorf("expected no result for an empty hash, got %+v, %v", none, err) } duplicates, err := repo.ListDuplicates() if err != nil || len(duplicates) != 2 { t.Errorf("unexpected duplicates %+v, %v", duplicates, err) } counts, err := repo.Counts() if err != nil || counts.All != 3 || counts.DuplicateGroups != 1 { t.Errorf("unexpected counts %+v, %v", counts, err) } renamed, err := repo.Rename(first.ID, "renamed.png") if err != nil || renamed.Filename != "renamed.png" || renamed.Path != first.Path { t.Errorf("unexpected rename result %+v, %v", renamed, err) } if _, err := repo.Rename(9999, "x.png"); !errors.Is(err, domainImage.ErrImageNotFound) { t.Errorf("expected ErrImageNotFound, got %v", err) } }) } func TestMediaArticleRepositoryFindsBodiesContainingAFilename(t *testing.T) { db, _, _ := newMediaPersistence(t) repo := persistence.NewArticleRepository(db) if _, err := repo.Create(domainArticle.Article{Title: "Kitchen Renovation", Body: "see /static/uploadedImages/aaa.png"}); err != nil { t.Fatalf("failed to seed article: %v", err) } if _, err := repo.Create(domainArticle.Article{Title: "Backyard Notes", Body: "nothing"}); err != nil { t.Fatalf("failed to seed article: %v", err) } found, err := repo.FindByBodyContaining("aaa.png") if err != nil || len(found) != 1 || found[0].Title != "Kitchen Renovation" { t.Errorf("unexpected result %+v, %v", found, err) } if none, err := repo.FindByBodyContaining("zzz.png"); err != nil || none == nil || len(none) != 0 { t.Errorf("expected an empty non-nil slice, got %+v, %v", none, err) } } func TestMediaImageBackfill(t *testing.T) { seedLegacyRow := func(t *testing.T, db *gorm.DB, name string) models.Image { t.Helper() row := models.Image{Path: "/static/uploadedImages/" + name} if err := db.Create(&row).Error; err != nil { t.Fatalf("failed to seed a legacy row: %v", err) } return row } writeFile := func(t *testing.T, dir string, name string, content []byte) { t.Helper() if err := os.WriteFile(filepath.Join(dir, name), content, 0666); err != nil { t.Fatalf("failed to write %s: %v", name, err) } } t.Run("Existing rows get metadata and hash from the files on disk", func(t *testing.T) { db, repo, uploadDir := newMediaPersistence(t) png, svg := pngBytes(t, 3, 2), []byte(svgFixture) writeFile(t, uploadDir, "one.png", png) writeFile(t, uploadDir, "two.svg", svg) pngRow, svgRow := seedLegacyRow(t, db, "one.png"), seedLegacyRow(t, db, "two.svg") if err := useCases.NewBackfillImageMetadataUseCase(repo, storage.NewLocalStorage(uploadDir)).Run(); err != nil { t.Fatalf("backfill failed: %v", err) } gotPng, _ := repo.Get(pngRow.ID) if gotPng.Filename != "one.png" || gotPng.Size != int64(len(png)) || gotPng.ContentType != "image/png" || gotPng.Width != 3 || gotPng.Height != 2 || gotPng.SHA256 != sha256Hex(png) || gotPng.UploadedByID != 0 { t.Errorf("unexpected png row %+v", gotPng) } gotSvg, _ := repo.Get(svgRow.ID) if gotSvg.ContentType != "image/svg+xml" || gotSvg.Width != 0 || gotSvg.Height != 0 || gotSvg.SHA256 != sha256Hex(svg) { t.Errorf("unexpected svg row %+v", gotSvg) } }) t.Run("A row whose file is missing does not stop the boot", func(t *testing.T) { db, repo, uploadDir := newMediaPersistence(t) row := seedLegacyRow(t, db, "gone.png") if err := useCases.NewBackfillImageMetadataUseCase(repo, storage.NewLocalStorage(uploadDir)).Run(); err != nil { t.Fatalf("backfill failed: %v", err) } got, _ := repo.Get(row.ID) if got.Filename != "" || got.SHA256 != "" { t.Errorf("expected the row to be left as is, got %+v", got) } }) t.Run("An unreadable file leaves its row as is and the others are still filled", func(t *testing.T) { db, repo, uploadDir := newMediaPersistence(t) writeFile(t, uploadDir, "broken.png", []byte("not an image")) writeFile(t, uploadDir, "fine.png", pngBytes(t, 3, 2)) broken, fine := seedLegacyRow(t, db, "broken.png"), seedLegacyRow(t, db, "fine.png") if err := useCases.NewBackfillImageMetadataUseCase(repo, storage.NewLocalStorage(uploadDir)).Run(); err != nil { t.Fatalf("backfill failed: %v", err) } if got, _ := repo.Get(broken.ID); got.Filename != "" { t.Errorf("expected the broken row to be left as is, got %+v", got) } if got, _ := repo.Get(fine.ID); got.Filename != "fine.png" { t.Errorf("expected the fine row to be filled, got %+v", got) } }) t.Run("The backfill is idempotent", func(t *testing.T) { db, repo, uploadDir := newMediaPersistence(t) writeFile(t, uploadDir, "one.png", pngBytes(t, 3, 2)) row := seedLegacyRow(t, db, "one.png") if err := useCases.NewBackfillImageMetadataUseCase(repo, storage.NewLocalStorage(uploadDir)).Run(); err != nil { t.Fatalf("backfill failed: %v", err) } afterFirst, _ := repo.Get(row.ID) if _, err := repo.Rename(row.ID, "renamed.png"); err != nil { t.Fatal(err) } if err := useCases.NewBackfillImageMetadataUseCase(repo, storage.NewLocalStorage(uploadDir)).Run(); err != nil { t.Fatalf("second backfill failed: %v", err) } afterSecond, _ := repo.Get(row.ID) if afterSecond.Filename != "renamed.png" || afterSecond.SHA256 != afterFirst.SHA256 { t.Errorf("expected the second run to change nothing, got %+v", afterSecond) } }) }