129 lines
4.9 KiB
Go
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()
|
|
}
|