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