260 lines
8.2 KiB
Go
260 lines
8.2 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
)
|
|
|
|
func newCollectTestServer() (*Server, *httptest.Server) {
|
|
s := &Server{
|
|
jobManager: NewJobManager(),
|
|
}
|
|
mux := http.NewServeMux()
|
|
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 TestCollectLifecycleToTerminal(t *testing.T) {
|
|
_, ts := newCollectTestServer()
|
|
defer ts.Close()
|
|
|
|
body := `{"host":"bmc01.local","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"secret","tls_mode":"strict"}`
|
|
resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
|
|
if err != nil {
|
|
t.Fatalf("post collect failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
t.Fatalf("expected 202, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var created CollectJobResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
|
|
t.Fatalf("decode create response: %v", err)
|
|
}
|
|
if created.JobID == "" {
|
|
t.Fatalf("expected job id")
|
|
}
|
|
|
|
status := waitForTerminalStatus(t, ts.URL, created.JobID, 4*time.Second)
|
|
if status.Status != CollectStatusSuccess {
|
|
t.Fatalf("expected success, got %q (error=%q)", status.Status, status.Error)
|
|
}
|
|
if status.Progress == nil || *status.Progress != 100 {
|
|
t.Fatalf("expected progress 100, got %#v", status.Progress)
|
|
}
|
|
if len(status.Logs) < 4 {
|
|
t.Fatalf("expected detailed logs, got %v", status.Logs)
|
|
}
|
|
}
|
|
|
|
func TestCollectCancel(t *testing.T) {
|
|
_, ts := newCollectTestServer()
|
|
defer ts.Close()
|
|
|
|
body := `{"host":"bmc02.local","protocol":"ipmi","port":623,"username":"operator","auth_type":"token","token":"keep-me-secret","tls_mode":"insecure"}`
|
|
resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
|
|
if err != nil {
|
|
t.Fatalf("post collect failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var created CollectJobResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
|
|
t.Fatalf("decode create response: %v", err)
|
|
}
|
|
|
|
cancelResp, err := http.Post(ts.URL+"/api/collect/"+created.JobID+"/cancel", "application/json", nil)
|
|
if err != nil {
|
|
t.Fatalf("cancel collect failed: %v", err)
|
|
}
|
|
defer cancelResp.Body.Close()
|
|
|
|
if cancelResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 cancel, got %d", cancelResp.StatusCode)
|
|
}
|
|
|
|
var canceled CollectJobStatusResponse
|
|
if err := json.NewDecoder(cancelResp.Body).Decode(&canceled); err != nil {
|
|
t.Fatalf("decode cancel response: %v", err)
|
|
}
|
|
if canceled.Status != CollectStatusCanceled {
|
|
t.Fatalf("expected canceled, got %q", canceled.Status)
|
|
}
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
final := getCollectStatus(t, ts.URL, created.JobID, http.StatusOK)
|
|
if final.Status != CollectStatusCanceled {
|
|
t.Fatalf("expected canceled to stay terminal, got %q", final.Status)
|
|
}
|
|
}
|
|
|
|
func TestCollectNotFoundAndSecretLeak(t *testing.T) {
|
|
_, ts := newCollectTestServer()
|
|
defer ts.Close()
|
|
|
|
notFound := getCollectStatus(t, ts.URL, "job_notfound123", http.StatusNotFound)
|
|
if notFound.JobID != "" || notFound.Status != "" {
|
|
t.Fatalf("unexpected body for not found: %+v", notFound)
|
|
}
|
|
cancelResp, err := http.Post(ts.URL+"/api/collect/job_notfound123/cancel", "application/json", nil)
|
|
if err != nil {
|
|
t.Fatalf("cancel not found request failed: %v", err)
|
|
}
|
|
cancelResp.Body.Close()
|
|
if cancelResp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404 for cancel not found, got %d", cancelResp.StatusCode)
|
|
}
|
|
|
|
body := `{"host":"need-fail.local","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"ultra-secret","tls_mode":"strict"}`
|
|
resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
|
|
if err != nil {
|
|
t.Fatalf("post collect failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var created CollectJobResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
|
|
t.Fatalf("decode create response: %v", err)
|
|
}
|
|
|
|
status := waitForTerminalStatus(t, ts.URL, created.JobID, 4*time.Second)
|
|
if status.Status != CollectStatusFailed {
|
|
t.Fatalf("expected failed by host toggle, got %q", status.Status)
|
|
}
|
|
|
|
raw, err := json.Marshal(status)
|
|
if err != nil {
|
|
t.Fatalf("marshal status: %v", err)
|
|
}
|
|
if strings.Contains(string(raw), "ultra-secret") || strings.Contains(strings.Join(status.Logs, " "), "ultra-secret") {
|
|
t.Fatalf("secret leaked into API response or logs")
|
|
}
|
|
}
|
|
|
|
func TestCollectStartPreservesCurrentResultUntilSuccess(t *testing.T) {
|
|
s, ts := newCollectTestServer()
|
|
defer ts.Close()
|
|
|
|
existing := &models.AnalysisResult{
|
|
Filename: "archive.tar.gz",
|
|
SourceType: models.SourceTypeArchive,
|
|
CollectedAt: time.Now().UTC(),
|
|
}
|
|
s.SetResult(existing)
|
|
|
|
body := `{"host":"bmc-success.local","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"secret","tls_mode":"strict"}`
|
|
resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
|
|
if err != nil {
|
|
t.Fatalf("post collect failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var created CollectJobResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
|
|
t.Fatalf("decode create response: %v", err)
|
|
}
|
|
|
|
current := s.GetResult()
|
|
if current != existing {
|
|
t.Fatalf("expected current result to stay unchanged before success")
|
|
}
|
|
|
|
status := waitForTerminalStatus(t, ts.URL, created.JobID, 4*time.Second)
|
|
if status.Status != CollectStatusSuccess {
|
|
t.Fatalf("expected success, got %q", status.Status)
|
|
}
|
|
|
|
finalResult := s.GetResult()
|
|
if finalResult == nil {
|
|
t.Fatalf("expected result to be set on success")
|
|
}
|
|
if finalResult.SourceType != models.SourceTypeAPI {
|
|
t.Fatalf("expected api source type after success, got %q", finalResult.SourceType)
|
|
}
|
|
if finalResult.TargetHost != "bmc-success.local" {
|
|
t.Fatalf("expected target host to be updated, got %q", finalResult.TargetHost)
|
|
}
|
|
}
|
|
|
|
func TestCollectFailedDoesNotOverwriteCurrentResult(t *testing.T) {
|
|
s, ts := newCollectTestServer()
|
|
defer ts.Close()
|
|
|
|
existing := &models.AnalysisResult{
|
|
Filename: "still-archive.tar.gz",
|
|
SourceType: models.SourceTypeArchive,
|
|
CollectedAt: time.Now().UTC(),
|
|
}
|
|
s.SetResult(existing)
|
|
|
|
body := `{"host":"contains-fail.local","protocol":"redfish","port":443,"username":"admin","auth_type":"password","password":"secret","tls_mode":"strict"}`
|
|
resp, err := http.Post(ts.URL+"/api/collect", "application/json", bytes.NewBufferString(body))
|
|
if err != nil {
|
|
t.Fatalf("post collect failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var created CollectJobResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
|
|
t.Fatalf("decode create response: %v", err)
|
|
}
|
|
|
|
status := waitForTerminalStatus(t, ts.URL, created.JobID, 4*time.Second)
|
|
if status.Status != CollectStatusFailed {
|
|
t.Fatalf("expected failed, got %q", status.Status)
|
|
}
|
|
|
|
finalResult := s.GetResult()
|
|
if finalResult != existing {
|
|
t.Fatalf("expected existing result to remain on failed job")
|
|
}
|
|
}
|
|
|
|
func waitForTerminalStatus(t *testing.T, baseURL, jobID string, timeout time.Duration) CollectJobStatusResponse {
|
|
t.Helper()
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
status := getCollectStatus(t, baseURL, jobID, http.StatusOK)
|
|
if status.Status == CollectStatusSuccess || status.Status == CollectStatusFailed || status.Status == CollectStatusCanceled {
|
|
return status
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
t.Fatalf("job %s did not reach terminal status before timeout", jobID)
|
|
return CollectJobStatusResponse{}
|
|
}
|
|
|
|
func getCollectStatus(t *testing.T, baseURL, jobID string, expectedCode int) CollectJobStatusResponse {
|
|
t.Helper()
|
|
resp, err := http.Get(baseURL + "/api/collect/" + jobID)
|
|
if err != nil {
|
|
t.Fatalf("get collect status failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != expectedCode {
|
|
t.Fatalf("expected status %d, got %d", expectedCode, resp.StatusCode)
|
|
}
|
|
|
|
if expectedCode != http.StatusOK {
|
|
return CollectJobStatusResponse{}
|
|
}
|
|
|
|
var status CollectJobStatusResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
|
|
t.Fatalf("decode collect status: %v", err)
|
|
}
|
|
return status
|
|
}
|