Files
core/internal/api/ingest_hardware_test.go

234 lines
6.2 KiB
Go

package api
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"reanimator/internal/ingest"
"reanimator/internal/repository"
)
func TestIngestHardwareIdempotent(t *testing.T) {
dsn := os.Getenv("DATABASE_DSN")
if dsn == "" {
t.Skip("DATABASE_DSN not set")
}
db, err := repository.Open(dsn)
if err != nil {
t.Fatalf("open db: %v", err)
}
defer db.Close()
if err := applyMigrations(db); err != nil {
t.Fatalf("apply migrations: %v", err)
}
if err := cleanupRegistry(db); err != nil {
t.Fatalf("cleanup: %v", err)
}
mux := http.NewServeMux()
RegisterIngestRoutes(mux, IngestDependencies{Service: ingest.NewService(db)})
server := httptest.NewServer(mux)
defer server.Close()
payload := map[string]any{
"target_host": "hwserver-1",
"collected_at": time.Now().UTC().Format(time.RFC3339),
"hardware": map[string]any{
"board": map[string]any{"serial_number": "BOARD123"},
"cpus": []map[string]any{{"socket": 0, "status": "OK"}},
},
}
body, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal payload: %v", err)
}
resp, err := http.Post(server.URL+"/ingest/hardware", "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("post: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("expected 201, got %d", resp.StatusCode)
}
var createdResp struct {
Summary struct {
TimelineEventsCreated int `json:"timeline_events_created"`
} `json:"summary"`
}
createdBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
if err := json.Unmarshal(createdBody, &createdResp); err != nil {
t.Fatalf("decode body: %v", err)
}
if createdResp.Summary.TimelineEventsCreated != 3 {
t.Fatalf("expected timeline_events_created=3, got %d", createdResp.Summary.TimelineEventsCreated)
}
resp, err = http.Post(server.URL+"/ingest/hardware", "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("post duplicate: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200 on duplicate, got %d", resp.StatusCode)
}
assertCount(t, db, "log_bundles", 1)
assertCount(t, db, "observations", 1)
assertCount(t, db, "installations", 1)
assertCount(t, db, "components", 1)
}
func TestIngestHardwareStatusEvents(t *testing.T) {
dsn := os.Getenv("DATABASE_DSN")
if dsn == "" {
t.Skip("DATABASE_DSN not set")
}
db, err := repository.Open(dsn)
if err != nil {
t.Fatalf("open db: %v", err)
}
defer db.Close()
if err := applyMigrations(db); err != nil {
t.Fatalf("apply migrations: %v", err)
}
if err := cleanupRegistry(db); err != nil {
t.Fatalf("cleanup: %v", err)
}
mux := http.NewServeMux()
RegisterIngestRoutes(mux, IngestDependencies{Service: ingest.NewService(db)})
server := httptest.NewServer(mux)
defer server.Close()
payload := map[string]any{
"target_host": "status-server",
"collected_at": time.Now().UTC().Format(time.RFC3339),
"hardware": map[string]any{
"board": map[string]any{"serial_number": "STATUS-123"},
"storage": []map[string]any{
{
"slot": "Storage-0",
"serial_number": "WARN-001",
"present": true,
"status": "Warning",
},
{
"slot": "Storage-1",
"serial_number": "CRIT-001",
"present": true,
"status": "Critical",
},
},
},
}
body, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal payload: %v", err)
}
resp, err := http.Post(server.URL+"/ingest/hardware", "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("post: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("expected 201, got %d", resp.StatusCode)
}
assertCountQuery(t, db, "SELECT COUNT(*) FROM failure_events", 1)
assertCountQuery(t, db, "SELECT COUNT(*) FROM timeline_events WHERE event_type = 'COMPONENT_WARNING'", 2)
assertCountQuery(t, db, "SELECT COUNT(*) FROM timeline_events WHERE event_type = 'COMPONENT_FAILED'", 2)
}
func TestIngestHardwareFirmwareChange(t *testing.T) {
dsn := os.Getenv("DATABASE_DSN")
if dsn == "" {
t.Skip("DATABASE_DSN not set")
}
db, err := repository.Open(dsn)
if err != nil {
t.Fatalf("open db: %v", err)
}
defer db.Close()
if err := applyMigrations(db); err != nil {
t.Fatalf("apply migrations: %v", err)
}
if err := cleanupRegistry(db); err != nil {
t.Fatalf("cleanup: %v", err)
}
mux := http.NewServeMux()
RegisterIngestRoutes(mux, IngestDependencies{Service: ingest.NewService(db)})
server := httptest.NewServer(mux)
defer server.Close()
boardSerial := "FIRMWARE-123"
firstPayload := map[string]any{
"target_host": "firmware-server",
"collected_at": time.Now().UTC().Format(time.RFC3339),
"hardware": map[string]any{
"board": map[string]any{"serial_number": boardSerial},
"firmware": []map[string]any{
{"device_name": "BIOS", "version": "1.0.0"},
},
},
}
secondPayload := map[string]any{
"target_host": "firmware-server",
"collected_at": time.Now().UTC().Add(5 * time.Minute).Format(time.RFC3339),
"hardware": map[string]any{
"board": map[string]any{"serial_number": boardSerial},
"firmware": []map[string]any{
{"device_name": "BIOS", "version": "1.1.0"},
},
},
}
for _, payload := range []map[string]any{firstPayload, secondPayload} {
body, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal payload: %v", err)
}
resp, err := http.Post(server.URL+"/ingest/hardware", "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("post: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("expected 201, got %d", resp.StatusCode)
}
}
var version string
row := db.QueryRow(`
SELECT firmware_version
FROM asset_firmware_states
WHERE asset_id = (SELECT id FROM assets WHERE vendor_serial = ? LIMIT 1) AND device_name = ?
`, boardSerial, "BIOS")
if err := row.Scan(&version); err != nil {
t.Fatalf("firmware state query: %v", err)
}
if version != "1.1.0" {
t.Fatalf("expected firmware_version 1.1.0, got %s", version)
}
assertCountQuery(t, db, "SELECT COUNT(*) FROM timeline_events WHERE subject_type = 'asset' AND event_type = 'FIRMWARE_CHANGED'", 2)
}