package api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/thepeterstone/claudomator/internal/story" ) // TestServer_CreateEpic_AndGetEpic mirrors the projects endpoint test // pattern: POST creates, GET retrieves what was created. func TestServer_CreateEpic_AndGetEpic(t *testing.T) { srv, _ := testServer(t) body, _ := json.Marshal(map[string]any{ "name": "Bigger Auth Rework", "description": "Overhaul the auth flow.", "discovery_source": "human", }) req := httptest.NewRequest("POST", "/api/epics", bytes.NewReader(body)) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create: expected 201, got %d: %s", w.Code, w.Body.String()) } var created story.Epic if err := json.Unmarshal(w.Body.Bytes(), &created); err != nil { t.Fatalf("decode create response: %v", err) } if created.ID == "" { t.Fatal("expected server-assigned ID") } if created.Status != "OPEN" { t.Errorf("expected default status OPEN, got %q", created.Status) } req = httptest.NewRequest("GET", "/api/epics/"+created.ID, nil) w = httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("get: expected 200, got %d: %s", w.Code, w.Body.String()) } var got story.Epic if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatalf("decode get response: %v", err) } if got.Name != "Bigger Auth Rework" { t.Errorf("Name: want %q, got %q", "Bigger Auth Rework", got.Name) } if got.DiscoverySource != "human" { t.Errorf("DiscoverySource: want human, got %q", got.DiscoverySource) } } func TestServer_CreateEpic_MissingName_Returns400(t *testing.T) { srv, _ := testServer(t) req := httptest.NewRequest("POST", "/api/epics", bytes.NewReader([]byte(`{}`))) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) } } func TestServer_GetEpic_NotFound(t *testing.T) { srv, _ := testServer(t) req := httptest.NewRequest("GET", "/api/epics/does-not-exist", nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String()) } } // TestServer_ListEpics_FiltersByStatus confirms ?status=OPEN narrows results. func TestServer_ListEpics_FiltersByStatus(t *testing.T) { srv, store := testServer(t) if err := store.CreateEpic(&story.Epic{ID: "e1", Name: "one", Status: "OPEN"}); err != nil { t.Fatal(err) } if err := store.CreateEpic(&story.Epic{ID: "e2", Name: "two", Status: "CLOSED"}); err != nil { t.Fatal(err) } req := httptest.NewRequest("GET", "/api/epics", nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var all []story.Epic if err := json.Unmarshal(w.Body.Bytes(), &all); err != nil { t.Fatalf("decode: %v", err) } if len(all) != 2 { t.Fatalf("want 2 epics, got %d", len(all)) } req = httptest.NewRequest("GET", "/api/epics?status=OPEN", nil) w = httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var open []story.Epic if err := json.Unmarshal(w.Body.Bytes(), &open); err != nil { t.Fatalf("decode: %v", err) } if len(open) != 1 || open[0].ID != "e1" { t.Fatalf("want exactly [e1], got %+v", open) } } func TestServer_UpdateEpic(t *testing.T) { srv, store := testServer(t) if err := store.CreateEpic(&story.Epic{ID: "e1", Name: "original", Status: "OPEN"}); err != nil { t.Fatal(err) } body, _ := json.Marshal(map[string]any{ "name": "updated", "status": "CLOSED", }) req := httptest.NewRequest("PUT", "/api/epics/e1", bytes.NewReader(body)) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var got story.Epic if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatalf("decode: %v", err) } if got.Name != "updated" || got.Status != "CLOSED" { t.Errorf("expected updated fields, got %+v", got) } if got.ID != "e1" { t.Errorf("ID must not change via body: got %q", got.ID) } } func TestServer_UpdateEpic_NotFound(t *testing.T) { srv, _ := testServer(t) req := httptest.NewRequest("PUT", "/api/epics/does-not-exist", bytes.NewReader([]byte(`{"name":"x"}`))) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String()) } } // TestServer_ListEpicStories confirms GET /api/epics/{id}/stories returns // only stories whose epic_id matches the path {id}. func TestServer_ListEpicStories(t *testing.T) { srv, store := testServer(t) if err := store.CreateEpic(&story.Epic{ID: "epic-a", Name: "a"}); err != nil { t.Fatal(err) } if err := store.CreateStory(&story.Story{ID: "s1", EpicID: "epic-a", Name: "one", Status: "DISCOVERY"}); err != nil { t.Fatal(err) } if err := store.CreateStory(&story.Story{ID: "s2", EpicID: "epic-b", Name: "two", Status: "DISCOVERY"}); err != nil { t.Fatal(err) } req := httptest.NewRequest("GET", "/api/epics/epic-a/stories", nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var got []story.Story if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatalf("decode: %v", err) } if len(got) != 1 || got[0].ID != "s1" { t.Fatalf("want exactly [s1], got %+v", got) } } // TestServer_ListEpicStories_UnknownEpic_ReturnsEmptyList mirrors // handleListSubtasks: no existence check on the parent, just an empty list. func TestServer_ListEpicStories_UnknownEpic_ReturnsEmptyList(t *testing.T) { srv, _ := testServer(t) req := httptest.NewRequest("GET", "/api/epics/does-not-exist/stories", nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var got []story.Story if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatalf("decode: %v", err) } if len(got) != 0 { t.Fatalf("want empty list, got %+v", got) } }