package server import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "git.mchus.pro/mchus/logpile/internal/models" ) func TestApplyArchiveSourceMetadata(t *testing.T) { result := &models.AnalysisResult{} applyArchiveSourceMetadata(result) if result.SourceType != models.SourceTypeArchive { t.Fatalf("expected source type %q, got %q", models.SourceTypeArchive, result.SourceType) } if result.Protocol != "" { t.Fatalf("expected empty protocol for archive, got %q", result.Protocol) } if result.TargetHost != "" { t.Fatalf("expected empty target host for archive, got %q", result.TargetHost) } if result.CollectedAt.IsZero() { t.Fatalf("expected collected_at to be set") } } func TestApplyArchiveSourceMetadata_PreservesExistingCollectedAt(t *testing.T) { expected := time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC) result := &models.AnalysisResult{ CollectedAt: expected, } applyArchiveSourceMetadata(result) if !result.CollectedAt.Equal(expected) { t.Fatalf("expected collected_at to be preserved: got %s want %s", result.CollectedAt, expected) } } func TestApplyArchiveSourceMetadata_InferCollectedAtFromEvents(t *testing.T) { oldTs := time.Date(2026, 2, 10, 13, 0, 0, 0, time.UTC) newTs := time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC) result := &models.AnalysisResult{ Events: []models.Event{ {Timestamp: oldTs}, {Timestamp: newTs}, }, } applyArchiveSourceMetadata(result) if !result.CollectedAt.Equal(newTs) { t.Fatalf("expected collected_at from latest event: got %s want %s", result.CollectedAt, newTs) } } func TestApplyArchiveSourceMetadata_InferCollectedAtFromFilename(t *testing.T) { result := &models.AnalysisResult{ Filename: "dump_23E100203_20260228-0428.tar.gz", } applyArchiveSourceMetadata(result) // 2026-02-28 04:28 in Europe/Moscow => 2026-02-28 01:28 UTC want := time.Date(2026, 2, 28, 1, 28, 0, 0, time.UTC) if !result.CollectedAt.Equal(want) { t.Fatalf("expected collected_at from filename: got %s want %s", result.CollectedAt, want) } } func TestApplyArchiveSourceMetadata_IgnoresSyntheticComponentNowEvents(t *testing.T) { realTs := time.Date(2026, 2, 28, 4, 18, 18, 217225000, time.FixedZone("UTC+8", 8*3600)) syntheticNow := time.Date(2026, 3, 5, 10, 0, 0, 0, time.UTC) result := &models.AnalysisResult{ Events: []models.Event{ { Timestamp: realTs, Source: "spx_restservice_ext", SensorType:"syslog", EventType: "System Log", }, { Timestamp: syntheticNow, Source: "Fan", SensorType: "fan", EventType: "Fan Status", }, }, } applyArchiveSourceMetadata(result) if !result.CollectedAt.Equal(realTs.UTC()) { t.Fatalf("expected collected_at from real log timestamp: got %s want %s", result.CollectedAt, realTs.UTC()) } } func TestInferRawExportCollectedAt_PrefersResultCollectedAt(t *testing.T) { expected := time.Date(2026, 2, 25, 8, 0, 0, 0, time.UTC) result := &models.AnalysisResult{CollectedAt: expected} pkg := &RawExportPackage{ ExportedAt: time.Date(2026, 2, 25, 9, 59, 41, 0, time.UTC), Source: RawExportSource{ CollectLogs: []string{ "2026-02-25T09:00:00Z step1", "2026-02-25T09:10:00Z step2", }, }, } got := inferRawExportCollectedAt(result, pkg) if !got.Equal(expected) { t.Fatalf("expected collected_at from result: got %s want %s", got, expected) } } func TestInferRawExportCollectedAt_UsesCollectLogsThenExportedAt(t *testing.T) { hintTs := time.Date(2026, 2, 25, 9, 58, 5, 912975300, time.UTC) pkgWithLogs := &RawExportPackage{ ExportedAt: time.Date(2026, 2, 25, 9, 59, 41, 0, time.UTC), CollectedAtHint: hintTs, Source: RawExportSource{ CollectLogs: []string{ "2026-02-25T09:10:13.7442032Z started", "2026-02-25T09:31:00.5077486Z finished", }, }, } got := inferRawExportCollectedAt(&models.AnalysisResult{}, pkgWithLogs) if !got.Equal(hintTs) { t.Fatalf("expected collected_at from parser_fields hint: got %s want %s", got, hintTs) } pkgFromLogs := &RawExportPackage{ ExportedAt: time.Date(2026, 2, 25, 9, 59, 41, 0, time.UTC), Source: RawExportSource{ CollectLogs: []string{ "2026-02-25T09:10:13.7442032Z started", "2026-02-25T09:31:00.5077486Z finished", }, }, } got = inferRawExportCollectedAt(&models.AnalysisResult{}, pkgFromLogs) wantFromLogs := time.Date(2026, 2, 25, 9, 31, 0, 507748600, time.UTC) if !got.Equal(wantFromLogs) { t.Fatalf("expected collected_at from collect logs: got %s want %s", got, wantFromLogs) } pkgWithoutLogs := &RawExportPackage{ ExportedAt: time.Date(2026, 2, 25, 9, 59, 41, 479023400, time.UTC), } got = inferRawExportCollectedAt(&models.AnalysisResult{}, pkgWithoutLogs) wantFromExportedAt := time.Date(2026, 2, 25, 9, 59, 41, 479023400, time.UTC) if !got.Equal(wantFromExportedAt) { t.Fatalf("expected collected_at from exported_at: got %s want %s", got, wantFromExportedAt) } } func TestApplyCollectSourceMetadata(t *testing.T) { req := CollectRequest{ Host: "bmc-api.local", Protocol: "redfish", Port: 443, Username: "admin", AuthType: "password", Password: "super-secret", TLSMode: "strict", } result := &models.AnalysisResult{ Events: make([]models.Event, 0), FRU: make([]models.FRUInfo, 0), Sensors: make([]models.SensorReading, 0), } applyCollectSourceMetadata(result, req) if result.SourceType != models.SourceTypeAPI { t.Fatalf("expected source type %q, got %q", models.SourceTypeAPI, result.SourceType) } if result.Protocol != req.Protocol { t.Fatalf("expected protocol %q, got %q", req.Protocol, result.Protocol) } if result.TargetHost != req.Host { t.Fatalf("expected target host %q, got %q", req.Host, result.TargetHost) } if result.CollectedAt.IsZero() { t.Fatalf("expected collected_at to be set") } if len(result.Events) != 0 || len(result.FRU) != 0 || len(result.Sensors) != 0 { t.Fatalf("expected empty slices for api result") } raw, err := json.Marshal(result) if err != nil { t.Fatalf("marshal result: %v", err) } if string(raw) == "" { t.Fatalf("expected non-empty json") } if strings.Contains(string(raw), req.Password) || (req.Token != "" && strings.Contains(string(raw), req.Token)) { t.Fatalf("secrets should not be present in api result") } } func TestApplyCollectSourceMetadata_PreservesCollectedAtAndTimezone(t *testing.T) { req := CollectRequest{ Host: "bmc-api.local", Protocol: "redfish", Port: 443, Username: "admin", AuthType: "password", Password: "super-secret", TLSMode: "strict", } collectedAt := time.Date(2026, 2, 28, 4, 18, 18, 0, time.FixedZone("UTC+8", 8*3600)) result := &models.AnalysisResult{ CollectedAt: collectedAt, SourceTimezone: "+08:00", } applyCollectSourceMetadata(result, req) if !result.CollectedAt.Equal(collectedAt) { t.Fatalf("expected collected_at to be preserved: got %s want %s", result.CollectedAt, collectedAt) } if result.SourceTimezone != "+08:00" { t.Fatalf("expected source_timezone to be preserved, got %q", result.SourceTimezone) } } func TestStatusAndConfigExposeSourceMetadata(t *testing.T) { s := &Server{} s.SetDetectedVendor("nvidia") s.SetResult(&models.AnalysisResult{ Filename: "archive.tar.gz", SourceType: models.SourceTypeArchive, Protocol: "", TargetHost: "", CollectedAt: time.Now().UTC(), Events: []models.Event{{ID: "1"}}, Sensors: []models.SensorReading{{Name: "Temp1"}}, FRU: []models.FRUInfo{{Description: "Board"}}, }) statusReq := httptest.NewRequest(http.MethodGet, "/api/status", nil) statusRec := httptest.NewRecorder() s.handleGetStatus(statusRec, statusReq) if statusRec.Code != http.StatusOK { t.Fatalf("expected 200 from /api/status, got %d", statusRec.Code) } var statusPayload map[string]interface{} if err := json.NewDecoder(statusRec.Body).Decode(&statusPayload); err != nil { t.Fatalf("decode status payload: %v", err) } if loaded, _ := statusPayload["loaded"].(bool); !loaded { t.Fatalf("expected loaded=true") } if statusPayload["source_type"] != models.SourceTypeArchive { t.Fatalf("expected source_type in status payload") } if _, ok := statusPayload["stats"]; !ok { t.Fatalf("expected legacy stats field to remain") } configReq := httptest.NewRequest(http.MethodGet, "/api/config", nil) configRec := httptest.NewRecorder() s.handleGetConfig(configRec, configReq) if configRec.Code != http.StatusOK { t.Fatalf("expected 200 from /api/config, got %d", configRec.Code) } var configPayload map[string]interface{} if err := json.NewDecoder(configRec.Body).Decode(&configPayload); err != nil { t.Fatalf("decode config payload: %v", err) } if configPayload["source_type"] != models.SourceTypeArchive { t.Fatalf("expected source_type in config payload") } if _, ok := configPayload["hardware"]; !ok { t.Fatalf("expected legacy hardware field in config payload") } if _, ok := configPayload["specification"]; !ok { t.Fatalf("expected legacy specification field in config payload") } }