package api import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" ) func newRevisionRouter(s *Server) http.Handler { r := chi.NewRouter() r.Route("/api/items/{partNumber}", func(r chi.Router) { r.Get("/revisions", s.HandleListRevisions) r.Get("/revisions/compare", s.HandleCompareRevisions) r.Get("/revisions/{revision}", s.HandleGetRevision) r.Post("/revisions", s.HandleCreateRevision) r.Patch("/revisions/{revision}", s.HandleUpdateRevision) r.Post("/revisions/{revision}/rollback", s.HandleRollbackRevision) }) return r } func TestHandleListRevisions(t *testing.T) { s := newTestServer(t) router := newRevisionRouter(s) createItemDirect(t, s, "REV-API-001", "revision list", nil) req := httptest.NewRequest("GET", "/api/items/REV-API-001/revisions", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) } var revisions []RevisionResponse if err := json.Unmarshal(w.Body.Bytes(), &revisions); err != nil { t.Fatalf("decoding response: %v", err) } if len(revisions) != 1 { t.Errorf("expected 1 revision (initial), got %d", len(revisions)) } } func TestHandleListRevisionsNotFound(t *testing.T) { s := newTestServer(t) router := newRevisionRouter(s) req := httptest.NewRequest("GET", "/api/items/NOEXIST/revisions", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("status: got %d, want %d", w.Code, http.StatusNotFound) } } func TestHandleGetRevision(t *testing.T) { s := newTestServer(t) router := newRevisionRouter(s) createItemDirect(t, s, "REVGET-001", "get revision", nil) req := httptest.NewRequest("GET", "/api/items/REVGET-001/revisions/1", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) } var rev RevisionResponse if err := json.Unmarshal(w.Body.Bytes(), &rev); err != nil { t.Fatalf("decoding response: %v", err) } if rev.RevisionNumber != 1 { t.Errorf("revision_number: got %d, want 1", rev.RevisionNumber) } } func TestHandleGetRevisionNotFound(t *testing.T) { s := newTestServer(t) router := newRevisionRouter(s) createItemDirect(t, s, "REVNF-001", "rev not found", nil) req := httptest.NewRequest("GET", "/api/items/REVNF-001/revisions/99", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("status: got %d, want %d", w.Code, http.StatusNotFound) } } func TestHandleCreateRevision(t *testing.T) { s := newTestServer(t) router := newRevisionRouter(s) createItemDirect(t, s, "REVCR-001", "create revision", nil) body := `{"properties":{"material":"steel"},"comment":"added material"}` req := authRequest(httptest.NewRequest("POST", "/api/items/REVCR-001/revisions", strings.NewReader(body))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("status: got %d, want %d; body: %s", w.Code, http.StatusCreated, w.Body.String()) } var rev RevisionResponse if err := json.Unmarshal(w.Body.Bytes(), &rev); err != nil { t.Fatalf("decoding response: %v", err) } if rev.RevisionNumber != 2 { t.Errorf("revision_number: got %d, want 2", rev.RevisionNumber) } } func TestHandleUpdateRevision(t *testing.T) { s := newTestServer(t) router := newRevisionRouter(s) createItemDirect(t, s, "REVUP-001", "update revision", nil) body := `{"status":"released","labels":["production"]}` req := authRequest(httptest.NewRequest("PATCH", "/api/items/REVUP-001/revisions/1", strings.NewReader(body))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) } var rev RevisionResponse if err := json.Unmarshal(w.Body.Bytes(), &rev); err != nil { t.Fatalf("decoding response: %v", err) } if rev.Status != "released" { t.Errorf("status: got %q, want %q", rev.Status, "released") } if len(rev.Labels) != 1 || rev.Labels[0] != "production" { t.Errorf("labels: got %v, want [production]", rev.Labels) } } func TestHandleCompareRevisions(t *testing.T) { s := newTestServer(t) router := newRevisionRouter(s) // Create item with properties, then create second revision with changed properties cost := 10.0 createItemDirect(t, s, "REVCMP-001", "compare revisions", &cost) body := `{"properties":{"standard_cost":20,"material":"aluminum"},"comment":"updated cost"}` req := authRequest(httptest.NewRequest("POST", "/api/items/REVCMP-001/revisions", strings.NewReader(body))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create rev 2: status %d; body: %s", w.Code, w.Body.String()) } // Compare rev 1 vs rev 2 req = httptest.NewRequest("GET", "/api/items/REVCMP-001/revisions/compare?from=1&to=2", nil) w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) } var diff RevisionDiffResponse if err := json.Unmarshal(w.Body.Bytes(), &diff); err != nil { t.Fatalf("decoding response: %v", err) } if diff.FromRevision != 1 || diff.ToRevision != 2 { t.Errorf("revisions: got from=%d to=%d, want from=1 to=2", diff.FromRevision, diff.ToRevision) } } func TestHandleRollbackRevision(t *testing.T) { s := newTestServer(t) router := newRevisionRouter(s) createItemDirect(t, s, "REVRB-001", "rollback test", nil) // Create rev 2 body := `{"properties":{"version":"v2"},"comment":"version 2"}` req := authRequest(httptest.NewRequest("POST", "/api/items/REVRB-001/revisions", strings.NewReader(body))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create rev 2: status %d; body: %s", w.Code, w.Body.String()) } // Rollback to rev 1 — should create rev 3 body = `{"comment":"rolling back"}` req = authRequest(httptest.NewRequest("POST", "/api/items/REVRB-001/revisions/1/rollback", strings.NewReader(body))) req.Header.Set("Content-Type", "application/json") w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("status: got %d, want %d; body: %s", w.Code, http.StatusCreated, w.Body.String()) } var rev RevisionResponse if err := json.Unmarshal(w.Body.Bytes(), &rev); err != nil { t.Fatalf("decoding response: %v", err) } if rev.RevisionNumber != 3 { t.Errorf("revision_number: got %d, want 3", rev.RevisionNumber) } }