test(maintenance): add policy executor and handler tests

Табличные тесты PolicyExecutor, ConfigProvider reload, memory CRUD политик и 503 для /v1/maintenance/* на memory-бэкенде без PostgreSQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Denozordec
2026-06-12 13:32:01 +07:00
co-authored by Cursor
parent d38ee68c4e
commit 1ccffc85da
4 changed files with 346 additions and 0 deletions
@@ -0,0 +1,46 @@
package httpapi
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestMaintenancePoliciesMemoryBackend503(t *testing.T) {
srv, err := New(Options{SeedDemo: true, InsecureDev: true})
if err != nil {
t.Fatal(err)
}
defer srv.Close()
handler := srv.Handler()
tests := []struct {
method string
path string
body string
}{
{http.MethodGet, "/v1/maintenance/policies", ""},
{http.MethodPost, "/v1/maintenance/policies", `{"name":"x","table_name":"job_audit","schedule":"0 3 * * *"}`},
{http.MethodPost, "/v1/maintenance/run", `{"policy_id":"00000000-0000-0000-0000-000000000001"}`},
{http.MethodGet, "/v1/maintenance/config-audit", ""},
}
for _, tc := range tests {
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
var req *http.Request
if tc.body != "" {
req = httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
req.Header.Set("Content-Type", "application/json")
} else {
req = httptest.NewRequest(tc.method, tc.path, nil)
}
req.Header.Set("Authorization", "Bearer dev")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
})
}
}
@@ -0,0 +1,43 @@
package maintenance
import (
"context"
"testing"
"evobgp/internal/store"
)
func TestConfigProviderReloadAndSnapshot(t *testing.T) {
mem := store.NewMemory()
ret := 3600
if _, err := mem.CreateMaintenancePolicy(&store.MaintenancePolicy{
Name: "p1",
TableName: "job_audit",
Schedule: "0 3 * * *",
RetentionPeriodSec: &ret,
Enabled: true,
}); err != nil {
t.Fatal(err)
}
cp := NewConfigProvider(mem)
if err := cp.Reload(context.Background()); err != nil {
t.Fatal(err)
}
snap := cp.Snapshot()
if len(snap) != 1 || snap[0].Name != "p1" {
t.Fatalf("snapshot: %+v", snap)
}
newName := "p1-updated"
if _, err := mem.UpdateMaintenancePolicy(snap[0].ID, &store.MaintenancePolicyPatch{Name: &newName}); err != nil {
t.Fatal(err)
}
if err := cp.Reload(context.Background()); err != nil {
t.Fatal(err)
}
snap2 := cp.Snapshot()
if len(snap2) != 1 || snap2[0].Name != newName {
t.Fatalf("after reload: %+v", snap2)
}
}
@@ -0,0 +1,161 @@
package maintenance
import (
"context"
"errors"
"strings"
"testing"
"evobgp/internal/store"
)
func validPolicy() *store.MaintenancePolicy {
ret := 86400
return &store.MaintenancePolicy{
ID: "11111111-1111-1111-1111-111111111111",
Name: "job audit",
TableName: "job_audit",
Condition: "true",
RetentionPeriodSec: &ret,
VacuumStrategy: store.VacuumStrategyNone,
Schedule: "0 3 * * *",
Enabled: true,
}
}
func TestPolicyExecutorExecuteValidation(t *testing.T) {
ctx := context.Background()
mem := store.NewMemory()
base := validPolicy()
tests := []struct {
name string
exec *PolicyExecutor
policy *store.MaintenancePolicy
wantErr error
contains string
}{
{
name: "nil policy",
exec: &PolicyExecutor{Store: mem},
policy: nil,
wantErr: store.ErrInvalidInput,
},
{
name: "nil pool",
exec: &PolicyExecutor{Store: mem, Pool: nil},
policy: base,
contains: "postgres not configured",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := tc.exec.Execute(ctx, tc.policy, false)
if tc.wantErr != nil {
if !errors.Is(err, tc.wantErr) {
t.Fatalf("Execute() err=%v want %v", err, tc.wantErr)
}
return
}
if err == nil {
t.Fatal("Execute() expected error")
}
if tc.contains != "" && !strings.Contains(err.Error(), tc.contains) {
t.Fatalf("Execute() err=%q want substring %q", err, tc.contains)
}
})
}
}
func TestPolicyExecutorPreExecuteValidation(t *testing.T) {
p := validPolicy()
p.TableName = "tenant"
if err := ValidateTableName(p.TableName); err == nil {
t.Fatal("expected blocked table error")
}
p = validPolicy()
p.Condition = "1=1; DROP TABLE job_audit"
if err := ValidateCondition(p.Condition); err == nil {
t.Fatal("expected unsafe condition error")
}
p = validPolicy()
p.VacuumStrategy = "invalid"
if !store.ValidVacuumStrategy(p.VacuumStrategy) {
return
}
t.Fatal("expected invalid vacuum strategy")
}
func TestPolicyAction(t *testing.T) {
ret := 3600
tests := []struct {
name string
p *store.MaintenancePolicy
want string
}{
{"nil", nil, "run"},
{"cleanup only", &store.MaintenancePolicy{RetentionPeriodSec: &ret, VacuumStrategy: store.VacuumStrategyNone}, "cleanup"},
{"vacuum only", &store.MaintenancePolicy{VacuumStrategy: store.VacuumStrategyVacuum}, "vacuum"},
{"cleanup+vacuum", &store.MaintenancePolicy{MaxRows: &ret, VacuumStrategy: store.VacuumStrategyAnalyze}, "cleanup_vacuum"},
{"noop run", &store.MaintenancePolicy{VacuumStrategy: store.VacuumStrategyNone}, "run"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := policyAction(tc.p); got != tc.want {
t.Fatalf("policyAction()=%q want %q", got, tc.want)
}
})
}
}
func TestVacuumKind(t *testing.T) {
tests := []struct {
strategy string
want string
}{
{store.VacuumStrategyVacuum, "vacuum"},
{store.VacuumStrategyAnalyze, "analyze"},
{store.VacuumStrategyVacuumAnalyze, "vacuum_analyze"},
{store.VacuumStrategyReindex, "reindex"},
{"unknown", "vacuum"},
}
for _, tc := range tests {
if got := vacuumKind(tc.strategy); got != tc.want {
t.Fatalf("vacuumKind(%q)=%q want %q", tc.strategy, got, tc.want)
}
}
}
func TestRowsDeletedFromDetail(t *testing.T) {
tests := []struct {
name string
detail map[string]any
want int64
}{
{"nil", nil, 0},
{"int64", map[string]any{"deleted": int64(42)}, 42},
{"int", map[string]any{"deleted": 7}, 7},
{"float64", map[string]any{"deleted": float64(3)}, 3},
{"missing", map[string]any{"other": 1}, 0},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := rowsDeletedFromDetail(tc.detail); got != tc.want {
t.Fatalf("rowsDeletedFromDetail()=%d want %d", got, tc.want)
}
})
}
}
func TestAdvisoryKeyStable(t *testing.T) {
a := advisoryKey("policy-a")
b := advisoryKey("policy-a")
c := advisoryKey("policy-b")
if a != b {
t.Fatal("advisory key not stable for same id")
}
if a == c {
t.Fatal("advisory key collision for different ids")
}
}
@@ -0,0 +1,96 @@
package store
import "testing"
func TestMemoryMaintenancePolicyCRUD(t *testing.T) {
m := NewMemory()
ret := int(86400)
max := 5000
created, err := m.CreateMaintenancePolicy(&MaintenancePolicy{
Name: "audit cleanup",
TableName: "job_audit",
Condition: "status = 'succeeded'",
RetentionPeriodSec: &ret,
MaxRows: &max,
VacuumStrategy: VacuumStrategyVacuumAnalyze,
Schedule: "0 4 * * *",
Enabled: true,
DryRunEnabled: true,
})
if err != nil {
t.Fatal(err)
}
if created.ID == "" {
t.Fatal("missing id")
}
items, _, hasMore, err := m.ListMaintenancePolicies("", 10)
if err != nil {
t.Fatal(err)
}
if len(items) != 1 || hasMore {
t.Fatalf("list: len=%d hasMore=%v", len(items), hasMore)
}
got, err := m.GetMaintenancePolicy(created.ID)
if err != nil {
t.Fatal(err)
}
if got.Name != "audit cleanup" || got.VacuumStrategy != VacuumStrategyVacuumAnalyze {
t.Fatalf("get: %+v", got)
}
newName := "renamed"
disabled := false
updated, err := m.UpdateMaintenancePolicy(created.ID, &MaintenancePolicyPatch{
Name: &newName,
Enabled: &disabled,
})
if err != nil {
t.Fatal(err)
}
if updated.Name != newName || updated.Enabled {
t.Fatalf("update: %+v", updated)
}
if err := m.TouchMaintenancePolicyRun(created.ID, "succeeded", ""); err != nil {
t.Fatal(err)
}
afterTouch, err := m.GetMaintenancePolicy(created.ID)
if err != nil {
t.Fatal(err)
}
if afterTouch.LastStatus != "succeeded" || afterTouch.LastRunAt == nil {
t.Fatalf("touch: %+v", afterTouch)
}
if err := m.AppendMaintenancePolicyConfigAudit("op:test", created.ID, "update", map[string]any{"name": "old"}, map[string]any{"name": newName}); err != nil {
t.Fatal(err)
}
audit, next, hasMore, err := m.ListMaintenancePolicyConfigAudit("", 10)
if err != nil {
t.Fatal(err)
}
if len(audit) != 1 || audit[0].Action != "update" || next != "" || hasMore {
t.Fatalf("audit: %+v next=%q hasMore=%v", audit, next, hasMore)
}
if err := m.DeleteMaintenancePolicy(created.ID); err != nil {
t.Fatal(err)
}
if _, err := m.GetMaintenancePolicy(created.ID); err != ErrNotFound {
t.Fatalf("after delete: %v", err)
}
}
func TestCreateMaintenancePolicyInvalid(t *testing.T) {
m := NewMemory()
_, err := m.CreateMaintenancePolicy(&MaintenancePolicy{Name: "", TableName: "job_audit", Schedule: "0 3 * * *"})
if err != ErrInvalidInput {
t.Fatalf("want ErrInvalidInput got %v", err)
}
_, err = m.CreateMaintenancePolicy(&MaintenancePolicy{Name: "x", TableName: "job_audit", Schedule: "0 3 * * *", VacuumStrategy: "bad"})
if err != ErrInvalidInput {
t.Fatalf("want ErrInvalidInput got %v", err)
}
}