Files
RenewCMS/test/media_storage_orchestration_test.go
T

211 lines
8.4 KiB
Go

package test
import (
domainImage "RenewCMS/internal/domain/image"
"RenewCMS/internal/infrastructure/persistence/database"
"RenewCMS/internal/infrastructure/persistence/models"
"RenewCMS/internal/infrastructure/storage"
"RenewCMS/internal/infrastructure/useCases"
"bytes"
"errors"
"strings"
"testing"
"time"
)
func pngUpload(t *testing.T) domainImage.ImageInput {
t.Helper()
return domainImage.ImageInput{
Content: bytes.NewReader(pngBytes(t, 3, 2)),
Filename: "a.png",
ContentType: "image/png",
}
}
func uploadPng(t *testing.T, uc *useCases.CreateImageUseCase) domainImage.Image {
t.Helper()
created, err := uc.CreateImage(testCtx, pngUpload(t))
if err != nil {
t.Fatalf("CreateImage failed: %v", err)
}
return created
}
func TestCreateImageStoresTheBlobThenTheRow(t *testing.T) {
t.Run("The row points at the stored name and the blob holds the bytes", func(t *testing.T) {
repo, store := newMockImageRepo(), newMockImageStorage()
created := uploadPng(t, useCases.NewCreateImageUseCase(repo, store, discardRecorder))
if !strings.HasPrefix(created.Path, domainImage.PublicPathPrefix) {
t.Errorf("unexpected path %q", created.Path)
}
if !bytes.Equal(store.blobs[domainImage.StoredName(created.Path)], pngBytes(t, 3, 2)) {
t.Error("expected the blob to hold the uploaded bytes")
}
})
t.Run("A storage failure creates no row", func(t *testing.T) {
repo, store := newMockImageRepo(), newMockImageStorage()
store.failPut = errRepositoryFailure
_, err := useCases.NewCreateImageUseCase(repo, store, discardRecorder).CreateImage(testCtx, pngUpload(t))
if !errors.Is(err, errRepositoryFailure) || repo.createCalls != 0 {
t.Errorf("got %v with %d Create calls", err, repo.createCalls)
}
})
t.Run("A row failure removes the blob that was just stored", func(t *testing.T) {
repo, store := newMockImageRepo(), newMockImageStorage()
repo.fail.failNextWith("Create", errRepositoryFailure)
_, err := useCases.NewCreateImageUseCase(repo, store, discardRecorder).CreateImage(testCtx, pngUpload(t))
if !errors.Is(err, errRepositoryFailure) {
t.Fatalf("expected the repository failure, got %v", err)
}
if len(store.blobs) != 0 || len(store.deleted) != 1 {
t.Errorf("expected the orphan blob to be deleted, blobs=%d deleted=%v", len(store.blobs), store.deleted)
}
})
}
func TestDeleteMediaRemovesTheBlobAndTheRow(t *testing.T) {
t.Run("Both disappear", func(t *testing.T) {
repo, store := newMockImageRepo(), newMockImageStorage()
created := uploadPng(t, useCases.NewCreateImageUseCase(repo, store, discardRecorder))
if err := useCases.NewDeleteMediaUseCase(repo, store, newMockVariantFileRepo(), discardRecorder, plainSites(), plainAuthors()).Delete(testCtx, created.ID); err != nil {
t.Fatalf("Delete failed: %v", err)
}
if len(store.blobs) != 0 || len(repo.images) != 0 {
t.Errorf("expected nothing left, blobs=%d rows=%d", len(store.blobs), len(repo.images))
}
})
t.Run("An unknown id is ErrImageNotFound and touches no blob", func(t *testing.T) {
repo, store := newMockImageRepo(), newMockImageStorage()
err := useCases.NewDeleteMediaUseCase(repo, store, newMockVariantFileRepo(), discardRecorder, plainSites(), plainAuthors()).Delete(testCtx, 7)
if !errors.Is(err, domainImage.ErrImageNotFound) || len(store.deleted) != 0 {
t.Errorf("got %v, deleted %v", err, store.deleted)
}
})
t.Run("A blob failure keeps the row so the delete can be retried", func(t *testing.T) {
repo, store := newMockImageRepo(), newMockImageStorage()
created := uploadPng(t, useCases.NewCreateImageUseCase(repo, store, discardRecorder))
store.failDelete = errRepositoryFailure
err := useCases.NewDeleteMediaUseCase(repo, store, newMockVariantFileRepo(), discardRecorder, plainSites(), plainAuthors()).Delete(testCtx, created.ID)
if !errors.Is(err, errRepositoryFailure) || len(repo.images) != 1 || len(repo.deleteCalls) != 0 {
t.Errorf("got %v, rows=%d deleteCalls=%v", err, len(repo.images), repo.deleteCalls)
}
})
}
func TestMergeMediaRemovesTheMergedBlobs(t *testing.T) {
repo, articles, store := newMockImageRepo(), newMockArticleRepo(), newMockImageStorage()
create := useCases.NewCreateImageUseCase(repo, store, discardRecorder)
kept, copyOne, copyTwo := uploadPng(t, create), uploadPng(t, create), uploadPng(t, create)
result, err := useCases.NewMergeMediaUseCase(repo, articles, store, newMockVariantFileRepo(), discardRecorder, plainSites(), plainAuthors()).Merge(testCtx, kept.ID, []uint32{copyOne.ID, copyTwo.ID})
if err != nil || result.Merged != 2 {
t.Fatalf("merge returned %+v, %v", result, err)
}
if len(store.blobs) != 1 || store.blobs[domainImage.StoredName(kept.Path)] == nil {
t.Errorf("expected only the kept blob to remain, got %d blobs", len(store.blobs))
}
}
func TestBackfillImageMetadataUseCase(t *testing.T) {
t.Run("Rows without metadata are filled from the stored blob and unreadable ones are skipped", func(t *testing.T) {
repo, store := newMockImageRepo(), newMockImageStorage()
store.blobs["one.png"] = pngBytes(t, 3, 2)
store.blobs["broken.png"] = []byte("nope")
repo.images[1] = domainImage.Image{ID: 1, Path: domainImage.PublicPath("one.png")}
repo.images[2] = domainImage.Image{ID: 2, Path: domainImage.PublicPath("broken.png")}
repo.images[3] = domainImage.Image{ID: 3, Path: domainImage.PublicPath("missing.png")}
repo.images[4] = domainImage.Image{ID: 4, Path: domainImage.PublicPath("notes.txt")}
repo.images[5] = domainImage.Image{ID: 5, Path: domainImage.PublicPath("done.png"), Filename: "done.png"}
if err := useCases.NewBackfillImageMetadataUseCase(repo, store).Run(); err != nil {
t.Fatalf("Run failed: %v", err)
}
got := repo.images[1]
if got.Filename != "one.png" || got.ContentType != "image/png" || got.Width != 3 || got.Height != 2 || got.SHA256 != sha256Hex(store.blobs["one.png"]) {
t.Errorf("unexpected backfilled row %+v", got)
}
for _, id := range []uint32{2, 3, 4} {
if repo.images[id].Filename != "" {
t.Errorf("row %d must be left as is, got %+v", id, repo.images[id])
}
}
})
t.Run("Repository failures stop the run", func(t *testing.T) {
repo, store := newMockImageRepo(), newMockImageStorage()
repo.fail.failNextWith("ListMissingMetadata", errRepositoryFailure)
if err := useCases.NewBackfillImageMetadataUseCase(repo, store).Run(); !errors.Is(err, errRepositoryFailure) {
t.Errorf("expected the list failure, got %v", err)
}
repo, store = newMockImageRepo(), newMockImageStorage()
store.blobs["one.png"] = pngBytes(t, 1, 1)
repo.images[1] = domainImage.Image{ID: 1, Path: domainImage.PublicPath("one.png")}
repo.fail.failNextWith("UpdateMetadata", errRepositoryFailure)
if err := useCases.NewBackfillImageMetadataUseCase(repo, store).Run(); !errors.Is(err, errRepositoryFailure) {
t.Errorf("expected the update failure, got %v", err)
}
})
}
func TestDriverRegistries(t *testing.T) {
t.Run("An unknown database driver is rejected", func(t *testing.T) {
if _, err := database.Connect(database.Config{Driver: "oracle", Source: "x"}); err == nil || !strings.Contains(err.Error(), "oracle") {
t.Errorf("expected an unsupported driver error, got %v", err)
}
})
t.Run("An unknown storage driver is rejected", func(t *testing.T) {
if _, err := storage.Open(storage.Config{Driver: "floppy"}); err == nil || !strings.Contains(err.Error(), "floppy") {
t.Errorf("expected an unsupported driver error, got %v", err)
}
})
t.Run("The sqlite driver connects, migrates, backs up and closes", func(t *testing.T) {
dir := t.TempDir()
connection, err := database.Connect(database.Config{Driver: database.DriverSQLite, Source: dir + "/nested/app.db"})
if err != nil {
t.Fatalf("Connect failed: %v", err)
}
if err := database.Migrate(connection.DB, migrationPublicURL); err != nil {
t.Fatalf("Migrate failed: %v", err)
}
if err := connection.DB.Create(&models.Article{Title: "t"}).Error; err != nil {
t.Fatalf("expected the schema to exist: %v", err)
}
if _, err := connection.NewBackup(dir+"/backups", time.Now).Backup(); err != nil {
t.Errorf("Backup failed: %v", err)
}
if err := connection.Close(); err != nil {
t.Errorf("Close failed: %v", err)
}
})
t.Run("The storage registry returns a working local storage", func(t *testing.T) {
store, err := storage.Open(storage.Config{Driver: storage.DriverLocal, Location: t.TempDir()})
if err != nil {
t.Fatalf("Open failed: %v", err)
}
if _, err := store.Put("image/png", strings.NewReader("x")); err != nil {
t.Errorf("Put failed: %v", err)
}
})
}