211 lines
8.5 KiB
Go
211 lines
8.5 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(), newMockArticleRepo()).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(), newMockArticleRepo()).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(), newMockArticleRepo()).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)
|
|
}
|
|
})
|
|
}
|