254 lines
9.3 KiB
Go
254 lines
9.3 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"
|
|
"bytes"
|
|
"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.Image{}); err != nil {
|
|
t.Fatalf("failed to migrate: %v", err)
|
|
}
|
|
uploadDir := t.TempDir()
|
|
return db, persistence.NewImageRepository(db, uploadDir), uploadDir
|
|
}
|
|
|
|
func createStoredImage(t *testing.T, repo *persistence.ImageRepository, filename string, content []byte, contentType string) domainImage.Image {
|
|
t.Helper()
|
|
created, err := repo.Create(domainImage.ImageInput{
|
|
Content: bytes.NewReader(content),
|
|
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 file and the metadata and returns the assigned id", func(t *testing.T) {
|
|
_, repo, uploadDir := 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)
|
|
}
|
|
onDisk, err := os.ReadFile(filepath.Join(uploadDir, filepath.Base(created.Path)))
|
|
if err != nil || !bytes.Equal(onDisk, content) {
|
|
t.Errorf("expected the file on disk, got %v", err)
|
|
}
|
|
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 file and the row", func(t *testing.T) {
|
|
_, repo, uploadDir := 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 := os.Stat(filepath.Join(uploadDir, filepath.Base(created.Path))); !os.IsNotExist(err) {
|
|
t.Errorf("expected the file to be gone")
|
|
}
|
|
if _, err := repo.Get(created.ID); !errors.Is(err, domainImage.ErrImageNotFound) {
|
|
t.Errorf("expected the row to be gone, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("A file already missing from disk still deletes the row", func(t *testing.T) {
|
|
_, repo, uploadDir := newMediaPersistence(t)
|
|
created := createStoredImage(t, repo, "a.png", pngBytes(t, 3, 2), "image/png")
|
|
if err := os.Remove(filepath.Join(uploadDir, filepath.Base(created.Path))); err != nil {
|
|
t.Fatalf("failed to remove the file: %v", err)
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
|
|
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 := persistence.BackfillImageMetadata(db, uploadDir); 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 := persistence.BackfillImageMetadata(db, uploadDir); 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 := persistence.BackfillImageMetadata(db, uploadDir); 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 := persistence.BackfillImageMetadata(db, uploadDir); 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 := persistence.BackfillImageMetadata(db, uploadDir); 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)
|
|
}
|
|
})
|
|
}
|