diff --git a/internal/server/upload_live_smoke_test.go b/internal/server/upload_live_smoke_test.go new file mode 100644 index 0000000..9a0b1b8 --- /dev/null +++ b/internal/server/upload_live_smoke_test.go @@ -0,0 +1,193 @@ +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(), + } + 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 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 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"]) + } +}