239 lines
7.6 KiB
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
|
|
}
|