Full CRUD for publications; link fruits to publications; upload per-fruit PDF descriptions and fruit images stored as BYTEA; fruit detail page shows publication descriptions and images. ImageOverlayDrawer for cover thumbnail full-size view. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
5.5 KiB
Go
184 lines
5.5 KiB
Go
package repository_test
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"osdb/internal/domain"
|
|
"osdb/internal/repository"
|
|
)
|
|
|
|
func TestPublicationRepo_Integration(t *testing.T) {
|
|
dsn := os.Getenv("DATABASE_URL")
|
|
if dsn == "" {
|
|
t.Skip("DATABASE_URL not set — skipping integration tests")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
pool, err := pgxpool.New(ctx, dsn)
|
|
if err != nil {
|
|
t.Fatalf("connect: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
repo := repository.NewPublicationRepo(pool)
|
|
fruitRepo := repository.NewFruitRepo(pool)
|
|
|
|
// Pre-cleanup stale test data from crashed prior runs
|
|
_, _ = pool.Exec(ctx, "DELETE FROM fruits WHERE osdb_number = $1", "TEST-PUB-001")
|
|
|
|
// Seed a fruit for linking
|
|
fruit, err := fruitRepo.Create(ctx, domain.FruitWriteDTO{
|
|
Name: "Testfrucht",
|
|
OSDBNumber: "TEST-PUB-001",
|
|
FruitType: "Apfelsorten",
|
|
Synonyms: []string{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("seed fruit: %v", err)
|
|
}
|
|
t.Cleanup(func() { fruitRepo.Delete(ctx, fruit.ID) }) //nolint:errcheck
|
|
|
|
t.Run("create and get publication", func(t *testing.T) {
|
|
pub, err := repo.Create(ctx, domain.PublicationWriteDTO{
|
|
Title: "Pomologie Test",
|
|
OSDBPubID: "TEST-PUB-INTEG-001",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
t.Cleanup(func() { repo.Delete(ctx, pub.ID) }) //nolint:errcheck
|
|
|
|
if pub.ID == 0 {
|
|
t.Fatal("want non-zero ID")
|
|
}
|
|
|
|
got, err := repo.Get(ctx, pub.ID)
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if got.Title != "Pomologie Test" {
|
|
t.Fatalf("want 'Pomologie Test' got %s", got.Title)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate osdb_pub_id → ErrDuplicateOSDBPubID", func(t *testing.T) {
|
|
pub, _ := repo.Create(ctx, domain.PublicationWriteDTO{Title: "Dup", OSDBPubID: "TEST-PUB-DUP-001"})
|
|
t.Cleanup(func() { repo.Delete(ctx, pub.ID) }) //nolint:errcheck
|
|
|
|
_, err := repo.Create(ctx, domain.PublicationWriteDTO{Title: "Dup2", OSDBPubID: "TEST-PUB-DUP-001"})
|
|
if err == nil {
|
|
t.Fatal("want error for duplicate osdb_pub_id")
|
|
}
|
|
})
|
|
|
|
t.Run("cover image round-trip", func(t *testing.T) {
|
|
pub, _ := repo.Create(ctx, domain.PublicationWriteDTO{Title: "Img Test", OSDBPubID: "TEST-PUB-IMG-001"})
|
|
t.Cleanup(func() { repo.Delete(ctx, pub.ID) }) //nolint:errcheck
|
|
|
|
imgData := []byte{0x89, 0x50, 0x4e, 0x47} // PNG magic
|
|
if err := repo.SetImage(ctx, pub.ID, imgData); err != nil {
|
|
t.Fatalf("set image: %v", err)
|
|
}
|
|
data, err := repo.GetImageData(ctx, pub.ID)
|
|
if err != nil {
|
|
t.Fatalf("get image data: %v", err)
|
|
}
|
|
if string(data) != string(imgData) {
|
|
t.Fatal("image data mismatch")
|
|
}
|
|
if err := repo.DeleteImage(ctx, pub.ID); err != nil {
|
|
t.Fatalf("delete image: %v", err)
|
|
}
|
|
if _, err := repo.GetImageData(ctx, pub.ID); err == nil {
|
|
t.Fatal("want not found after delete")
|
|
}
|
|
})
|
|
|
|
t.Run("link and unlink fruit", func(t *testing.T) {
|
|
pub, _ := repo.Create(ctx, domain.PublicationWriteDTO{Title: "Link Test", OSDBPubID: "TEST-PUB-LINK-001"})
|
|
t.Cleanup(func() { repo.Delete(ctx, pub.ID) }) //nolint:errcheck
|
|
|
|
if err := repo.LinkFruit(ctx, pub.ID, fruit.ID); err != nil {
|
|
t.Fatalf("link fruit: %v", err)
|
|
}
|
|
fruits, err := repo.ListFruits(ctx, pub.ID)
|
|
if err != nil {
|
|
t.Fatalf("list fruits: %v", err)
|
|
}
|
|
if len(fruits) != 1 {
|
|
t.Fatalf("want 1 fruit got %d", len(fruits))
|
|
}
|
|
if err := repo.UnlinkFruit(ctx, pub.ID, fruit.ID); err != nil {
|
|
t.Fatalf("unlink fruit: %v", err)
|
|
}
|
|
fruits, _ = repo.ListFruits(ctx, pub.ID)
|
|
if len(fruits) != 0 {
|
|
t.Fatalf("want 0 fruits after unlink got %d", len(fruits))
|
|
}
|
|
})
|
|
|
|
t.Run("description upload and serve", func(t *testing.T) {
|
|
pub, _ := repo.Create(ctx, domain.PublicationWriteDTO{Title: "Desc Test", OSDBPubID: "TEST-PUB-DESC-001"})
|
|
t.Cleanup(func() { repo.Delete(ctx, pub.ID) }) //nolint:errcheck
|
|
|
|
pdfData := []byte("%PDF-1.4 test content")
|
|
desc, err := repo.AddDescription(ctx, pub.ID, fruit.ID, pdfData)
|
|
if err != nil {
|
|
t.Fatalf("add description: %v", err)
|
|
}
|
|
data, err := repo.GetDescriptionData(ctx, pub.ID, desc.ID)
|
|
if err != nil {
|
|
t.Fatalf("get desc data: %v", err)
|
|
}
|
|
if string(data) != string(pdfData) {
|
|
t.Fatal("pdf data mismatch")
|
|
}
|
|
if err := repo.DeleteDescription(ctx, pub.ID, desc.ID); err != nil {
|
|
t.Fatalf("delete desc: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("fruit image upload and serve", func(t *testing.T) {
|
|
pub, _ := repo.Create(ctx, domain.PublicationWriteDTO{Title: "FruitImg Test", OSDBPubID: "TEST-PUB-FI-001"})
|
|
t.Cleanup(func() { repo.Delete(ctx, pub.ID) }) //nolint:errcheck
|
|
|
|
imgData := []byte{0x89, 0x50, 0x4e, 0x47}
|
|
fname := "test.png"
|
|
img, err := repo.AddFruitImage(ctx, pub.ID, fruit.ID, &fname, imgData)
|
|
if err != nil {
|
|
t.Fatalf("add fruit image: %v", err)
|
|
}
|
|
if img.ImageType != "fruit" {
|
|
t.Fatalf("want image_type=fruit got %s", img.ImageType)
|
|
}
|
|
data, err := repo.GetFruitImageData(ctx, pub.ID, img.ID)
|
|
if err != nil {
|
|
t.Fatalf("get fruit image data: %v", err)
|
|
}
|
|
if string(data) != string(imgData) {
|
|
t.Fatal("image data mismatch")
|
|
}
|
|
if err := repo.DeleteFruitImage(ctx, pub.ID, img.ID); err != nil {
|
|
t.Fatalf("delete fruit image: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("GetFruitDescriptions returns descriptions across publications", func(t *testing.T) {
|
|
pub, _ := repo.Create(ctx, domain.PublicationWriteDTO{Title: "CrossPub", OSDBPubID: "TEST-PUB-CROSS-001"})
|
|
t.Cleanup(func() { repo.Delete(ctx, pub.ID) }) //nolint:errcheck
|
|
|
|
_, _ = repo.AddDescription(ctx, pub.ID, fruit.ID, []byte("%PDF-1.4"))
|
|
descs, err := repo.GetFruitDescriptions(ctx, fruit.ID)
|
|
if err != nil {
|
|
t.Fatalf("get fruit descriptions: %v", err)
|
|
}
|
|
if len(descs) == 0 {
|
|
t.Fatal("want at least 1 description")
|
|
}
|
|
})
|
|
}
|