feat(models): add source metadata to analysis result

Closes #7
This commit is contained in:
Mikhail Chusavitin
2026-02-04 10:09:15 +03:00
parent d38d0c9d30
commit 596eda709c
4 changed files with 221 additions and 33 deletions

View File

@@ -62,6 +62,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
}
result := p.Result()
applyArchiveSourceMetadata(result)
s.SetResult(result)
s.SetDetectedVendor(p.DetectedVendor())
@@ -114,18 +115,31 @@ func (s *Server) handleGetSensors(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
result := s.GetResult()
if result == nil || result.Hardware == nil {
if result == nil {
jsonResponse(w, map[string]interface{}{})
return
}
response := map[string]interface{}{
"source_type": result.SourceType,
"protocol": result.Protocol,
"target_host": result.TargetHost,
"collected_at": result.CollectedAt,
}
if result.Hardware == nil {
response["hardware"] = map[string]interface{}{}
response["specification"] = []SpecLine{}
jsonResponse(w, response)
return
}
// Build specification summary
spec := buildSpecification(result)
jsonResponse(w, map[string]interface{}{
"hardware": result.Hardware,
"specification": spec,
})
response["hardware"] = result.Hardware
response["specification"] = spec
jsonResponse(w, response)
}
// SpecLine represents a single line in specification
@@ -495,9 +509,13 @@ func (s *Server) handleGetStatus(w http.ResponseWriter, r *http.Request) {
}
jsonResponse(w, map[string]interface{}{
"loaded": true,
"filename": result.Filename,
"vendor": s.GetDetectedVendor(),
"loaded": true,
"filename": result.Filename,
"vendor": s.GetDetectedVendor(),
"source_type": result.SourceType,
"protocol": result.Protocol,
"target_host": result.TargetHost,
"collected_at": result.CollectedAt,
"stats": map[string]int{
"events": len(result.Events),
"sensors": len(result.Sensors),
@@ -574,6 +592,8 @@ func (s *Server) handleCollectStart(w http.ResponseWriter, r *http.Request) {
}
job := s.jobManager.CreateJob(req)
s.SetResult(newAPIResult(req))
s.SetDetectedVendor("")
s.startMockCollectionJob(job.ID, req)
w.Header().Set("Content-Type", "application/json")
@@ -726,6 +746,28 @@ func generateJobID() string {
return fmt.Sprintf("job_%x", buf)
}
func applyArchiveSourceMetadata(result *models.AnalysisResult) {
if result == nil {
return
}
result.SourceType = models.SourceTypeArchive
result.Protocol = ""
result.TargetHost = ""
result.CollectedAt = time.Now().UTC()
}
func newAPIResult(req CollectRequest) *models.AnalysisResult {
return &models.AnalysisResult{
SourceType: models.SourceTypeAPI,
Protocol: req.Protocol,
TargetHost: req.Host,
CollectedAt: time.Now().UTC(),
Events: make([]models.Event, 0),
FRU: make([]models.FRUInfo, 0),
Sensors: make([]models.SensorReading, 0),
}
}
func jsonResponse(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)

View File

@@ -0,0 +1,131 @@
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 TestNewAPIResultMetadata(t *testing.T) {
req := CollectRequest{
Host: "bmc-api.local",
Protocol: "redfish",
Port: 443,
Username: "admin",
AuthType: "password",
Password: "super-secret",
TLSMode: "strict",
}
result := newAPIResult(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 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")
}
}