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