333 lines
9.9 KiB
Go
333 lines
9.9 KiB
Go
package server
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"encoding/json"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors"
|
|
)
|
|
|
|
func newFlowTestServer() (*Server, *httptest.Server) {
|
|
s := &Server{
|
|
jobManager: NewJobManager(),
|
|
collectors: testCollectorRegistry(),
|
|
}
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("POST /api/upload", s.handleUpload)
|
|
mux.HandleFunc("GET /api/status", s.handleGetStatus)
|
|
mux.HandleFunc("POST /api/collect", s.handleCollectStart)
|
|
mux.HandleFunc("GET /api/collect/{id}", s.handleCollectStatus)
|
|
mux.HandleFunc("POST /api/collect/{id}/cancel", s.handleCollectCancel)
|
|
return s, httptest.NewServer(mux)
|
|
}
|
|
|
|
func TestUploadArchiveRegressionAndSourceMetadata(t *testing.T) {
|
|
_, ts := newFlowTestServer()
|
|
defer ts.Close()
|
|
|
|
archiveBody := buildTarArchive(t, "logs/plain.txt", "smoke archive content")
|
|
reqBody := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(reqBody)
|
|
part, err := writer.CreateFormFile("archive", "smoke.tar")
|
|
if err != nil {
|
|
t.Fatalf("create form file: %v", err)
|
|
}
|
|
if _, err := part.Write(archiveBody); err != nil {
|
|
t.Fatalf("write archive body: %v", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("close multipart writer: %v", err)
|
|
}
|
|
|
|
uploadReq, err := http.NewRequest(http.MethodPost, ts.URL+"/api/upload", reqBody)
|
|
if err != nil {
|
|
t.Fatalf("build upload request: %v", err)
|
|
}
|
|
uploadReq.Header.Set("Content-Type", writer.FormDataContentType())
|
|
|
|
uploadResp, err := http.DefaultClient.Do(uploadReq)
|
|
if err != nil {
|
|
t.Fatalf("upload request failed: %v", err)
|
|
}
|
|
defer uploadResp.Body.Close()
|
|
|
|
if uploadResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 from /api/upload, got %d", uploadResp.StatusCode)
|
|
}
|
|
|
|
var uploadPayload map[string]interface{}
|
|
if err := json.NewDecoder(uploadResp.Body).Decode(&uploadPayload); err != nil {
|
|
t.Fatalf("decode upload response: %v", err)
|
|
}
|
|
if uploadPayload["status"] != "ok" {
|
|
t.Fatalf("expected upload status ok, got %v", uploadPayload["status"])
|
|
}
|
|
if uploadPayload["filename"] != "smoke.tar" {
|
|
t.Fatalf("expected filename smoke.tar, got %v", uploadPayload["filename"])
|
|
}
|
|
stats, ok := uploadPayload["stats"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected stats object in upload response")
|
|
}
|
|
if events, ok := stats["events"].(float64); !ok || events < 1 {
|
|
t.Fatalf("expected at least one parsed event, got %v", stats["events"])
|
|
}
|
|
|
|
statusResp, err := http.Get(ts.URL + "/api/status")
|
|
if err != nil {
|
|
t.Fatalf("status request failed: %v", err)
|
|
}
|
|
defer statusResp.Body.Close()
|
|
|
|
if statusResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 from /api/status, got %d", statusResp.StatusCode)
|
|
}
|
|
|
|
var statusPayload map[string]interface{}
|
|
if err := json.NewDecoder(statusResp.Body).Decode(&statusPayload); err != nil {
|
|
t.Fatalf("decode status response: %v", err)
|
|
}
|
|
if loaded, _ := statusPayload["loaded"].(bool); !loaded {
|
|
t.Fatalf("expected loaded=true after upload")
|
|
}
|
|
if statusPayload["source_type"] != "archive" {
|
|
t.Fatalf("expected source_type=archive, got %v", statusPayload["source_type"])
|
|
}
|
|
if protocol, _ := statusPayload["protocol"].(string); protocol != "" {
|
|
t.Fatalf("expected empty protocol for archive, got %q", protocol)
|
|
}
|
|
if targetHost, _ := statusPayload["target_host"].(string); targetHost != "" {
|
|
t.Fatalf("expected empty target_host for archive, got %q", targetHost)
|
|
}
|
|
if collectedAt, _ := statusPayload["collected_at"].(string); strings.TrimSpace(collectedAt) == "" {
|
|
t.Fatalf("expected non-empty collected_at for archive")
|
|
}
|
|
}
|
|
|
|
func TestUploadTXTFile(t *testing.T) {
|
|
_, ts := newFlowTestServer()
|
|
defer ts.Close()
|
|
|
|
txt := `Version:
|
|
--------
|
|
14.3.0.5
|
|
|
|
loader_brand="XigmaNAS"
|
|
`
|
|
|
|
reqBody := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(reqBody)
|
|
part, err := writer.CreateFormFile("archive", "xigmanas.txt")
|
|
if err != nil {
|
|
t.Fatalf("create form file: %v", err)
|
|
}
|
|
if _, err := part.Write([]byte(txt)); err != nil {
|
|
t.Fatalf("write txt body: %v", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("close multipart writer: %v", err)
|
|
}
|
|
|
|
uploadReq, err := http.NewRequest(http.MethodPost, ts.URL+"/api/upload", reqBody)
|
|
if err != nil {
|
|
t.Fatalf("build upload request: %v", err)
|
|
}
|
|
uploadReq.Header.Set("Content-Type", writer.FormDataContentType())
|
|
|
|
uploadResp, err := http.DefaultClient.Do(uploadReq)
|
|
if err != nil {
|
|
t.Fatalf("upload request failed: %v", err)
|
|
}
|
|
defer uploadResp.Body.Close()
|
|
|
|
if uploadResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 from /api/upload, got %d", uploadResp.StatusCode)
|
|
}
|
|
|
|
var uploadPayload map[string]interface{}
|
|
if err := json.NewDecoder(uploadResp.Body).Decode(&uploadPayload); err != nil {
|
|
t.Fatalf("decode upload response: %v", err)
|
|
}
|
|
if uploadPayload["status"] != "ok" {
|
|
t.Fatalf("expected upload status ok, got %v", uploadPayload["status"])
|
|
}
|
|
if uploadPayload["filename"] != "xigmanas.txt" {
|
|
t.Fatalf("expected filename xigmanas.txt, got %v", uploadPayload["filename"])
|
|
}
|
|
if uploadPayload["vendor"] != "XigmaNAS Parser" {
|
|
t.Fatalf("expected vendor XigmaNAS Parser, got %v", uploadPayload["vendor"])
|
|
}
|
|
}
|
|
|
|
func TestCollectSmokeErrorFormat(t *testing.T) {
|
|
_, ts := newFlowTestServer()
|
|
defer ts.Close()
|
|
|
|
invalidJSONResp, err := http.Post(ts.URL+"/api/collect", "application/json", strings.NewReader("{"))
|
|
if err != nil {
|
|
t.Fatalf("post collect invalid json failed: %v", err)
|
|
}
|
|
defer invalidJSONResp.Body.Close()
|
|
|
|
if invalidJSONResp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for invalid json, got %d", invalidJSONResp.StatusCode)
|
|
}
|
|
assertJSONError(t, invalidJSONResp, "Invalid JSON body")
|
|
|
|
invalidFieldsBody := `{"host":"","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"secret","tls_mode":"strict"}`
|
|
invalidFieldsResp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(invalidFieldsBody))
|
|
if err != nil {
|
|
t.Fatalf("post collect invalid fields failed: %v", err)
|
|
}
|
|
defer invalidFieldsResp.Body.Close()
|
|
|
|
if invalidFieldsResp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422 for invalid fields, got %d", invalidFieldsResp.StatusCode)
|
|
}
|
|
assertJSONError(t, invalidFieldsResp, "field 'host' is required")
|
|
}
|
|
|
|
func TestCollectStatusNotFoundSmoke(t *testing.T) {
|
|
_, ts := newFlowTestServer()
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/api/collect/job_notfound123456")
|
|
if err != nil {
|
|
t.Fatalf("get collect status failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404 for missing collect job, got %d", resp.StatusCode)
|
|
}
|
|
assertJSONError(t, resp, "Collect job not found")
|
|
}
|
|
|
|
func TestUploadRedfishSnapshotJSON(t *testing.T) {
|
|
_, ts := newFlowTestServer()
|
|
defer ts.Close()
|
|
|
|
snapshot := `{
|
|
"filename": "redfish://bmc01.local",
|
|
"source_type": "api",
|
|
"protocol": "redfish",
|
|
"target_host": "bmc01.local",
|
|
"hardware": {
|
|
"storage": [
|
|
{
|
|
"slot": "Drive1",
|
|
"type": "NVMe",
|
|
"model": "KIOXIA CD8",
|
|
"size_gb": 3840,
|
|
"serial_number": "SN-NVME-1",
|
|
"present": true
|
|
}
|
|
]
|
|
},
|
|
"raw_payloads": {
|
|
"redfish_tree": {
|
|
"/redfish/v1": {"Name": "ServiceRoot"}
|
|
}
|
|
}
|
|
}`
|
|
|
|
reqBody := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(reqBody)
|
|
part, err := writer.CreateFormFile("archive", "snapshot.json")
|
|
if err != nil {
|
|
t.Fatalf("create form file: %v", err)
|
|
}
|
|
if _, err := part.Write([]byte(snapshot)); err != nil {
|
|
t.Fatalf("write snapshot body: %v", err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatalf("close multipart writer: %v", err)
|
|
}
|
|
|
|
uploadReq, err := http.NewRequest(http.MethodPost, ts.URL+"/api/upload", reqBody)
|
|
if err != nil {
|
|
t.Fatalf("build upload request: %v", err)
|
|
}
|
|
uploadReq.Header.Set("Content-Type", writer.FormDataContentType())
|
|
|
|
uploadResp, err := http.DefaultClient.Do(uploadReq)
|
|
if err != nil {
|
|
t.Fatalf("upload request failed: %v", err)
|
|
}
|
|
defer uploadResp.Body.Close()
|
|
|
|
if uploadResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 from /api/upload, got %d", uploadResp.StatusCode)
|
|
}
|
|
|
|
var uploadPayload map[string]interface{}
|
|
if err := json.NewDecoder(uploadResp.Body).Decode(&uploadPayload); err != nil {
|
|
t.Fatalf("decode upload response: %v", err)
|
|
}
|
|
if uploadPayload["vendor"] != "redfish" {
|
|
t.Fatalf("expected vendor redfish, got %v", uploadPayload["vendor"])
|
|
}
|
|
|
|
statusResp, err := http.Get(ts.URL + "/api/status")
|
|
if err != nil {
|
|
t.Fatalf("status request failed: %v", err)
|
|
}
|
|
defer statusResp.Body.Close()
|
|
|
|
var statusPayload map[string]interface{}
|
|
if err := json.NewDecoder(statusResp.Body).Decode(&statusPayload); err != nil {
|
|
t.Fatalf("decode status response: %v", err)
|
|
}
|
|
if statusPayload["protocol"] != "redfish" {
|
|
t.Fatalf("expected protocol redfish, got %v", statusPayload["protocol"])
|
|
}
|
|
if statusPayload["filename"] != "redfish://bmc01.local" {
|
|
t.Fatalf("expected snapshot filename, got %v", statusPayload["filename"])
|
|
}
|
|
}
|
|
|
|
func buildTarArchive(t *testing.T, name, content string) []byte {
|
|
t.Helper()
|
|
|
|
var buf bytes.Buffer
|
|
tw := tar.NewWriter(&buf)
|
|
if err := tw.WriteHeader(&tar.Header{
|
|
Name: name,
|
|
Mode: 0o600,
|
|
Size: int64(len(content)),
|
|
}); err != nil {
|
|
t.Fatalf("write tar header: %v", err)
|
|
}
|
|
if _, err := tw.Write([]byte(content)); err != nil {
|
|
t.Fatalf("write tar content: %v", err)
|
|
}
|
|
if err := tw.Close(); err != nil {
|
|
t.Fatalf("close tar writer: %v", err)
|
|
}
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func assertJSONError(t *testing.T, resp *http.Response, expectedMessage string) {
|
|
t.Helper()
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if !strings.Contains(contentType, "application/json") {
|
|
t.Fatalf("expected application/json error response, got %q", contentType)
|
|
}
|
|
|
|
var payload map[string]string
|
|
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode error payload: %v", err)
|
|
}
|
|
if payload["error"] != expectedMessage {
|
|
t.Fatalf("expected error %q, got %q", expectedMessage, payload["error"])
|
|
}
|
|
}
|