Files
kubeviz/internal/httpserver/server_test.go
Clemens Hering 1a0bbe9dfd
Some checks failed
Deploy KubeViz / deploy (push) Has been cancelled
Teststand
2026-03-01 07:40:49 +01:00

460 lines
13 KiB
Go

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)
}
}