Files
osdb-1-claude-opusplan/backend/internal/handler/fruit_handler_test.go
juliaandClaude Sonnet 4.6 1b889ac112 feat: manage fruits — full CRUD with images and synonyms (story #02)
Backend: domain structs, FruitRepository interface + pg implementation,
9 Echo v4 handlers (list/get/create/update/delete, image sub-resources),
migration 000002 (fruit_type ENUM, fruits, fruit_synonyms, fruit_images),
route-scoped BodyLimit("5M") for uploads, http.DetectContentType for serving.

Frontend: typed fetch API layer, Pinia setup-style fruitStore, FruitList
(paginated), FruitCreate, and FruitDetail (edit + synonyms editor + image
gallery). 25 backend unit tests + integration test; 18 frontend tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 10:18:01 +02:00

701 lines
20 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/labstack/echo/v4"
"osdb/internal/domain"
"osdb/internal/handler"
)
// pngFixture is a minimal valid PNG (1×1 white pixel) so DetectContentType returns "image/png".
var pngFixture = []byte{
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, // IDAT chunk
0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00,
0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21, 0xbc,
0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, // IEND chunk
0x44, 0xae, 0x42, 0x60, 0x82,
}
// fakeRepo is an in-memory implementation of handler.FruitRepository.
type fakeRepo struct {
fruits map[int]domain.Fruit
images map[int]domain.FruitImage
imageData map[int][]byte
nextFruitID int
nextImageID int
forceDup bool
forceErr bool
}
func newFakeRepo() *fakeRepo {
return &fakeRepo{
fruits: make(map[int]domain.Fruit),
images: make(map[int]domain.FruitImage),
imageData: make(map[int][]byte),
nextFruitID: 1,
nextImageID: 1,
}
}
func (r *fakeRepo) List(_ context.Context, limit, offset int) ([]domain.Fruit, int, error) {
all := make([]domain.Fruit, 0, len(r.fruits))
for _, f := range r.fruits {
all = append(all, f)
}
total := len(all)
if offset >= total {
return []domain.Fruit{}, total, nil
}
end := offset + limit
if end > total {
end = total
}
return all[offset:end], total, nil
}
func (r *fakeRepo) Get(_ context.Context, id int) (domain.Fruit, error) {
f, ok := r.fruits[id]
if !ok {
return domain.Fruit{}, handler.ErrNotFound
}
return f, nil
}
func (r *fakeRepo) Create(_ context.Context, dto domain.FruitWriteDTO) (domain.Fruit, error) {
if r.forceDup {
return domain.Fruit{}, handler.ErrDuplicateOSDBNumber
}
if r.forceErr {
return domain.Fruit{}, errors.New("db error")
}
id := r.nextFruitID
r.nextFruitID++
syns := dto.Synonyms
if syns == nil {
syns = []string{}
}
f := domain.Fruit{
ID: id,
Name: dto.Name,
OSDBNumber: dto.OSDBNumber,
Comment: dto.Comment,
FruitType: dto.FruitType,
Synonyms: syns,
Images: []domain.FruitImage{},
}
r.fruits[id] = f
return f, nil
}
func (r *fakeRepo) Update(_ context.Context, id int, dto domain.FruitWriteDTO) (domain.Fruit, error) {
if r.forceDup {
return domain.Fruit{}, handler.ErrDuplicateOSDBNumber
}
f, ok := r.fruits[id]
if !ok {
return domain.Fruit{}, handler.ErrNotFound
}
syns := dto.Synonyms
if syns == nil {
syns = []string{}
}
f.Name = dto.Name
f.OSDBNumber = dto.OSDBNumber
f.Comment = dto.Comment
f.FruitType = dto.FruitType
f.Synonyms = syns
r.fruits[id] = f
return f, nil
}
func (r *fakeRepo) Delete(_ context.Context, id int) error {
if _, ok := r.fruits[id]; !ok {
return handler.ErrNotFound
}
delete(r.fruits, id)
return nil
}
func (r *fakeRepo) ListImages(_ context.Context, fruitID int) ([]domain.FruitImage, error) {
if _, ok := r.fruits[fruitID]; !ok {
return nil, handler.ErrNotFound
}
result := []domain.FruitImage{}
for _, img := range r.images {
if img.FruitID == fruitID {
result = append(result, img)
}
}
return result, nil
}
func (r *fakeRepo) AddImage(_ context.Context, fruitID int, filename *string, data []byte, imageType string, title *string) (domain.FruitImage, error) {
if _, ok := r.fruits[fruitID]; !ok {
return domain.FruitImage{}, handler.ErrNotFound
}
id := r.nextImageID
r.nextImageID++
img := domain.FruitImage{
ID: id,
FruitID: fruitID,
Filename: filename,
ImageType: imageType,
Title: title,
URL: fmt.Sprintf("/api/v1/fruits/%d/images/%d", fruitID, id),
}
r.images[id] = img
r.imageData[id] = data
return img, nil
}
func (r *fakeRepo) GetImageData(_ context.Context, fruitID, imageID int) ([]byte, error) {
img, ok := r.images[imageID]
if !ok || img.FruitID != fruitID {
return nil, handler.ErrNotFound
}
return r.imageData[imageID], nil
}
func (r *fakeRepo) DeleteImage(_ context.Context, fruitID, imageID int) error {
img, ok := r.images[imageID]
if !ok || img.FruitID != fruitID {
return handler.ErrNotFound
}
delete(r.images, imageID)
delete(r.imageData, imageID)
return nil
}
// helpers
func newEcho() *echo.Echo {
e := echo.New()
e.HideBanner = true
return e
}
func jsonBody(v any) *strings.Reader {
b, _ := json.Marshal(v)
return strings.NewReader(string(b))
}
// -- List --
func TestFruitList_Empty(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
req := httptest.NewRequest(http.MethodGet, "/api/v1/fruits", 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 resp domain.FruitListResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if len(resp.Items) != 0 {
t.Fatalf("want 0 items got %d", len(resp.Items))
}
if resp.Limit != 50 {
t.Fatalf("want default limit 50 got %d", resp.Limit)
}
}
func TestFruitList_WithItems(t *testing.T) {
repo := newFakeRepo()
repo.fruits[1] = domain.Fruit{ID: 1, Name: "Boskop", OSDBNumber: "A001", FruitType: "Apfelsorten", Synonyms: []string{}, Images: []domain.FruitImage{}}
h := handler.NewFruitHandler(repo)
e := newEcho()
req := httptest.NewRequest(http.MethodGet, "/api/v1/fruits?limit=10&offset=0", 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 resp domain.FruitListResponse
json.Unmarshal(rec.Body.Bytes(), &resp)
if resp.Total != 1 {
t.Fatalf("want total 1 got %d", resp.Total)
}
}
// -- Get --
func TestFruitGet_Found(t *testing.T) {
repo := newFakeRepo()
repo.fruits[1] = domain.Fruit{ID: 1, Name: "Boskop", OSDBNumber: "A001", FruitType: "Apfelsorten", Synonyms: []string{"Boskop-Renette"}, Images: []domain.FruitImage{}}
h := handler.NewFruitHandler(repo)
e := newEcho()
req := httptest.NewRequest(http.MethodGet, "/api/v1/fruits/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)
}
var f domain.Fruit
json.Unmarshal(rec.Body.Bytes(), &f)
if len(f.Synonyms) != 1 || f.Synonyms[0] != "Boskop-Renette" {
t.Fatalf("want synonyms [Boskop-Renette] got %v", f.Synonyms)
}
}
func TestFruitGet_NotFound(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
req := httptest.NewRequest(http.MethodGet, "/api/v1/fruits/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 TestFruitGet_BadID(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
req := httptest.NewRequest(http.MethodGet, "/api/v1/fruits/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)
}
}
// -- Create --
func TestFruitCreate_201(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
body := jsonBody(map[string]any{"name": "Boskop", "osdb_number": "A001", "fruit_type": "Apfelsorten", "synonyms": []string{"Boskop-Renette"}})
req := httptest.NewRequest(http.MethodPost, "/api/v1/fruits", 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 f domain.Fruit
json.Unmarshal(rec.Body.Bytes(), &f)
if f.ID == 0 {
t.Fatal("want non-zero ID")
}
if len(f.Synonyms) != 1 {
t.Fatalf("want 1 synonym got %d", len(f.Synonyms))
}
}
func TestFruitCreate_422_MissingName(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
body := jsonBody(map[string]any{"osdb_number": "A001", "fruit_type": "Apfelsorten"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/fruits", 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 TestFruitCreate_422_MissingOSDBNumber(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
body := jsonBody(map[string]any{"name": "Boskop", "fruit_type": "Apfelsorten"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/fruits", 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 TestFruitCreate_422_InvalidFruitType(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
body := jsonBody(map[string]any{"name": "Boskop", "osdb_number": "A001", "fruit_type": "Bananen"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/fruits", 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 TestFruitCreate_409_Duplicate(t *testing.T) {
repo := newFakeRepo()
repo.forceDup = true
h := handler.NewFruitHandler(repo)
e := newEcho()
body := jsonBody(map[string]any{"name": "Boskop", "osdb_number": "A001", "fruit_type": "Apfelsorten"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/fruits", 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)
}
}
func TestFruitCreate_400_MalformedJSON(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
req := httptest.NewRequest(http.MethodPost, "/api/v1/fruits", strings.NewReader("{not json"))
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.StatusBadRequest {
t.Fatalf("want 400 got %d", rec.Code)
}
}
// -- Update --
func TestFruitUpdate_200(t *testing.T) {
repo := newFakeRepo()
repo.fruits[1] = domain.Fruit{ID: 1, Name: "Boskop", OSDBNumber: "A001", FruitType: "Apfelsorten", Synonyms: []string{}, Images: []domain.FruitImage{}}
h := handler.NewFruitHandler(repo)
e := newEcho()
body := jsonBody(map[string]any{"name": "Boskop Updated", "osdb_number": "A001", "fruit_type": "Apfelsorten", "synonyms": []string{"Syn1"}})
req := httptest.NewRequest(http.MethodPut, "/api/v1/fruits/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 f domain.Fruit
json.Unmarshal(rec.Body.Bytes(), &f)
if f.Name != "Boskop Updated" {
t.Fatalf("want updated name got %s", f.Name)
}
}
func TestFruitUpdate_404(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
body := jsonBody(map[string]any{"name": "X", "osdb_number": "A001", "fruit_type": "Apfelsorten"})
req := httptest.NewRequest(http.MethodPut, "/api/v1/fruits/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 TestFruitDelete_204(t *testing.T) {
repo := newFakeRepo()
repo.fruits[1] = domain.Fruit{ID: 1, Name: "Boskop", Synonyms: []string{}, Images: []domain.FruitImage{}}
h := handler.NewFruitHandler(repo)
e := newEcho()
req := httptest.NewRequest(http.MethodDelete, "/api/v1/fruits/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 TestFruitDelete_404(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
req := httptest.NewRequest(http.MethodDelete, "/api/v1/fruits/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)
}
}
// -- Images --
func TestFruitListImages_200(t *testing.T) {
repo := newFakeRepo()
repo.fruits[1] = domain.Fruit{ID: 1, Synonyms: []string{}, Images: []domain.FruitImage{}}
h := handler.NewFruitHandler(repo)
e := newEcho()
req := httptest.NewRequest(http.MethodGet, "/api/v1/fruits/1/images", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("id")
c.SetParamValues("1")
if err := h.ListImages(c); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusOK {
t.Fatalf("want 200 got %d", rec.Code)
}
}
func buildMultipartUpload(t *testing.T, fieldName, filename string, data []byte, imageType, title 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)
}
if imageType != "" {
w.WriteField("image_type", imageType)
}
if title != "" {
w.WriteField("title", title)
}
w.Close()
return &buf, w.FormDataContentType()
}
func TestFruitUploadImage_201(t *testing.T) {
repo := newFakeRepo()
repo.fruits[1] = domain.Fruit{ID: 1, Synonyms: []string{}, Images: []domain.FruitImage{}}
h := handler.NewFruitHandler(repo)
e := newEcho()
buf, ct := buildMultipartUpload(t, "image", "test.png", pngFixture, "fruit", "Test image")
req := httptest.NewRequest(http.MethodPost, "/api/v1/fruits/1/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.UploadImage(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.FruitImage
json.Unmarshal(rec.Body.Bytes(), &img)
if img.ID == 0 {
t.Fatal("want non-zero image ID")
}
}
func TestFruitUploadImage_422_BadImageType(t *testing.T) {
repo := newFakeRepo()
repo.fruits[1] = domain.Fruit{ID: 1, Synonyms: []string{}, Images: []domain.FruitImage{}}
h := handler.NewFruitHandler(repo)
e := newEcho()
buf, ct := buildMultipartUpload(t, "image", "test.png", pngFixture, "invalid_type", "")
req := httptest.NewRequest(http.MethodPost, "/api/v1/fruits/1/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.UploadImage(c); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusUnprocessableEntity {
t.Fatalf("want 422 got %d", rec.Code)
}
}
func TestFruitUploadImage_400_NoFile(t *testing.T) {
repo := newFakeRepo()
repo.fruits[1] = domain.Fruit{ID: 1, Synonyms: []string{}, Images: []domain.FruitImage{}}
h := handler.NewFruitHandler(repo)
e := newEcho()
// Send multipart with image_type but no file field
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
w.WriteField("image_type", "fruit")
w.Close()
req := httptest.NewRequest(http.MethodPost, "/api/v1/fruits/1/images", &buf)
req.Header.Set(echo.HeaderContentType, w.FormDataContentType())
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("id")
c.SetParamValues("1")
if err := h.UploadImage(c); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusBadRequest {
t.Fatalf("want 400 got %d", rec.Code)
}
}
func TestFruitUploadImage_404_FruitMissing(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
buf, ct := buildMultipartUpload(t, "image", "test.png", pngFixture, "fruit", "")
req := httptest.NewRequest(http.MethodPost, "/api/v1/fruits/99/images", buf)
req.Header.Set(echo.HeaderContentType, ct)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("id")
c.SetParamValues("99")
if err := h.UploadImage(c); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusNotFound {
t.Fatalf("want 404 got %d", rec.Code)
}
}
func TestFruitServeImage_200(t *testing.T) {
repo := newFakeRepo()
repo.fruits[1] = domain.Fruit{ID: 1, Synonyms: []string{}, Images: []domain.FruitImage{}}
repo.images[1] = domain.FruitImage{ID: 1, FruitID: 1, ImageType: "fruit"}
repo.imageData[1] = pngFixture
h := handler.NewFruitHandler(repo)
e := newEcho()
req := httptest.NewRequest(http.MethodGet, "/api/v1/fruits/1/images/1", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("id", "imageId")
c.SetParamValues("1", "1")
if err := h.ServeImage(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("response body does not match uploaded image bytes")
}
ct := rec.Header().Get("Content-Type")
if ct != "image/png" {
t.Fatalf("want Content-Type image/png got %s", ct)
}
}
func TestFruitServeImage_404(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
req := httptest.NewRequest(http.MethodGet, "/api/v1/fruits/1/images/99", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("id", "imageId")
c.SetParamValues("1", "99")
if err := h.ServeImage(c); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusNotFound {
t.Fatalf("want 404 got %d", rec.Code)
}
}
func TestFruitDeleteImage_204(t *testing.T) {
repo := newFakeRepo()
repo.fruits[1] = domain.Fruit{ID: 1, Synonyms: []string{}, Images: []domain.FruitImage{}}
repo.images[1] = domain.FruitImage{ID: 1, FruitID: 1}
repo.imageData[1] = pngFixture
h := handler.NewFruitHandler(repo)
e := newEcho()
req := httptest.NewRequest(http.MethodDelete, "/api/v1/fruits/1/images/1", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("id", "imageId")
c.SetParamValues("1", "1")
if err := h.DeleteImage(c); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusNoContent {
t.Fatalf("want 204 got %d", rec.Code)
}
}
func TestFruitDeleteImage_404(t *testing.T) {
h := handler.NewFruitHandler(newFakeRepo())
e := newEcho()
req := httptest.NewRequest(http.MethodDelete, "/api/v1/fruits/1/images/99", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("id", "imageId")
c.SetParamValues("1", "99")
if err := h.DeleteImage(c); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusNotFound {
t.Fatalf("want 404 got %d", rec.Code)
}
}