460 lines
13 KiB
Go
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)
|
|
}
|
|
}
|