- Auto-crop thumbnails on image upload (Go stdlib, no external deps): find non-background bounding box, pad 5px, scale to ≤300px, encode JPEG - Add thumbnail_data BYTEA column to fruit_images and publication_fruit_images - New GET /api/v1/fruits/:id/images/:imageId/thumbnail endpoint (fallback to full data) - New GET /api/v1/publications/:id/fruit-images/:imgId/thumbnail endpoint - New POST /api/v1/admin/backfill-thumbnails to retroactively generate thumbnails - ListFruits response now includes synonyms and thumbnail_url per fruit - Replace FruitList table with responsive FruitCard grid (thumbnail + name + OSDB ID + type + synonyms) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1009 lines
29 KiB
Go
1009 lines
29 KiB
Go
package handler_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
|
|
"osdb/internal/domain"
|
|
"osdb/internal/handler"
|
|
)
|
|
|
|
// pdfFixture is a minimal PDF magic-byte prefix.
|
|
var pdfFixture = []byte("%PDF-1.4 test")
|
|
|
|
// fakePubRepo is an in-memory implementation of handler.PublicationRepository.
|
|
type fakePubRepo struct {
|
|
pubs map[int]domain.Publication
|
|
pubFruits map[int][]domain.PublicationFruit // pubID → fruits
|
|
descs map[int]domain.PublicationDescription
|
|
descData map[int][]byte
|
|
fruitImages map[int]domain.PublicationFruitImage
|
|
fruitImgData map[int][]byte
|
|
imageData map[int][]byte // pubID → cover image bytes
|
|
nextPubID int
|
|
nextDescID int
|
|
nextImgID int
|
|
forceDup bool
|
|
forceDupDesc bool
|
|
}
|
|
|
|
func newFakePubRepo() *fakePubRepo {
|
|
return &fakePubRepo{
|
|
pubs: make(map[int]domain.Publication),
|
|
pubFruits: make(map[int][]domain.PublicationFruit),
|
|
descs: make(map[int]domain.PublicationDescription),
|
|
descData: make(map[int][]byte),
|
|
fruitImages: make(map[int]domain.PublicationFruitImage),
|
|
fruitImgData: make(map[int][]byte),
|
|
imageData: make(map[int][]byte),
|
|
nextPubID: 1,
|
|
nextDescID: 1,
|
|
nextImgID: 1,
|
|
}
|
|
}
|
|
|
|
func (r *fakePubRepo) List(_ context.Context) ([]domain.Publication, error) {
|
|
pubs := make([]domain.Publication, 0, len(r.pubs))
|
|
for _, p := range r.pubs {
|
|
pubs = append(pubs, p)
|
|
}
|
|
return pubs, nil
|
|
}
|
|
|
|
func (r *fakePubRepo) Get(_ context.Context, id int) (domain.Publication, error) {
|
|
p, ok := r.pubs[id]
|
|
if !ok {
|
|
return domain.Publication{}, handler.ErrNotFound
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (r *fakePubRepo) Create(_ context.Context, dto domain.PublicationWriteDTO) (domain.Publication, error) {
|
|
if r.forceDup {
|
|
return domain.Publication{}, handler.ErrDuplicateOSDBPubID
|
|
}
|
|
id := r.nextPubID
|
|
r.nextPubID++
|
|
p := domain.Publication{ID: id, Title: dto.Title, Author: dto.Author, OSDBPubID: dto.OSDBPubID}
|
|
r.pubs[id] = p
|
|
return p, nil
|
|
}
|
|
|
|
func (r *fakePubRepo) Update(_ context.Context, id int, dto domain.PublicationWriteDTO) (domain.Publication, error) {
|
|
if r.forceDup {
|
|
return domain.Publication{}, handler.ErrDuplicateOSDBPubID
|
|
}
|
|
p, ok := r.pubs[id]
|
|
if !ok {
|
|
return domain.Publication{}, handler.ErrNotFound
|
|
}
|
|
p.Title = dto.Title
|
|
p.Author = dto.Author
|
|
p.OSDBPubID = dto.OSDBPubID
|
|
r.pubs[id] = p
|
|
return p, nil
|
|
}
|
|
|
|
func (r *fakePubRepo) Delete(_ context.Context, id int) error {
|
|
if _, ok := r.pubs[id]; !ok {
|
|
return handler.ErrNotFound
|
|
}
|
|
delete(r.pubs, id)
|
|
return nil
|
|
}
|
|
|
|
func (r *fakePubRepo) SetImage(_ context.Context, pubID int, data []byte) error {
|
|
if _, ok := r.pubs[pubID]; !ok {
|
|
return handler.ErrNotFound
|
|
}
|
|
r.imageData[pubID] = data
|
|
p := r.pubs[pubID]
|
|
url := fmt.Sprintf("/api/v1/publications/%d/image", pubID)
|
|
p.ImageURL = &url
|
|
r.pubs[pubID] = p
|
|
return nil
|
|
}
|
|
|
|
func (r *fakePubRepo) GetImageData(_ context.Context, pubID int) ([]byte, error) {
|
|
data, ok := r.imageData[pubID]
|
|
if !ok {
|
|
return nil, handler.ErrNotFound
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func (r *fakePubRepo) DeleteImage(_ context.Context, pubID int) error {
|
|
if _, ok := r.imageData[pubID]; !ok {
|
|
return handler.ErrNotFound
|
|
}
|
|
delete(r.imageData, pubID)
|
|
p := r.pubs[pubID]
|
|
p.ImageURL = nil
|
|
r.pubs[pubID] = p
|
|
return nil
|
|
}
|
|
|
|
func (r *fakePubRepo) ListFruits(_ context.Context, pubID int) ([]domain.PublicationFruit, error) {
|
|
if _, ok := r.pubs[pubID]; !ok {
|
|
return nil, handler.ErrNotFound
|
|
}
|
|
return r.pubFruits[pubID], nil
|
|
}
|
|
|
|
func (r *fakePubRepo) LinkFruit(_ context.Context, pubID, fruitID int) error {
|
|
if _, ok := r.pubs[pubID]; !ok {
|
|
return handler.ErrNotFound
|
|
}
|
|
r.pubFruits[pubID] = append(r.pubFruits[pubID], domain.PublicationFruit{FruitID: fruitID, Name: "Fruit", OSDBNumber: "X001"})
|
|
return nil
|
|
}
|
|
|
|
func (r *fakePubRepo) UnlinkFruit(_ context.Context, pubID, fruitID int) error {
|
|
fruits, ok := r.pubFruits[pubID]
|
|
if !ok {
|
|
return handler.ErrNotFound
|
|
}
|
|
found := false
|
|
remaining := fruits[:0]
|
|
for _, f := range fruits {
|
|
if f.FruitID == fruitID {
|
|
found = true
|
|
} else {
|
|
remaining = append(remaining, f)
|
|
}
|
|
}
|
|
if !found {
|
|
return handler.ErrNotFound
|
|
}
|
|
r.pubFruits[pubID] = remaining
|
|
return nil
|
|
}
|
|
|
|
func (r *fakePubRepo) ListDescriptions(_ context.Context, pubID int) ([]domain.PublicationDescription, error) {
|
|
if _, ok := r.pubs[pubID]; !ok {
|
|
return nil, handler.ErrNotFound
|
|
}
|
|
var result []domain.PublicationDescription
|
|
for _, d := range r.descs {
|
|
if d.PublicationID == pubID {
|
|
result = append(result, d)
|
|
}
|
|
}
|
|
if result == nil {
|
|
result = []domain.PublicationDescription{}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (r *fakePubRepo) AddDescription(_ context.Context, pubID, fruitID int, data []byte) (domain.PublicationDescription, error) {
|
|
if _, ok := r.pubs[pubID]; !ok {
|
|
return domain.PublicationDescription{}, handler.ErrNotFound
|
|
}
|
|
if r.forceDupDesc {
|
|
return domain.PublicationDescription{}, handler.ErrDuplicatePubDescription
|
|
}
|
|
id := r.nextDescID
|
|
r.nextDescID++
|
|
d := domain.PublicationDescription{
|
|
ID: id,
|
|
PublicationID: pubID,
|
|
FruitID: fruitID,
|
|
URL: fmt.Sprintf("/api/v1/publications/%d/descriptions/%d", pubID, id),
|
|
}
|
|
r.descs[id] = d
|
|
r.descData[id] = data
|
|
return d, nil
|
|
}
|
|
|
|
func (r *fakePubRepo) GetDescriptionData(_ context.Context, pubID, descID int) ([]byte, error) {
|
|
d, ok := r.descs[descID]
|
|
if !ok || d.PublicationID != pubID {
|
|
return nil, handler.ErrNotFound
|
|
}
|
|
return r.descData[descID], nil
|
|
}
|
|
|
|
func (r *fakePubRepo) DeleteDescription(_ context.Context, pubID, descID int) error {
|
|
d, ok := r.descs[descID]
|
|
if !ok || d.PublicationID != pubID {
|
|
return handler.ErrNotFound
|
|
}
|
|
delete(r.descs, descID)
|
|
delete(r.descData, descID)
|
|
return nil
|
|
}
|
|
|
|
func (r *fakePubRepo) ListFruitImages(_ context.Context, pubID int) ([]domain.PublicationFruitImage, error) {
|
|
if _, ok := r.pubs[pubID]; !ok {
|
|
return nil, handler.ErrNotFound
|
|
}
|
|
var result []domain.PublicationFruitImage
|
|
for _, img := range r.fruitImages {
|
|
if img.PublicationID == pubID {
|
|
result = append(result, img)
|
|
}
|
|
}
|
|
if result == nil {
|
|
result = []domain.PublicationFruitImage{}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (r *fakePubRepo) AddFruitImage(_ context.Context, pubID, fruitID int, filename *string, data []byte) (domain.PublicationFruitImage, error) {
|
|
if _, ok := r.pubs[pubID]; !ok {
|
|
return domain.PublicationFruitImage{}, handler.ErrNotFound
|
|
}
|
|
id := r.nextImgID
|
|
r.nextImgID++
|
|
img := domain.PublicationFruitImage{
|
|
ID: id,
|
|
PublicationID: pubID,
|
|
FruitID: fruitID,
|
|
Filename: filename,
|
|
ImageType: "fruit",
|
|
URL: fmt.Sprintf("/api/v1/publications/%d/fruit-images/%d", pubID, id),
|
|
}
|
|
r.fruitImages[id] = img
|
|
r.fruitImgData[id] = data
|
|
return img, nil
|
|
}
|
|
|
|
func (r *fakePubRepo) GetFruitImageData(_ context.Context, pubID, imgID int) ([]byte, error) {
|
|
img, ok := r.fruitImages[imgID]
|
|
if !ok || img.PublicationID != pubID {
|
|
return nil, handler.ErrNotFound
|
|
}
|
|
return r.fruitImgData[imgID], nil
|
|
}
|
|
|
|
func (r *fakePubRepo) GetFruitImageThumbnailData(_ context.Context, pubID, imgID int) ([]byte, error) {
|
|
img, ok := r.fruitImages[imgID]
|
|
if !ok || img.PublicationID != pubID {
|
|
return nil, handler.ErrNotFound
|
|
}
|
|
return r.fruitImgData[imgID], nil
|
|
}
|
|
|
|
func (r *fakePubRepo) DeleteFruitImage(_ context.Context, pubID, imgID int) error {
|
|
img, ok := r.fruitImages[imgID]
|
|
if !ok || img.PublicationID != pubID {
|
|
return handler.ErrNotFound
|
|
}
|
|
delete(r.fruitImages, imgID)
|
|
delete(r.fruitImgData, imgID)
|
|
return nil
|
|
}
|
|
|
|
func (r *fakePubRepo) GetFruitDescriptions(_ context.Context, fruitID int) ([]domain.PublicationDescription, error) {
|
|
var result []domain.PublicationDescription
|
|
for _, d := range r.descs {
|
|
if d.FruitID == fruitID {
|
|
result = append(result, d)
|
|
}
|
|
}
|
|
if result == nil {
|
|
result = []domain.PublicationDescription{}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (r *fakePubRepo) GetFruitPublicationImages(_ context.Context, fruitID int) ([]domain.PublicationFruitImage, error) {
|
|
var result []domain.PublicationFruitImage
|
|
for _, img := range r.fruitImages {
|
|
if img.FruitID == fruitID {
|
|
result = append(result, img)
|
|
}
|
|
}
|
|
if result == nil {
|
|
result = []domain.PublicationFruitImage{}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// helpers
|
|
|
|
func buildFileUpload(t *testing.T, fieldName, filename string, data []byte, extraFields map[string]string) (*bytes.Buffer, string) {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
w := multipart.NewWriter(&buf)
|
|
if data != nil {
|
|
fw, _ := w.CreateFormFile(fieldName, filename)
|
|
fw.Write(data)
|
|
}
|
|
for k, v := range extraFields {
|
|
w.WriteField(k, v)
|
|
}
|
|
w.Close()
|
|
return &buf, w.FormDataContentType()
|
|
}
|
|
|
|
func newPubHandler() *handler.PublicationHandler {
|
|
return handler.NewPublicationHandler(newFakePubRepo())
|
|
}
|
|
|
|
// -- List --
|
|
|
|
func TestPubList_200(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
if err := h.List(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", rec.Code)
|
|
}
|
|
var pubs []domain.Publication
|
|
json.Unmarshal(rec.Body.Bytes(), &pubs)
|
|
if len(pubs) != 1 {
|
|
t.Fatalf("want 1 publication got %d", len(pubs))
|
|
}
|
|
}
|
|
|
|
// -- Create --
|
|
|
|
func TestPubCreate_201(t *testing.T) {
|
|
h := newPubHandler()
|
|
e := newEcho()
|
|
body := jsonBody(map[string]any{"title": "Pomologie", "osdb_pub_id": "P001"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/publications", body)
|
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
if err := h.Create(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("want 201 got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
var p domain.Publication
|
|
json.Unmarshal(rec.Body.Bytes(), &p)
|
|
if p.ID == 0 {
|
|
t.Fatal("want non-zero ID")
|
|
}
|
|
if p.Title != "Pomologie" {
|
|
t.Fatalf("want title Pomologie got %s", p.Title)
|
|
}
|
|
}
|
|
|
|
func TestPubCreate_422_MissingTitle(t *testing.T) {
|
|
h := newPubHandler()
|
|
e := newEcho()
|
|
body := jsonBody(map[string]any{"osdb_pub_id": "P001"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/publications", body)
|
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
if err := h.Create(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusUnprocessableEntity {
|
|
t.Fatalf("want 422 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestPubCreate_422_MissingOSDBPubID(t *testing.T) {
|
|
h := newPubHandler()
|
|
e := newEcho()
|
|
body := jsonBody(map[string]any{"title": "Pomologie"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/publications", body)
|
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
if err := h.Create(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusUnprocessableEntity {
|
|
t.Fatalf("want 422 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestPubCreate_409_Duplicate(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.forceDup = true
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
body := jsonBody(map[string]any{"title": "Pomologie", "osdb_pub_id": "P001"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/publications", body)
|
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
if err := h.Create(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusConflict {
|
|
t.Fatalf("want 409 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// -- Get --
|
|
|
|
func TestPubGet_200(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.Get(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestPubGet_404(t *testing.T) {
|
|
h := newPubHandler()
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications/99", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("99")
|
|
if err := h.Get(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("want 404 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestPubGet_BadID(t *testing.T) {
|
|
h := newPubHandler()
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications/abc", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("abc")
|
|
if err := h.Get(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("want 400 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// -- Update --
|
|
|
|
func TestPubUpdate_200(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Old", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
body := jsonBody(map[string]any{"title": "New Title", "osdb_pub_id": "P001"})
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/publications/1", body)
|
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.Update(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", rec.Code)
|
|
}
|
|
var p domain.Publication
|
|
json.Unmarshal(rec.Body.Bytes(), &p)
|
|
if p.Title != "New Title" {
|
|
t.Fatalf("want 'New Title' got %s", p.Title)
|
|
}
|
|
}
|
|
|
|
func TestPubUpdate_404(t *testing.T) {
|
|
h := newPubHandler()
|
|
e := newEcho()
|
|
body := jsonBody(map[string]any{"title": "X", "osdb_pub_id": "P001"})
|
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/publications/99", body)
|
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("99")
|
|
if err := h.Update(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("want 404 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// -- Delete --
|
|
|
|
func TestPubDelete_204(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/publications/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.Delete(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("want 204 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestPubDelete_404(t *testing.T) {
|
|
h := newPubHandler()
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/publications/99", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("99")
|
|
if err := h.Delete(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("want 404 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// -- Cover image --
|
|
|
|
func TestPubUploadImage_201(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
buf, ct := buildFileUpload(t, "image", "cover.png", pngFixture, nil)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/publications/1/image", buf)
|
|
req.Header.Set(echo.HeaderContentType, ct)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.UploadCoverImage(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("want 201 got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestPubServeImage_200(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
repo.imageData[1] = pngFixture
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications/1/image", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.ServeCoverImage(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", rec.Code)
|
|
}
|
|
if !bytes.Equal(rec.Body.Bytes(), pngFixture) {
|
|
t.Fatal("body bytes mismatch")
|
|
}
|
|
if ct := rec.Header().Get("Content-Type"); ct != "image/png" {
|
|
t.Fatalf("want image/png got %s", ct)
|
|
}
|
|
}
|
|
|
|
func TestPubDeleteImage_204(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
repo.imageData[1] = pngFixture
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/publications/1/image", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.DeleteCoverImage(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("want 204 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestPubServeImage_404_NoImage(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications/1/image", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.ServeCoverImage(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("want 404 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// -- Linked fruits --
|
|
|
|
func TestPubListFruits_200(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
repo.pubFruits[1] = []domain.PublicationFruit{{FruitID: 2, Name: "Boskop", OSDBNumber: "A001"}}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications/1/fruits", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.ListFruits(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", rec.Code)
|
|
}
|
|
var fruits []domain.PublicationFruit
|
|
json.Unmarshal(rec.Body.Bytes(), &fruits)
|
|
if len(fruits) != 1 {
|
|
t.Fatalf("want 1 fruit got %d", len(fruits))
|
|
}
|
|
}
|
|
|
|
func TestPubLinkFruit_201(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
body := jsonBody(map[string]any{"fruit_id": 2})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/publications/1/fruits", body)
|
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.LinkFruit(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("want 201 got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestPubUnlinkFruit_204(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
repo.pubFruits[1] = []domain.PublicationFruit{{FruitID: 2, Name: "Boskop", OSDBNumber: "A001"}}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/publications/1/fruits/2", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id", "fruitId")
|
|
c.SetParamValues("1", "2")
|
|
if err := h.UnlinkFruit(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("want 204 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestPubUnlinkFruit_404(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/publications/1/fruits/99", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id", "fruitId")
|
|
c.SetParamValues("1", "99")
|
|
if err := h.UnlinkFruit(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("want 404 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// -- Descriptions (PDFs) --
|
|
|
|
func TestPubListDescriptions_200(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications/1/descriptions", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.ListDescriptions(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", rec.Code)
|
|
}
|
|
var descs []domain.PublicationDescription
|
|
json.Unmarshal(rec.Body.Bytes(), &descs)
|
|
if len(descs) != 0 {
|
|
t.Fatalf("want 0 descriptions got %d", len(descs))
|
|
}
|
|
}
|
|
|
|
func TestPubUploadDescription_201(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
buf, ct := buildFileUpload(t, "pdf", "desc.pdf", pdfFixture, map[string]string{"fruit_id": "2"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/publications/1/descriptions", buf)
|
|
req.Header.Set(echo.HeaderContentType, ct)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.UploadDescription(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("want 201 got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestPubUploadDescription_409_Duplicate(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
repo.forceDupDesc = true
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
buf, ct := buildFileUpload(t, "pdf", "desc.pdf", pdfFixture, map[string]string{"fruit_id": "2"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/publications/1/descriptions", buf)
|
|
req.Header.Set(echo.HeaderContentType, ct)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.UploadDescription(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusConflict {
|
|
t.Fatalf("want 409 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestPubServeDescription_200(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
repo.descs[1] = domain.PublicationDescription{ID: 1, PublicationID: 1, FruitID: 2, URL: "/api/v1/publications/1/descriptions/1"}
|
|
repo.descData[1] = pdfFixture
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications/1/descriptions/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id", "descId")
|
|
c.SetParamValues("1", "1")
|
|
if err := h.ServeDescription(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", rec.Code)
|
|
}
|
|
if ct := rec.Header().Get("Content-Type"); ct != "application/pdf" {
|
|
t.Fatalf("want application/pdf got %s", ct)
|
|
}
|
|
if !bytes.Equal(rec.Body.Bytes(), pdfFixture) {
|
|
t.Fatal("body bytes mismatch")
|
|
}
|
|
}
|
|
|
|
func TestPubDeleteDescription_204(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
repo.descs[1] = domain.PublicationDescription{ID: 1, PublicationID: 1, FruitID: 2}
|
|
repo.descData[1] = pdfFixture
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/publications/1/descriptions/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id", "descId")
|
|
c.SetParamValues("1", "1")
|
|
if err := h.DeleteDescription(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("want 204 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// -- Fruit images --
|
|
|
|
func TestPubListFruitImages_200(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications/1/fruit-images", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.ListFruitImages(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestPubUploadFruitImage_201(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
buf, ct := buildFileUpload(t, "image", "fruit.png", pngFixture, map[string]string{"fruit_id": "2"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/publications/1/fruit-images", buf)
|
|
req.Header.Set(echo.HeaderContentType, ct)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("1")
|
|
if err := h.UploadFruitImage(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("want 201 got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
var img domain.PublicationFruitImage
|
|
json.Unmarshal(rec.Body.Bytes(), &img)
|
|
if img.ImageType != "fruit" {
|
|
t.Fatalf("want image_type=fruit got %s", img.ImageType)
|
|
}
|
|
}
|
|
|
|
func TestPubServeFruitImage_200(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
repo.fruitImages[1] = domain.PublicationFruitImage{ID: 1, PublicationID: 1, FruitID: 2, ImageType: "fruit"}
|
|
repo.fruitImgData[1] = pngFixture
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications/1/fruit-images/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id", "imgId")
|
|
c.SetParamValues("1", "1")
|
|
if err := h.ServeFruitImage(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", rec.Code)
|
|
}
|
|
if !bytes.Equal(rec.Body.Bytes(), pngFixture) {
|
|
t.Fatal("body bytes mismatch")
|
|
}
|
|
}
|
|
|
|
func TestPubDeleteFruitImage_204(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
repo.fruitImages[1] = domain.PublicationFruitImage{ID: 1, PublicationID: 1, FruitID: 2, ImageType: "fruit"}
|
|
repo.fruitImgData[1] = pngFixture
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/publications/1/fruit-images/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id", "imgId")
|
|
c.SetParamValues("1", "1")
|
|
if err := h.DeleteFruitImage(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("want 204 got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
// -- Fruit-scoped reads --
|
|
|
|
func TestFruitGetDescriptions_200(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.descs[1] = domain.PublicationDescription{ID: 1, PublicationID: 1, FruitID: 5, URL: "/api/v1/publications/1/descriptions/1"}
|
|
repo.descData[1] = pdfFixture
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/fruits/5/descriptions", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("5")
|
|
if err := h.GetFruitDescriptions(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", rec.Code)
|
|
}
|
|
var descs []domain.PublicationDescription
|
|
json.Unmarshal(rec.Body.Bytes(), &descs)
|
|
if len(descs) != 1 {
|
|
t.Fatalf("want 1 description got %d", len(descs))
|
|
}
|
|
}
|
|
|
|
func TestFruitGetPublicationImages_200(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.fruitImages[1] = domain.PublicationFruitImage{ID: 1, PublicationID: 1, FruitID: 5, ImageType: "fruit"}
|
|
repo.fruitImgData[1] = pngFixture
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/fruits/5/publication-images", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id")
|
|
c.SetParamValues("5")
|
|
if err := h.GetFruitPublicationImages(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("want 200 got %d", rec.Code)
|
|
}
|
|
var imgs []domain.PublicationFruitImage
|
|
json.Unmarshal(rec.Body.Bytes(), &imgs)
|
|
if len(imgs) != 1 {
|
|
t.Fatalf("want 1 image got %d", len(imgs))
|
|
}
|
|
}
|
|
|
|
// ServeDescription Content-Type detection for PDF
|
|
func TestPubServeDescription_ContentTypePDF(t *testing.T) {
|
|
repo := newFakePubRepo()
|
|
repo.pubs[1] = domain.Publication{ID: 1, Title: "Pomologie", OSDBPubID: "P001"}
|
|
repo.descs[1] = domain.PublicationDescription{ID: 1, PublicationID: 1, FruitID: 2}
|
|
repo.descData[1] = []byte("%PDF-1.4 fake content here")
|
|
h := handler.NewPublicationHandler(repo)
|
|
e := newEcho()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/publications/1/descriptions/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
c.SetParamNames("id", "descId")
|
|
c.SetParamValues("1", "1")
|
|
if err := h.ServeDescription(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ct := rec.Header().Get("Content-Type")
|
|
// %PDF prefix should be detected or we hardcode application/pdf for descriptions
|
|
if ct == "" {
|
|
t.Fatal("want Content-Type header")
|
|
}
|
|
// Read body to verify content matches
|
|
body, _ := io.ReadAll(rec.Body)
|
|
if !bytes.Equal(body, repo.descData[1]) {
|
|
t.Fatal("body bytes mismatch")
|
|
}
|
|
}
|