feat: improve timeline/admin flows and ingest projection repairs
This commit is contained in:
@@ -102,6 +102,146 @@ func TestTimelineInstalledRemovedFirmwareChanged(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssetTimelineMovementBulkByIngestSourceRef(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()
|
||||
historySvc := history.NewService(db)
|
||||
RegisterIngestRoutes(mux, IngestDependencies{Service: ingest.NewService(db)})
|
||||
RegisterAssetComponentRoutes(mux, AssetComponentDependencies{
|
||||
Assets: registry.NewAssetRepository(db),
|
||||
Components: registry.NewComponentRepository(db),
|
||||
History: historySvc,
|
||||
})
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
collectedAt := time.Now().UTC().Format(time.RFC3339)
|
||||
payload := map[string]any{
|
||||
"target_host": "server-asset-bulk-01",
|
||||
"collected_at": collectedAt,
|
||||
"hardware": map[string]any{
|
||||
"board": map[string]any{"serial_number": "ASSET-BULK-01"},
|
||||
"storage": []map[string]any{
|
||||
{"serial_number": "VSN-BULK-A", "firmware": "1.0.0", "present": true, "status": "OK"},
|
||||
{"serial_number": "VSN-BULK-B", "firmware": "1.0.0", "present": true, "status": "OK"},
|
||||
},
|
||||
},
|
||||
}
|
||||
postJSON(t, server.URL+"/ingest/hardware", payload, http.StatusCreated)
|
||||
|
||||
assetID := lookupAssetID(t, db, "ASSET-BULK-01")
|
||||
events := fetchTimelineCards(t, server.URL+"/assets/"+assetID+"/timeline", 100, nil)
|
||||
|
||||
var found bool
|
||||
for _, card := range events {
|
||||
if card.VisualAction != "component_installed" {
|
||||
continue
|
||||
}
|
||||
if card.Kind == "bulk" && card.Counts.Events >= 2 && card.Counts.AffectedComponents >= 2 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected bulk component_installed card for asset timeline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssetTimelineFirmwareBulkPerHourSingleCard(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()
|
||||
historySvc := history.NewService(db)
|
||||
RegisterIngestRoutes(mux, IngestDependencies{Service: ingest.NewService(db)})
|
||||
RegisterAssetComponentRoutes(mux, AssetComponentDependencies{
|
||||
Assets: registry.NewAssetRepository(db),
|
||||
Components: registry.NewComponentRepository(db),
|
||||
History: historySvc,
|
||||
})
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
firstHour := time.Now().UTC().Truncate(time.Hour).Add(-2 * time.Hour)
|
||||
payload1 := map[string]any{
|
||||
"target_host": "server-fw-bulk-01",
|
||||
"collected_at": firstHour.Format(time.RFC3339),
|
||||
"hardware": map[string]any{
|
||||
"board": map[string]any{"serial_number": "ASSET-FW-BULK-01"},
|
||||
"storage": []map[string]any{
|
||||
{"serial_number": "VSN-FW-A", "firmware": "1.0.0", "present": true, "status": "OK"},
|
||||
{"serial_number": "VSN-FW-B", "firmware": "1.0.0", "present": true, "status": "OK"},
|
||||
},
|
||||
},
|
||||
}
|
||||
postJSON(t, server.URL+"/ingest/hardware", payload1, http.StatusCreated)
|
||||
|
||||
secondHour := firstHour.Add(40 * time.Minute)
|
||||
payload2 := map[string]any{
|
||||
"target_host": "server-fw-bulk-01",
|
||||
"collected_at": secondHour.Format(time.RFC3339),
|
||||
"hardware": map[string]any{
|
||||
"board": map[string]any{"serial_number": "ASSET-FW-BULK-01"},
|
||||
"storage": []map[string]any{
|
||||
{"serial_number": "VSN-FW-A", "firmware": "2.0.0", "present": true, "status": "OK"},
|
||||
{"serial_number": "VSN-FW-B", "firmware": "2.0.0", "present": true, "status": "OK"},
|
||||
},
|
||||
},
|
||||
}
|
||||
postJSON(t, server.URL+"/ingest/hardware", payload2, http.StatusCreated)
|
||||
|
||||
assetID := lookupAssetID(t, db, "ASSET-FW-BULK-01")
|
||||
events := fetchTimelineCards(t, server.URL+"/assets/"+assetID+"/timeline", 100, nil)
|
||||
|
||||
var fwBulkCount int
|
||||
for _, card := range events {
|
||||
if card.VisualAction != "firmware_changed" {
|
||||
continue
|
||||
}
|
||||
if card.Kind != "bulk" {
|
||||
continue
|
||||
}
|
||||
if card.Counts.Events >= 2 && card.Counts.AffectedComponents >= 2 {
|
||||
fwBulkCount++
|
||||
}
|
||||
}
|
||||
if fwBulkCount != 1 {
|
||||
t.Fatalf("expected exactly one bulk firmware_changed card, got %d", fwBulkCount)
|
||||
}
|
||||
}
|
||||
|
||||
func postJSON(t *testing.T, url string, payload any, expectedStatus int) {
|
||||
t.Helper()
|
||||
body, err := json.Marshal(payload)
|
||||
@@ -127,6 +267,15 @@ func lookupComponentID(t *testing.T, db *sql.DB, serial string) string {
|
||||
return id
|
||||
}
|
||||
|
||||
func lookupAssetID(t *testing.T, db *sql.DB, serial string) string {
|
||||
t.Helper()
|
||||
var id string
|
||||
if err := db.QueryRow(`SELECT id FROM machines WHERE vendor_serial = ?`, serial).Scan(&id); err != nil {
|
||||
t.Fatalf("lookup asset: %v", err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func fetchTimelineCards(t *testing.T, baseURL string, limit int, cursor *string) []history.TimelineCard {
|
||||
t.Helper()
|
||||
resp := fetchTimelineResponse(t, baseURL, limit, cursor)
|
||||
|
||||
Reference in New Issue
Block a user