summaryrefslogtreecommitdiff
path: root/internal/api/epics_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/epics_test.go')
-rw-r--r--internal/api/epics_test.go202
1 files changed, 202 insertions, 0 deletions
diff --git a/internal/api/epics_test.go b/internal/api/epics_test.go
new file mode 100644
index 0000000..bae7468
--- /dev/null
+++ b/internal/api/epics_test.go
@@ -0,0 +1,202 @@
+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)
+ }
+}