Files
ai-ops/internal/service/healing_engine_test.go
2026-05-12 17:48:22 +08:00

129 lines
4.9 KiB
Go

package service
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/company/ai-ops/internal/domain/model"
)
type fakeHealingRepo struct {
created []HealingLog
updated []HealingLog
}
func (r *fakeHealingRepo) CreateHealing(ctx context.Context, h *HealingLog) error {
r.created = append(r.created, *h)
return nil
}
func (r *fakeHealingRepo) UpdateHealingStatus(ctx context.Context, id, status string, result map[string]any, errCode string) error {
r.updated = append(r.updated, HealingLog{ID: id, Status: status, ResultDetail: result, ErrorCode: errCode})
return nil
}
func TestHealingEngineExecutesConfiguredEndpointAndRecordsSuccess(t *testing.T) {
called := false
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
if r.Method != http.MethodPost {
t.Fatalf("method = %s, want POST", r.Method)
}
w.WriteHeader(http.StatusAccepted)
}))
defer server.Close()
action := "switch_route"
alertRepo := &fakeAggregationAlertRepo{rules: []model.AlertRule{{
ID: "rule-1",
HealingAction: &action,
HealingConfig: map[string]any{"endpoint": server.URL},
IsSandboxed: false,
}}}
healingRepo := &fakeHealingRepo{}
engine := NewHealingEngine(alertRepo, healingRepo)
err := engine.handleEvent(context.Background(), &model.AlertEvent{ID: "alert-1", RuleID: "rule-1"})
if err != nil {
t.Fatalf("handle event: %v", err)
}
if !called {
t.Fatalf("expected healing endpoint to be called")
}
if len(healingRepo.updated) != 1 || healingRepo.updated[0].Status != "succeeded" {
t.Fatalf("updated healing logs = %#v, want one succeeded", healingRepo.updated)
}
}
func TestHealingEngineRejectsRestartWithoutExplicitAllow(t *testing.T) {
healing := &HealingLog{ActionType: "restart_instance", Config: map[string]any{"endpoint": "http://127.0.0.1"}}
engine := NewHealingEngine(nil, nil)
_, err := engine.executeAction(context.Background(), healing)
if err == nil {
t.Fatalf("expected restart_instance without allow_restart to fail")
}
}
func TestHealingEngineProcessDryRunAndActionBranches(t *testing.T) {
action := "throttle"
alertRepo := &fakeAggregationAlertRepo{rules: []model.AlertRule{{ID: "rule-heal", HealingAction: &action, HealingConfig: map[string]any{"limit": 1}, IsSandboxed: true}}}
alertRepo.createdEvents = nil
healingRepo := &fakeHealingRepo{}
engine := NewHealingEngine(alertRepo, healingRepo)
alertRepo.rules[0].ID = "rule-heal"
// fakeAggregationAlertRepo ListEvents returns nil, so cover direct handleEvent dry-run.
if err := engine.handleEvent(context.Background(), &model.AlertEvent{ID: "event-heal", RuleID: "rule-heal"}); err != nil {
t.Fatal(err)
}
if len(healingRepo.created) != 1 || len(healingRepo.updated) != 1 || healingRepo.updated[0].Status != "succeeded" {
t.Fatalf("dry-run healing logs = created=%+v updated=%+v", healingRepo.created, healingRepo.updated)
}
if _, err := engine.executeAction(context.Background(), &HealingLog{ActionType: "unsupported", Config: map[string]any{}}); err == nil {
t.Fatal("expected unsupported action error")
}
if _, err := engine.executeInvokeScript(context.Background(), &HealingLog{ActionType: "invoke_script", Config: map[string]any{"endpoint": "http://example.invalid"}}); err == nil {
t.Fatal("expected missing script_id error")
}
if generateHealingID() == "" {
t.Fatal("empty healing id")
}
}
func TestHealingEngineEndpointVariants(t *testing.T) {
var gotAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
w.WriteHeader(http.StatusAccepted)
}))
defer server.Close()
engine := NewHealingEngine(&fakeAggregationAlertRepo{}, &fakeHealingRepo{})
if _, err := engine.executeThrottle(context.Background(), &HealingLog{ID: "h", AlertID: "a", ActionType: "throttle", Config: map[string]any{"endpoint": server.URL, "method": http.MethodPatch, "token": "tok"}}); err != nil {
t.Fatal(err)
}
if gotAuth != "Bearer tok" {
t.Fatalf("auth header = %s", gotAuth)
}
if _, err := engine.executeRestartInstance(context.Background(), &HealingLog{ID: "h", AlertID: "a", ActionType: "restart_instance", Config: map[string]any{"endpoint": server.URL, "allow_restart": true}}); err != nil {
t.Fatal(err)
}
if _, err := engine.executeInvokeScript(context.Background(), &HealingLog{ID: "h", AlertID: "a", ActionType: "invoke_script", Config: map[string]any{"endpoint": server.URL, "script_id": "script-1"}}); err != nil {
t.Fatal(err)
}
if _, err := engine.callConfiguredEndpoint(context.Background(), &HealingLog{Config: map[string]any{"endpoint": server.URL, "method": http.MethodGet}}, "bad"); err == nil {
t.Fatal("expected disallowed method error")
}
}
func TestHealingEngineStartStopAndProcess(t *testing.T) {
engine := NewHealingEngine(&fakeAggregationAlertRepo{}, &fakeHealingRepo{})
engine.interval = time.Hour
engine.process(context.Background())
engine.Start()
time.Sleep(5 * time.Millisecond)
engine.Stop()
}