package httpserver import ( "bytes" "encoding/json" "mime/multipart" "net/http" "net/http/httptest" "strings" "testing" "time" "kubeviz/internal/config" ) func TestParseGraphAndResourceFlow(t *testing.T) { srv, err := New(config.Config{Addr: ":0", SessionTTL: 30 * time.Minute, MaxUploadSize: 1024 * 1024}) if err != nil { t.Fatalf("server init failed: %v", err) } handler := srv.Handler() manifest := `apiVersion: v1 kind: ConfigMap metadata: name: app-cfg namespace: demo ` body := map[string]string{"manifest": manifest} payload, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/api/manifests/parse", bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if got := w.Result().StatusCode; got != http.StatusOK { t.Fatalf("parse status: got %d body=%s", got, w.Body.String()) } cookies := w.Result().Cookies() if len(cookies) == 0 { t.Fatalf("expected session cookie") } graphReq := httptest.NewRequest(http.MethodGet, "/api/graph", nil) graphReq.AddCookie(cookies[0]) graphW := httptest.NewRecorder() handler.ServeHTTP(graphW, graphReq) if got := graphW.Result().StatusCode; got != http.StatusOK { t.Fatalf("graph status: got %d", got) } var graph map[string]any if err := json.NewDecoder(graphW.Result().Body).Decode(&graph); err != nil { t.Fatalf("graph decode failed: %v", err) } nodes, _ := graph["nodes"].([]any) if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) } resReq := httptest.NewRequest(http.MethodGet, "/api/resources/demo/ConfigMap/app-cfg", nil) resReq.AddCookie(cookies[0]) resW := httptest.NewRecorder() handler.ServeHTTP(resW, resReq) if got := resW.Result().StatusCode; got != http.StatusOK { t.Fatalf("resource status: got %d", got) } } func TestParseMultipleUploadedFiles(t *testing.T) { srv, err := New(config.Config{Addr: ":0", SessionTTL: 30 * time.Minute, MaxUploadSize: 1024 * 1024}) if err != nil { t.Fatalf("server init failed: %v", err) } handler := srv.Handler() var body bytes.Buffer writer := multipart.NewWriter(&body) f1, err := writer.CreateFormFile("manifestFile", "deployment.yaml") if err != nil { t.Fatalf("failed creating file part 1: %v", err) } _, _ = f1.Write([]byte(`apiVersion: apps/v1 kind: Deployment metadata: name: kubeviz namespace: kubeviz `)) f2, err := writer.CreateFormFile("manifestFile", "service.yaml") if err != nil { t.Fatalf("failed creating file part 2: %v", err) } _, _ = f2.Write([]byte(`apiVersion: v1 kind: Service metadata: name: kubeviz namespace: kubeviz spec: selector: app: kubeviz `)) if err := writer.Close(); err != nil { t.Fatalf("failed to close multipart writer: %v", err) } req := httptest.NewRequest(http.MethodPost, "/api/manifests/parse", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if got := w.Result().StatusCode; got != http.StatusOK { t.Fatalf("parse status: got %d body=%s", got, w.Body.String()) } cookies := w.Result().Cookies() if len(cookies) == 0 { t.Fatalf("expected session cookie") } graphReq := httptest.NewRequest(http.MethodGet, "/api/graph", nil) graphReq.AddCookie(cookies[0]) graphW := httptest.NewRecorder() handler.ServeHTTP(graphW, graphReq) if got := graphW.Result().StatusCode; got != http.StatusOK { t.Fatalf("graph status: got %d", got) } var graph map[string]any if err := json.NewDecoder(graphW.Result().Body).Decode(&graph); err != nil { t.Fatalf("graph decode failed: %v", err) } nodes, _ := graph["nodes"].([]any) if len(nodes) != 2 { t.Fatalf("expected 2 nodes from two files, got %d", len(nodes)) } } func TestParseMultipleUploadedFilesToleratesSingleInvalidFile(t *testing.T) { srv, err := New(config.Config{Addr: ":0", SessionTTL: 30 * time.Minute, MaxUploadSize: 1024 * 1024}) if err != nil { t.Fatalf("server init failed: %v", err) } handler := srv.Handler() var body bytes.Buffer writer := multipart.NewWriter(&body) valid, err := writer.CreateFormFile("manifestFile", "service.yaml") if err != nil { t.Fatalf("failed creating valid file part: %v", err) } _, _ = valid.Write([]byte(`apiVersion: v1 kind: Service metadata: name: kubeviz namespace: kubeviz spec: selector: app: kubeviz `)) invalid, err := writer.CreateFormFile("manifestFile", "broken.yaml") if err != nil { t.Fatalf("failed creating invalid file part: %v", err) } _, _ = invalid.Write([]byte(`apiVersion: v1 kind Service metadata: name: broken `)) if err := writer.Close(); err != nil { t.Fatalf("failed to close multipart writer: %v", err) } req := httptest.NewRequest(http.MethodPost, "/api/manifests/parse", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if got := w.Result().StatusCode; got != http.StatusOK { t.Fatalf("parse status: got %d", got) } var resp struct { Summary struct { Resources int `json:"resources"` Issues []struct { Message string `json:"message"` } `json:"issues"` } `json:"summary"` } if err := json.NewDecoder(w.Result().Body).Decode(&resp); err != nil { t.Fatalf("parse response decode failed: %v", err) } if resp.Summary.Resources != 1 { t.Fatalf("expected 1 parsed resource, got %d", resp.Summary.Resources) } if len(resp.Summary.Issues) == 0 { t.Fatalf("expected parse issue for invalid file") } if !strings.Contains(resp.Summary.Issues[0].Message, "broken.yaml") { t.Fatalf("expected issue message to include filename, got: %q", resp.Summary.Issues[0].Message) } } func TestParseMultipleUploadedFilesAllInvalidReturnsIssues(t *testing.T) { srv, err := New(config.Config{Addr: ":0", SessionTTL: 30 * time.Minute, MaxUploadSize: 1024 * 1024}) if err != nil { t.Fatalf("server init failed: %v", err) } handler := srv.Handler() var body bytes.Buffer writer := multipart.NewWriter(&body) f1, err := writer.CreateFormFile("manifestFile", "values.yaml") if err != nil { t.Fatalf("failed creating file part 1: %v", err) } _, _ = f1.Write([]byte(`replicaCount: 2 image: repository: nginx `)) f2, err := writer.CreateFormFile("manifestFile", "Chart.yaml") if err != nil { t.Fatalf("failed creating file part 2: %v", err) } _, _ = f2.Write([]byte(`apiVersion: v2 name: sample-chart version: 0.1.0 `)) if err := writer.Close(); err != nil { t.Fatalf("failed to close multipart writer: %v", err) } req := httptest.NewRequest(http.MethodPost, "/api/manifests/parse", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if got := w.Result().StatusCode; got != http.StatusOK { t.Fatalf("parse status: got %d body=%s", got, w.Body.String()) } var resp struct { Summary struct { Resources int `json:"resources"` Issues []struct { Message string `json:"message"` } `json:"issues"` } `json:"summary"` } if err := json.NewDecoder(w.Result().Body).Decode(&resp); err != nil { t.Fatalf("parse response decode failed: %v", err) } if resp.Summary.Resources != 0 { t.Fatalf("expected 0 parsed resources, got %d", resp.Summary.Resources) } if len(resp.Summary.Issues) < 2 { t.Fatalf("expected issues for all invalid files, got %d", len(resp.Summary.Issues)) } } func TestDiffEndpoint(t *testing.T) { srv, err := New(config.Config{Addr: ":0", SessionTTL: 30 * time.Minute, MaxUploadSize: 1024 * 1024}) if err != nil { t.Fatalf("server init failed: %v", err) } handler := srv.Handler() body := map[string]string{ "baseManifest": `apiVersion: v1 kind: ConfigMap metadata: name: cfg namespace: demo data: a: "1" `, "targetManifest": `apiVersion: v1 kind: ConfigMap metadata: name: cfg namespace: demo data: a: "2" --- apiVersion: v1 kind: Service metadata: name: svc namespace: demo `, } payload, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/api/diff", bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if got := w.Result().StatusCode; got != http.StatusOK { t.Fatalf("diff status: got %d", got) } var diff map[string]any if err := json.NewDecoder(w.Result().Body).Decode(&diff); err != nil { t.Fatalf("diff decode failed: %v", err) } changed, _ := diff["changed"].([]any) added, _ := diff["added"].([]any) if len(changed) != 1 { t.Fatalf("expected 1 changed item, got %d", len(changed)) } if len(added) != 1 { t.Fatalf("expected 1 added item, got %d", len(added)) } } func TestGraphEndpointProvidesFindingsAndGroups(t *testing.T) { srv, err := New(config.Config{Addr: ":0", SessionTTL: 30 * time.Minute, MaxUploadSize: 1024 * 1024}) if err != nil { t.Fatalf("server init failed: %v", err) } handler := srv.Handler() manifest := `apiVersion: v1 kind: Service metadata: name: app namespace: demo spec: selector: app: no-match ` body := map[string]string{"manifest": manifest} payload, _ := json.Marshal(body) parseReq := httptest.NewRequest(http.MethodPost, "/api/manifests/parse", bytes.NewReader(payload)) parseReq.Header.Set("Content-Type", "application/json") parseW := httptest.NewRecorder() handler.ServeHTTP(parseW, parseReq) if got := parseW.Result().StatusCode; got != http.StatusOK { t.Fatalf("parse status: got %d", got) } cookies := parseW.Result().Cookies() if len(cookies) == 0 { t.Fatalf("expected session cookie") } graphReq := httptest.NewRequest(http.MethodGet, "/api/graph?groupBy=namespace&collapsed=demo", nil) graphReq.AddCookie(cookies[0]) graphW := httptest.NewRecorder() handler.ServeHTTP(graphW, graphReq) if got := graphW.Result().StatusCode; got != http.StatusOK { t.Fatalf("graph status: got %d", got) } var graph map[string]any if err := json.NewDecoder(graphW.Result().Body).Decode(&graph); err != nil { t.Fatalf("graph decode failed: %v", err) } if findings, _ := graph["findings"].([]any); len(findings) == 0 { t.Fatalf("expected findings in graph response") } if groups, _ := graph["groups"].([]any); len(groups) == 0 { t.Fatalf("expected groups in graph response") } } func TestGitImportValidation(t *testing.T) { srv, err := New(config.Config{Addr: ":0", SessionTTL: 30 * time.Minute, MaxUploadSize: 1024 * 1024}) if err != nil { t.Fatalf("server init failed: %v", err) } handler := srv.Handler() req := httptest.NewRequest(http.MethodPost, "/api/git/import", bytes.NewBufferString(`{"sourceType":"manifests"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if got := w.Result().StatusCode; got != http.StatusBadRequest { t.Fatalf("expected 400 for missing repoURL, got %d", got) } } func TestHelmRenderValidation(t *testing.T) { srv, err := New(config.Config{Addr: ":0", SessionTTL: 30 * time.Minute, MaxUploadSize: 1024 * 1024}) if err != nil { t.Fatalf("server init failed: %v", err) } handler := srv.Handler() req := httptest.NewRequest(http.MethodPost, "/api/helm/render", bytes.NewBufferString(`{}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if got := w.Result().StatusCode; got != http.StatusBadRequest { t.Fatalf("expected 400 for missing repoURL, got %d", got) } } func TestGitImportRejectsNonHTTPSRepoURL(t *testing.T) { srv, err := New(config.Config{Addr: ":0", SessionTTL: 30 * time.Minute, MaxUploadSize: 1024 * 1024}) if err != nil { t.Fatalf("server init failed: %v", err) } handler := srv.Handler() req := httptest.NewRequest(http.MethodPost, "/api/git/import", bytes.NewBufferString(`{"repoURL":"http://github.com/org/repo.git","sourceType":"manifests"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if got := w.Result().StatusCode; got != http.StatusBadRequest { t.Fatalf("expected 400 for non-https repoURL, got %d", got) } } func TestGitImportRejectsDisallowedHost(t *testing.T) { srv, err := New(config.Config{ Addr: ":0", SessionTTL: 30 * time.Minute, MaxUploadSize: 1024 * 1024, GitAllowedHosts: []string{"github.com"}, }) if err != nil { t.Fatalf("server init failed: %v", err) } handler := srv.Handler() req := httptest.NewRequest(http.MethodPost, "/api/git/import", bytes.NewBufferString(`{"repoURL":"https://evil.example/repo.git","sourceType":"manifests"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if got := w.Result().StatusCode; got != http.StatusBadRequest { t.Fatalf("expected 400 for disallowed host, got %d", got) } } func TestDiffRejectsTooLargePayload(t *testing.T) { srv, err := New(config.Config{Addr: ":0", SessionTTL: 30 * time.Minute, MaxUploadSize: 8 * 1024 * 1024}) if err != nil { t.Fatalf("server init failed: %v", err) } handler := srv.Handler() tooLarge := strings.Repeat("a", maxDiffFieldBytes+1) body := map[string]string{ "baseManifest": tooLarge, "targetManifest": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: demo\n", } payload, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/api/diff", bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if got := w.Result().StatusCode; got != http.StatusBadRequest { t.Fatalf("expected 400 for oversized diff payload, got %d", got) } }