Files
core/internal/api/failures_api_test.go

239 lines
7.6 KiB
Go

package api
import (
"bytes"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"reanimator/internal/domain"
"reanimator/internal/history"
"reanimator/internal/repository/failures"
"reanimator/internal/repository/registry"
)
func TestFailuresPostRegistersManualFailureAndHistoryStatus(t *testing.T) {
db := openTestDB(t)
defer db.Close()
historySvc := history.NewService(db)
mux := http.NewServeMux()
assetRepo := registry.NewAssetRepository(db)
componentRepo := registry.NewComponentRepository(db)
installRepo := registry.NewInstallationRepository(db)
failureRepo := failures.NewFailureRepository(db)
RegisterRegistryRoutes(mux, RegistryDependencies{Assets: assetRepo, Components: componentRepo, History: historySvc})
RegisterHistoryRoutes(mux, HistoryDependencies{Service: historySvc})
RegisterFailureRoutes(mux, FailureDependencies{
Failures: failureRepo,
Assets: assetRepo,
Components: componentRepo,
Installations: installRepo,
History: historySvc,
})
server := httptest.NewServer(mux)
defer server.Close()
component := createComponentViaAPI(t, server.URL)
asset := createAssetViaAPI(t, server.URL)
insertOpenInstallation(t, db, asset.ID, component.ID, "AOC#1")
payload := map[string]any{
"component_serial": component.VendorSerial,
"server_serial": asset.VendorSerial,
"failure_date": "2026-02-23",
"description": "manual test failure",
}
body := postJSONWithResponse(t, server.URL+"/failures", payload, http.StatusOK)
var resp struct {
Status string `json:"status"`
FailureEventID string `json:"failure_event_id"`
HistoryEventID *string `json:"history_event_id"`
}
if err := json.Unmarshal(body, &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.Status != "registered" {
t.Fatalf("unexpected status: %q", resp.Status)
}
if resp.FailureEventID == "" {
t.Fatalf("missing failure_event_id")
}
if resp.HistoryEventID == nil || *resp.HistoryEventID == "" {
t.Fatalf("missing history_event_id")
}
var source, failureType string
if err := db.QueryRow(`SELECT source, failure_type FROM failure_events WHERE id = ?`, resp.FailureEventID).Scan(&source, &failureType); err != nil {
t.Fatalf("query failure event: %v", err)
}
if source != "manual_ui" {
t.Fatalf("expected source manual_ui, got %q", source)
}
if failureType != "component_failed_manual" {
t.Fatalf("expected manual failure type, got %q", failureType)
}
var historyCount int
if err := db.QueryRow(`SELECT COUNT(*) FROM component_change_events WHERE id = ? AND part_id = ? AND change_type = 'COMPONENT_STATUS_SET'`, *resp.HistoryEventID, component.ID).Scan(&historyCount); err != nil {
t.Fatalf("query component_change_events: %v", err)
}
if historyCount != 1 {
t.Fatalf("expected history status event count=1, got %d", historyCount)
}
}
func TestFailuresPostRejectsServerOnlyInput(t *testing.T) {
db := openTestDB(t)
defer db.Close()
historySvc := history.NewService(db)
mux := http.NewServeMux()
RegisterFailureRoutes(mux, FailureDependencies{
Failures: failures.NewFailureRepository(db),
Assets: registry.NewAssetRepository(db),
Components: registry.NewComponentRepository(db),
History: historySvc,
})
server := httptest.NewServer(mux)
defer server.Close()
payload := map[string]any{
"server_serial": "SRV-ONLY-1",
"failure_date": "2026-02-23",
"description": "server only",
}
body, status := postJSONRaw(t, server.URL+"/failures", payload)
if status != http.StatusBadRequest {
t.Fatalf("expected 400, got %d body=%s", status, string(body))
}
}
func TestFailuresPostRejectsMismatchedServerSerialForInstalledComponent(t *testing.T) {
db := openTestDB(t)
defer db.Close()
historySvc := history.NewService(db)
mux := http.NewServeMux()
assetRepo := registry.NewAssetRepository(db)
componentRepo := registry.NewComponentRepository(db)
installRepo := registry.NewInstallationRepository(db)
failureRepo := failures.NewFailureRepository(db)
RegisterRegistryRoutes(mux, RegistryDependencies{Assets: assetRepo, Components: componentRepo, History: historySvc})
RegisterHistoryRoutes(mux, HistoryDependencies{Service: historySvc})
RegisterFailureRoutes(mux, FailureDependencies{
Failures: failureRepo,
Assets: assetRepo,
Components: componentRepo,
Installations: installRepo,
History: historySvc,
})
server := httptest.NewServer(mux)
defer server.Close()
component := createComponentViaAPI(t, server.URL)
assetA := createAssetViaAPI(t, server.URL)
assetB := createAssetViaAPI(t, server.URL)
insertOpenInstallation(t, db, assetA.ID, component.ID, "DRV#1")
body, status := postJSONRaw(t, server.URL+"/failures", map[string]any{
"component_serial": component.VendorSerial,
"server_serial": assetB.VendorSerial,
"failure_date": "2026-02-23",
"description": "mismatch",
})
if status != http.StatusConflict {
t.Fatalf("expected 409, got %d body=%s", status, string(body))
}
}
func TestUIFailuresPageRendersActiveAndChronologySections(t *testing.T) {
db := openTestDB(t)
defer db.Close()
historySvc := history.NewService(db)
mux := http.NewServeMux()
assetRepo := registry.NewAssetRepository(db)
componentRepo := registry.NewComponentRepository(db)
installRepo := registry.NewInstallationRepository(db)
failureRepo := failures.NewFailureRepository(db)
RegisterUIRoutes(mux, UIDependencies{
Assets: assetRepo,
Components: componentRepo,
Installations: installRepo,
Failures: failureRepo,
})
RegisterFailureRoutes(mux, FailureDependencies{
Failures: failureRepo,
Assets: assetRepo,
Components: componentRepo,
Installations: installRepo,
History: historySvc,
})
server := httptest.NewServer(mux)
defer server.Close()
resp, err := http.Get(server.URL + "/ui/failures")
if err != nil {
t.Fatalf("GET /ui/failures: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
body := string(mustReadAll(t, resp))
for _, needle := range []string{"Active Failures", "Failure Chronology", "Register Failure"} {
if !strings.Contains(body, needle) {
t.Fatalf("expected page to contain %q", needle)
}
}
}
func createAssetViaAPI(t *testing.T, baseURL string) domain.Asset {
t.Helper()
payload := map[string]any{
"name": "srv-" + time.Now().UTC().Format("150405.000000000"),
"vendor_serial": "SRV-" + time.Now().UTC().Format("150405.000000000"),
}
respBody := postJSONWithResponse(t, baseURL+"/assets", payload, http.StatusCreated)
var out domain.Asset
if err := json.Unmarshal(respBody, &out); err != nil {
t.Fatalf("decode asset: %v", err)
}
return out
}
func insertOpenInstallation(t *testing.T, db *sql.DB, assetID, componentID, slot string) {
t.Helper()
id := "INS" + time.Now().UTC().Format("150405999999")
if len(id) > 16 {
id = id[:16]
}
if _, err := db.Exec(
`INSERT INTO installations (id, machine_id, part_id, slot_name, installed_at) VALUES (?, ?, ?, ?, ?)`,
id, assetID, componentID, slot, time.Now().UTC().Add(-1*time.Hour),
); err != nil {
t.Fatalf("insert installation: %v", err)
}
}
func postJSONRaw(t *testing.T, url string, payload any) ([]byte, int) {
t.Helper()
body, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal payload: %v", err)
}
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("post %s: %v", url, err)
}
defer resp.Body.Close()
var buf bytes.Buffer
if _, err := buf.ReadFrom(resp.Body); err != nil {
t.Fatalf("read body: %v", err)
}
return buf.Bytes(), resp.StatusCode
}