package handlers import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" ) func newComplianceGraphTestRouter(t *testing.T) *gin.Engine { t.Helper() gin.SetMode(gin.TestMode) h := NewComplianceGraphHandlers() if err := h.LoadError(); err != nil { t.Fatalf("compliance graph failed to load (candidate paths): %v", err) } r := gin.New() h.RegisterRoutes(r.Group("/sdk/v1")) return r } func getObligationStatus(t *testing.T, r *gin.Engine, query string) (int, cgStatusResponse) { t.Helper() w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/sdk/v1/compliance/obligation-status"+query, nil) r.ServeHTTP(w, req) var resp cgStatusResponse if w.Code == http.StatusOK { if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("decode body %q: %v", w.Body.String(), err) } } return w.Code, resp } func TestObligationStatus(t *testing.T) { r := newComplianceGraphTestRouter(t) tests := []struct { name string query string wantHTTP int wantOverall string wantControls bool // expect >=1 control }{ {"missing param -> 400", "", http.StatusBadRequest, "", false}, {"unknown id -> unknown_obligation", "?obligation_id=does_not_exist", http.StatusOK, "unknown_obligation", false}, {"mapped (OWASP V6) -> not_assessed", "?obligation_id=user_authentication_required", http.StatusOK, "not_assessed", true}, {"in registry, no control -> unmapped", "?obligation_id=sbom_creation", http.StatusOK, "unmapped", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { code, resp := getObligationStatus(t, r, tt.query) if code != tt.wantHTTP { t.Fatalf("http %d, want %d", code, tt.wantHTTP) } if tt.wantHTTP != http.StatusOK { return } if resp.OverallStatus != tt.wantOverall { t.Errorf("overall_status=%q, want %q", resp.OverallStatus, tt.wantOverall) } if tt.wantControls && len(resp.Controls) == 0 { t.Error("expected >=1 control") } if !tt.wantControls && len(resp.Controls) != 0 { t.Errorf("expected 0 controls, got %d", len(resp.Controls)) } if resp.CitationSpans != "pending" { t.Errorf("citation_spans=%q, want pending", resp.CitationSpans) } }) } } // The MVP must NEVER auto-assert fulfillment: with no evidence collection wired, every required // evidence is "missing" and the overall status stays "not_assessed". func TestObligationStatus_NoFulfillmentClaim(t *testing.T) { r := newComplianceGraphTestRouter(t) code, resp := getObligationStatus(t, r, "?obligation_id=user_authentication_required") if code != http.StatusOK { t.Fatalf("http %d", code) } if resp.OverallStatus == "met" || resp.OverallStatus == "erfuellt" { t.Fatalf("MVP must not assert fulfillment, got overall_status=%q", resp.OverallStatus) } for _, ctl := range resp.Controls { if len(ctl.EvidenceRequired) > 0 && ctl.EvidenceStatus != "missing" { t.Errorf("control %s/%s evidence_status=%q, want missing (no collection wired)", ctl.Framework, ctl.Control, ctl.EvidenceStatus) } } }