package app import ( "bytes" "context" "encoding/json" "net/http" "testing" "galo/internal/auth" "galo/internal/models" ) // The PATCH /photos/:id/edit endpoint must accept the whole edit payload, // including the effects block: the HTTP layer has its own request struct, so a // new parameter group can silently disappear if it is not added there. func TestEditEndpointAcceptsEffects(t *testing.T) { ctx := context.Background() r, token, uid, repos := newAvatarServer(t) album := &models.Album{ID: auth.NewID(), OwnerID: uid, Title: "Effects"} if err := repos.Albums.Create(ctx, album); err != nil { t.Fatalf("create album: %v", err) } if err := repos.Members.Add(ctx, album.ID, uid, "editor", nil); err != nil { t.Fatalf("add owner member: %v", err) } body, contentType := multipartFile(t, "file", "effects.png", makeTestPNG(t)) rec := doAuthed(r, http.MethodPost, "/api/albums/"+album.ID+"/photos", body, contentType, token) if rec.Code != http.StatusCreated { t.Fatalf("upload = %d body=%s", rec.Code, rec.Body.String()) } var uploaded struct { Photos []models.Photo `json:"photos"` } if err := json.Unmarshal(rec.Body.Bytes(), &uploaded); err != nil { t.Fatalf("decode upload: %v", err) } if len(uploaded.Photos) != 1 { t.Fatalf("uploaded photos = %d", len(uploaded.Photos)) } photoID := uploaded.Photos[0].ID // Explicit zero midpoint/feather/size/roughness must survive the round trip, // and omitted strengths must stay zero. payload := []byte(`{ "effects": { "texture": 40, "clarity": -20, "dehaze": 10, "vignette": { "mode": "color", "amount": 50, "midpoint": 0, "feather": 0, "roundness": 10, "highlights": 5, "color": "#ff0000" }, "grain": {"amount": 30, "size": 0, "roughness": 0} } }`) rec = doAuthed(r, http.MethodPatch, "/api/photos/"+photoID+"/edit", bytes.NewBuffer(payload), "application/json", token) if rec.Code != http.StatusOK { t.Fatalf("edit = %d body=%s", rec.Code, rec.Body.String()) } var edited struct { Photo models.Photo `json:"photo"` } if err := json.Unmarshal(rec.Body.Bytes(), &edited); err != nil { t.Fatalf("decode edit: %v", err) } effects, ok := edited.Photo.EditParams["effects"].(map[string]any) if !ok { t.Fatalf("effects missing from edit_params: %+v", edited.Photo.EditParams) } if got := effects["texture"]; got != float64(40) { t.Fatalf("texture = %v, want 40", got) } vignette, ok := effects["vignette"].(map[string]any) if !ok { t.Fatalf("vignette missing from effects: %+v", effects) } if vignette["mode"] != "color" || vignette["midpoint"] != float64(0) || vignette["feather"] != float64(0) || vignette["color"] != "#ff0000" { t.Fatalf("vignette params not stored as sent: %+v", vignette) } grain, ok := effects["grain"].(map[string]any) if !ok { t.Fatalf("grain missing from effects: %+v", effects) } if grain["size"] != float64(0) || grain["roughness"] != float64(0) { t.Fatalf("grain params not stored as sent: %+v", grain) } }