115 lines
3.6 KiB
Go
115 lines
3.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/company/ai-ops/internal/config"
|
|
"github.com/company/ai-ops/internal/database"
|
|
)
|
|
|
|
func setupServicePGIntegration(t *testing.T) context.Context {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
if database.Pool == nil {
|
|
ports := []int{15432, 5432}
|
|
var lastErr error
|
|
for _, port := range ports {
|
|
lastErr = database.Init(config.DatabaseConfig{Host: "localhost", Port: port, User: "aiops", Password: "aiops123", DBName: "ai_ops", SSLMode: "disable", PoolSize: 4})
|
|
if lastErr == nil {
|
|
break
|
|
}
|
|
database.Close()
|
|
database.Pool = nil
|
|
}
|
|
if lastErr != nil {
|
|
t.Skipf("PostgreSQL integration database not available: %v", lastErr)
|
|
}
|
|
}
|
|
files, err := filepath.Glob(filepath.Join("..", "..", "tech", "migrations", "*.up.sql"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sort.Strings(files)
|
|
if _, err := database.Pool.Exec(ctx, `SELECT pg_advisory_lock(424242001)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer database.Pool.Exec(ctx, `SELECT pg_advisory_unlock(424242001)`)
|
|
for _, f := range files {
|
|
b, err := os.ReadFile(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := database.Pool.Exec(ctx, string(b)); err != nil {
|
|
t.Fatalf("apply migration %s: %v", f, err)
|
|
}
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
func serviceTestUUID(t *testing.T) string {
|
|
t.Helper()
|
|
b := make([]byte, 16)
|
|
if _, err := rand.Read(b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b[6] = (b[6] & 0x0f) | 0x40
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
return hex.EncodeToString(b[0:4]) + "-" + hex.EncodeToString(b[4:6]) + "-" + hex.EncodeToString(b[6:8]) + "-" + hex.EncodeToString(b[8:10]) + "-" + hex.EncodeToString(b[10:16])
|
|
}
|
|
|
|
func cleanupAudit(t *testing.T, ctx context.Context, ids ...string) {
|
|
t.Helper()
|
|
for _, id := range ids {
|
|
_, _ = database.Pool.Exec(ctx, `DELETE FROM ai_ops_audits WHERE id=$1 OR parent_audit_id=$1 OR object_id=$1`, id)
|
|
}
|
|
}
|
|
|
|
func TestAuditServiceRecordListRollback(t *testing.T) {
|
|
ctx := setupServicePGIntegration(t)
|
|
svc := NewAuditService()
|
|
id := serviceTestUUID(t)
|
|
defer cleanupAudit(t, ctx, id)
|
|
|
|
log := &AuditLog{ID: id, TenantID: "tenant", ObjectType: "rule", ObjectID: id, Action: "update", BeforeState: map[string]any{"enabled": false}, AfterState: map[string]any{"enabled": true}, RequestID: "req", ResultCode: "SUCCESS", SourceIP: "127.0.0.1", ActorID: "actor", RiskLevel: "normal"}
|
|
if err := svc.Record(ctx, log); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
logs, total, err := svc.List(ctx, "rule", id, 0, 500)
|
|
if err != nil || total != 1 || len(logs) != 1 || logs[0].ID != id {
|
|
t.Fatalf("list = total=%d logs=%+v err=%v", total, logs, err)
|
|
}
|
|
rollback, err := svc.Rollback(ctx, id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rollback.Action != "rollback" || rollback.ParentAuditID == nil || *rollback.ParentAuditID != id || rollback.RiskLevel != "high" {
|
|
t.Fatalf("rollback = %+v", rollback)
|
|
}
|
|
cleanupAudit(t, ctx, rollback.ID)
|
|
}
|
|
|
|
func TestAuditServiceRollbackRejectsMissingBeforeState(t *testing.T) {
|
|
ctx := setupServicePGIntegration(t)
|
|
svc := NewAuditService()
|
|
id := serviceTestUUID(t)
|
|
defer cleanupAudit(t, ctx, id)
|
|
|
|
log := &AuditLog{ID: id, TenantID: "tenant", ObjectType: "rule", ObjectID: id, Action: "create", AfterState: map[string]any{"enabled": true}, RequestID: "req", ResultCode: "SUCCESS", SourceIP: "127.0.0.1", ActorID: "actor", RiskLevel: "normal", CreatedAt: time.Now()}
|
|
if err := svc.Record(ctx, log); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := svc.Rollback(ctx, id); err == nil {
|
|
t.Fatal("expected rollback error without before state")
|
|
}
|
|
if _, err := svc.Rollback(ctx, serviceTestUUID(t)); err == nil {
|
|
t.Fatal("expected missing audit error")
|
|
}
|
|
}
|