refac: decouple persistence and image storage behind driver registries
This commit is contained in:
1 parent
421cfafec3
commit
81e299f09a
46 files changed
+1036
-374
No files matched your search
@@ -0,0 +1,210 @@
|
||||
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(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))
|
||||
|
||||
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).CreateImage(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).CreateImage(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))
|
||||
|
||||
if err := useCases.NewDeleteMediaUseCase(repo, store).Delete(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).Delete(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))
|
||||
store.failDelete = errRepositoryFailure
|
||||
|
||||
err := useCases.NewDeleteMediaUseCase(repo, store).Delete(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)
|
||||
kept, copyOne, copyTwo := uploadPng(t, create), uploadPng(t, create), uploadPng(t, create)
|
||||
|
||||
result, err := useCases.NewMergeMediaUseCase(repo, articles, store).Merge(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); 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user