Files
osdb-1-claude-opusplan/backend/internal/repository/fruit_repo_integration_test.go
T
juliaandClaude Sonnet 4.6 78c23557da feat: fruit search by name/synonym and type filter (story #06)
Backend: List repo query gains name (ILIKE with wildcard escaping on
name + synonym LEFT JOIN, SELECT DISTINCT) and types (ANY cast) params;
handler parses ?name= and ?type=, resolves combined-type aliases from
domain.FruitTypeAliases, validates plain types against validFruitTypes
to prevent Postgres enum cast errors. ORDER BY f.name, f.id for stable
pagination.

Frontend: listFruits gains optional {name, type} params; fruitStore adds
searchName/searchType state and setSearch action (resets offset, refetches);
FruitList.vue wires text input (debounced 300 ms + Enter) and flat type
dropdown (17 enum values + 4 aliases per spec §5 order); debounce timer
cleared on unmount.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 12:46:38 +02:00

149 lines
3.8 KiB
Go

package repository_test
import (
"context"
"os"
"testing"
"osdb/internal/database"
"osdb/internal/domain"
"osdb/internal/handler"
"osdb/internal/repository"
)
// pngFixture is a minimal valid PNG for byte round-trip testing.
var pngFixture = []byte{
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41,
0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00,
0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21, 0xbc,
0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
0x44, 0xae, 0x42, 0x60, 0x82,
}
func TestFruitRepoIntegration(t *testing.T) {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
t.Skip("DATABASE_URL not set — skipping integration test")
}
ctx := context.Background()
pool, err := database.Connect(ctx, dsn)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer pool.Close()
repo := repository.NewFruitRepo(pool)
// cleanup after test
t.Cleanup(func() {
if _, err := pool.Exec(ctx, `DELETE FROM fruits WHERE osdb_number LIKE 'TEST-%'`); err != nil {
t.Logf("cleanup: %v", err)
}
})
// Create
dto := domain.FruitWriteDTO{
Name: "Boskop",
OSDBNumber: "TEST-001",
FruitType: "Apfelsorten",
Synonyms: []string{"Boskop-Renette", "Schöner aus Boskoop"},
}
f, err := repo.Create(ctx, dto)
if err != nil {
t.Fatalf("Create: %v", err)
}
if f.ID == 0 {
t.Fatal("want non-zero ID")
}
if len(f.Synonyms) != 2 {
t.Fatalf("want 2 synonyms got %d", len(f.Synonyms))
}
// Duplicate osdb_number → ErrDuplicateOSDBNumber
_, err = repo.Create(ctx, dto)
if err != handler.ErrDuplicateOSDBNumber {
t.Fatalf("want ErrDuplicateOSDBNumber got %v", err)
}
// Get (synonyms present)
got, err := repo.Get(ctx, f.ID)
if err != nil {
t.Fatalf("Get: %v", err)
}
if len(got.Synonyms) != 2 {
t.Fatalf("want 2 synonyms got %d", len(got.Synonyms))
}
// Update (replaces synonyms wholesale)
updated, err := repo.Update(ctx, f.ID, domain.FruitWriteDTO{
Name: "Boskop Updated",
OSDBNumber: "TEST-001",
FruitType: "Apfelsorten",
Synonyms: []string{"Renamed"},
})
if err != nil {
t.Fatalf("Update: %v", err)
}
if updated.Name != "Boskop Updated" {
t.Fatalf("want updated name got %s", updated.Name)
}
if len(updated.Synonyms) != 1 || updated.Synonyms[0] != "Renamed" {
t.Fatalf("want [Renamed] got %v", updated.Synonyms)
}
// List
fruits, total, err := repo.List(ctx, 50, 0, "", nil)
if err != nil {
t.Fatalf("List: %v", err)
}
if total < 1 {
t.Fatalf("want total >= 1 got %d", total)
}
_ = fruits
// AddImage + GetImageData byte round-trip
filename := "test.png"
imageType := "fruit"
img, err := repo.AddImage(ctx, f.ID, &filename, pngFixture, imageType, nil)
if err != nil {
t.Fatalf("AddImage: %v", err)
}
if img.ID == 0 {
t.Fatal("want non-zero image ID")
}
data, err := repo.GetImageData(ctx, f.ID, img.ID)
if err != nil {
t.Fatalf("GetImageData: %v", err)
}
if len(data) != len(pngFixture) {
t.Fatalf("byte round-trip: want %d bytes got %d", len(pngFixture), len(data))
}
// DeleteImage
if err := repo.DeleteImage(ctx, f.ID, img.ID); err != nil {
t.Fatalf("DeleteImage: %v", err)
}
if _, err := repo.GetImageData(ctx, f.ID, img.ID); err != handler.ErrNotFound {
t.Fatalf("after delete: want ErrNotFound got %v", err)
}
// Delete (cascade check)
if err := repo.Delete(ctx, f.ID); err != nil {
t.Fatalf("Delete: %v", err)
}
if _, err := repo.Get(ctx, f.ID); err != handler.ErrNotFound {
t.Fatalf("after delete: want ErrNotFound got %v", err)
}
// Update non-existent → ErrNotFound
if _, err := repo.Update(ctx, 999999, dto); err != handler.ErrNotFound {
t.Fatalf("Update non-existent: want ErrNotFound got %v", err)
}
}