Files
ITWorx-Pulse-Public/internal/alertcontrol/repository_test.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

122 lines
4.1 KiB
Go

package alertcontrol
import (
"context"
"os"
"sync"
"testing"
"time"
"github.com/itworx/pulse/internal/database"
)
func TestPostgreSQLControlsAreExpiringAuditedAndIdempotent(t *testing.T) {
dsn := os.Getenv("PULSE_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("PULSE_TEST_DATABASE_URL is not set")
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
pool, err := database.NewPool(ctx, database.Config{URL: dsn, MaxConns: 8, MinConns: 1})
if err != nil {
t.Fatal(err)
}
defer pool.Close()
if err := database.Migrate(ctx, pool); err != nil {
t.Fatal(err)
}
repo := Repository{Pool: pool}
now := time.Now().UTC().Truncate(time.Microsecond)
silenceID, maintenanceID, expiringID := NewID(), NewID(), NewID()
t.Cleanup(func() {
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cleanupCancel()
_, _ = pool.Exec(cleanupCtx, `DELETE FROM alert_silences WHERE id = ANY($1::uuid[])`, []string{silenceID, expiringID})
_, _ = pool.Exec(cleanupCtx, `DELETE FROM maintenance_windows WHERE id = $1::uuid`, maintenanceID)
})
silence, err := repo.CreateSilence(ctx, "operator-1", Silence{ID: silenceID, Name: "planned deploy", Reason: "change window", Owner: "operator-1", Matchers: Matcher{Severities: []string{"critical"}}, StartsAt: now.Add(-time.Minute), ExpiresAt: now.Add(time.Hour)})
if err != nil {
t.Fatal(err)
}
if silence.State != StateActive || silence.Revision != 1 {
t.Fatalf("unexpected silence: %#v", silence)
}
window, err := repo.CreateMaintenance(ctx, "operator-1", MaintenanceWindow{ID: maintenanceID, Name: "maintenance", Reason: "firmware", Selector: Matcher{EntityTypes: []string{"host"}}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(time.Hour)})
if err != nil {
t.Fatal(err)
}
if window.State != StateActive {
t.Fatalf("maintenance state = %s", window.State)
}
items, err := repo.ListSilences(ctx, 100, now)
if err != nil || len(items) != 1 {
t.Fatalf("list silences: %d, %v", len(items), err)
}
expiring, err := repo.CreateSilence(ctx, "operator-1", Silence{ID: expiringID, Name: "short", Reason: "test expiry", Owner: "operator-1", Matchers: Matcher{}, StartsAt: now.Add(-time.Minute), ExpiresAt: now.Add(-time.Second)})
if err != nil {
t.Fatal(err)
}
if expiring.State != StateExpired {
t.Fatalf("expired control before job = %s", expiring.State)
}
first, err := repo.Expire(ctx, now)
if err != nil {
t.Fatal(err)
}
second, err := repo.Expire(ctx, now)
if err != nil {
t.Fatal(err)
}
if first.Silences != 1 || first.MaintenanceWindows != 0 || second != (ExpiryResult{}) {
t.Fatalf("expiry not idempotent: first=%#v second=%#v", first, second)
}
concurrentID := NewID()
defer func() {
_, _ = pool.Exec(context.Background(), `DELETE FROM alert_silences WHERE id=$1::uuid`, concurrentID)
}()
if _, err := repo.CreateSilence(ctx, "operator-1", Silence{ID: concurrentID, Name: "concurrent", Reason: "test", Owner: "operator-1", Matchers: Matcher{}, StartsAt: now.Add(-time.Minute), ExpiresAt: now.Add(-time.Second)}); err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
results := make(chan ExpiryResult, 2)
errorsCh := make(chan error, 2)
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
result, err := repo.Expire(ctx, now.Add(time.Second))
if err != nil {
errorsCh <- err
return
}
results <- result
}()
}
wg.Wait()
close(results)
close(errorsCh)
total := 0
for result := range results {
total += result.Silences + result.MaintenanceWindows
}
for err := range errorsCh {
t.Fatal(err)
}
if total != 1 {
t.Fatalf("concurrent expiry count = %d, want 1", total)
}
if _, err := repo.CreateSilence(ctx, "operator-1", Silence{ID: silenceID, Name: "duplicate", Reason: "duplicate", Owner: "operator-1", Matchers: Matcher{}, StartsAt: now, ExpiresAt: now.Add(time.Hour)}); err == nil {
t.Fatal("duplicate silence should fail")
}
if _, err := repo.RevokeSilence(ctx, silenceID, "operator-1", silence.Revision, now); err != nil {
t.Fatal(err)
}
if _, err := repo.RevokeSilence(ctx, silenceID, "operator-1", silence.Revision+1, now); err == nil {
t.Fatal("repeated revoke should conflict")
}
}