Files
RenewCMS/test/media_persistence_test.go

236 lines
8.9 KiB
Go

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)
}
})
}