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