CI / ci (push) Successful in 3m51s
Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
148 lines
5.5 KiB
Go
148 lines
5.5 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
|
)
|
|
|
|
const rotationReviewsPath = "/api/v1/rotation-reviews"
|
|
|
|
// triedPadThai plans the Quest log dinner Pad Thai on Wednesday 2026-10-14
|
|
// and marks the day tried, so Pad Thai has a pending Rotation check.
|
|
func triedPadThai(t *testing.T, e plansEnv) {
|
|
t.Helper()
|
|
rec, _ := callDinners(t, e.h, http.MethodPut, plansPath+"2026-10-12/days/2026-10-14", `{"dinner_id":"`+e.questLog+`"}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("swap Pad Thai in = %d: %s", rec.Code, rec.Body)
|
|
}
|
|
day, err := store.ParseLocalDate("2026-10-14")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n, err := e.svc.MarkTried(t.Context(), day); err != nil || n != 1 {
|
|
t.Fatalf("MarkTried = %d, %v, want 1", n, err)
|
|
}
|
|
}
|
|
|
|
func TestGetRotationReviews(t *testing.T) {
|
|
t.Parallel()
|
|
e := newPlansRouter(t)
|
|
|
|
rec, body := callDinners(t, e.h, http.MethodGet, rotationReviewsPath, "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("GET = %d: %s", rec.Code, rec.Body)
|
|
}
|
|
if reviews, ok := body["reviews"].([]any); !ok || len(reviews) != 0 {
|
|
t.Errorf("GET before MarkTried = %v, want an empty reviews list", body)
|
|
}
|
|
|
|
triedPadThai(t, e)
|
|
rec, body = callDinners(t, e.h, http.MethodGet, rotationReviewsPath, "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("GET = %d: %s", rec.Code, rec.Body)
|
|
}
|
|
reviews, _ := body["reviews"].([]any)
|
|
if len(reviews) != 1 {
|
|
t.Fatalf("reviews = %v, want one", body)
|
|
}
|
|
r := reviews[0].(map[string]any)
|
|
d, _ := r["dinner"].(map[string]any)
|
|
if len(r) != 2 || r["plan_date"] != "2026-10-14" || len(d) != 3 ||
|
|
d["id"] != e.questLog || d["name"] != "Pad Thai" || d["photo"] != nil {
|
|
t.Errorf("review = %v, want {dinner {id, name, photo}, plan_date}", r)
|
|
}
|
|
}
|
|
|
|
func TestPostRotationReview(t *testing.T) {
|
|
t.Parallel()
|
|
for _, tc := range []struct{ decision, status string }{
|
|
{"keep", "regular"},
|
|
{"drop", "archived"},
|
|
{"not_made", "quest_log"},
|
|
} {
|
|
t.Run(tc.decision, func(t *testing.T) {
|
|
t.Parallel()
|
|
e := newPlansRouter(t)
|
|
triedPadThai(t, e)
|
|
target := dinnersPath + "/" + e.questLog + "/rotation-review"
|
|
|
|
rec, body := callDinners(t, e.h, http.MethodPost, target, `{"decision":"`+tc.decision+`"}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("POST = %d: %s", rec.Code, rec.Body)
|
|
}
|
|
if body["decision"] != tc.decision || body["dinner_status"] != tc.status || body["plan_date"] != "2026-10-14" ||
|
|
body["updated_by"] != testSession.Subject || body["decided_at"] == "" {
|
|
t.Errorf("decided = %v", body)
|
|
}
|
|
var status, origin, updatedBy string
|
|
if err := e.st.Reader.QueryRowContext(t.Context(), `SELECT status, origin, updated_by FROM dinners WHERE id = ?`, e.questLog).
|
|
Scan(&status, &origin, &updatedBy); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status != tc.status || origin != "regular" || updatedBy != testSession.Subject {
|
|
t.Errorf("dinner = %s %s %s, want %s regular %s", status, origin, updatedBy, tc.status, testSession.Subject)
|
|
}
|
|
|
|
rec, body = callDinners(t, e.h, http.MethodPost, target, `{"decision":"keep"}`)
|
|
if rec.Code != http.StatusConflict || errorCode(body) != "no_pending_review" {
|
|
t.Errorf("second decision = %d %v, want 409 no_pending_review", rec.Code, body)
|
|
}
|
|
rec, body = callDinners(t, e.h, http.MethodGet, rotationReviewsPath, "")
|
|
if reviews, _ := body["reviews"].([]any); rec.Code != http.StatusOK || len(reviews) != 0 {
|
|
t.Errorf("GET after deciding = %d %v, want no reviews", rec.Code, body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A dinner that has never had a Rotation check is 404; a second decision
|
|
// on one already decided is 409 (TestPostRotationReview).
|
|
func TestPostRotationReviewNeverChecked(t *testing.T) {
|
|
t.Parallel()
|
|
e := newPlansRouter(t)
|
|
rec, body := callDinners(t, e.h, http.MethodPost, dinnersPath+"/"+e.questLog+"/rotation-review", `{"decision":"keep"}`)
|
|
if rec.Code != http.StatusNotFound || errorCode(body) != "not_found" {
|
|
t.Errorf("never checked = %d %v, want 404 not_found", rec.Code, body)
|
|
}
|
|
var status string
|
|
if err := e.st.Reader.QueryRowContext(t.Context(), `SELECT status FROM dinners WHERE id = ?`, e.questLog).Scan(&status); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status != "quest_log" {
|
|
t.Errorf("status after a refused decision = %s, want quest_log", status)
|
|
}
|
|
}
|
|
|
|
func TestPostRotationReviewRefused(t *testing.T) {
|
|
t.Parallel()
|
|
e := newPlansRouter(t)
|
|
triedPadThai(t, e)
|
|
target := dinnersPath + "/" + e.questLog + "/rotation-review"
|
|
|
|
for _, tc := range []struct {
|
|
name, target, body string
|
|
code int
|
|
errCode string
|
|
}{
|
|
{"no decision", target, `{}`, http.StatusBadRequest, "invalid_request"},
|
|
{"null decision", target, `{"decision":null}`, http.StatusBadRequest, "invalid_request"},
|
|
{"unknown field", target, `{"decision":"keep","status":"regular"}`, http.StatusBadRequest, "invalid_request"},
|
|
{"unknown decision", target, `{"decision":"promote"}`, http.StatusBadRequest, "invalid_decision"},
|
|
{"unknown dinner", dinnersPath + "/nope/rotation-review", `{"decision":"keep"}`, http.StatusNotFound, "not_found"},
|
|
} {
|
|
rec, body := callDinners(t, e.h, http.MethodPost, tc.target, tc.body)
|
|
if rec.Code != tc.code || errorCode(body) != tc.errCode {
|
|
t.Errorf("%s = %d %v, want %d %s", tc.name, rec.Code, body, tc.code, tc.errCode)
|
|
}
|
|
}
|
|
var status string
|
|
if err := e.st.Reader.QueryRowContext(t.Context(), `SELECT status FROM dinners WHERE id = ?`, e.questLog).Scan(&status); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status != "tried" {
|
|
t.Errorf("status after refused decisions = %s, want tried", status)
|
|
}
|
|
}
|