105 lines
4.4 KiB
Go
105 lines
4.4 KiB
Go
package alerts
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
func TestScannerDetectionDedupSilenceAndReopenLifecycle(t *testing.T) {
|
|
databaseURL := os.Getenv("TEST_DATABASE_URL")
|
|
if databaseURL == "" {
|
|
t.Skip("TEST_DATABASE_URL is not set")
|
|
}
|
|
ctx := context.Background()
|
|
pool, err := pgxpool.New(ctx, databaseURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer pool.Close()
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
scanner, err := NewScanner(pool, logger, time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := NewScanner(nil, logger, time.Second); err == nil {
|
|
t.Fatal("nil scanner pool accepted")
|
|
}
|
|
if _, err := NewScanner(pool, nil, time.Second); err == nil {
|
|
t.Fatal("nil scanner logger accepted")
|
|
}
|
|
if _, err := NewScanner(pool, logger, 0); err == nil {
|
|
t.Fatal("zero scanner interval accepted")
|
|
}
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
tenantID := "10000000-0000-0000-0000-000000000001"
|
|
resourceID := uuid.NewString()
|
|
fingerprint := "core-detect-" + uuid.NewString()
|
|
silenceID := uuid.NewString()
|
|
defer func() {
|
|
_, _ = pool.Exec(ctx, `DELETE FROM alert_incident WHERE tenant_id=$1::uuid AND fingerprint=$2`, tenantID, fingerprint)
|
|
_, _ = pool.Exec(ctx, `DELETE FROM alert_silence WHERE id=$1::uuid`, silenceID)
|
|
}()
|
|
if err := scanner.detect(ctx, Signal{TenantID: tenantID, RuleCode: "bad code", Category: "TECHNICAL", Severity: "LOW", Fingerprint: fingerprint, Title: "标题", Summary: "摘要"}, now); !errors.Is(err, ErrInvalid) {
|
|
t.Fatalf("invalid signal error=%v", err)
|
|
}
|
|
signal := Signal{
|
|
TenantID: tenantID, RuleCode: " core_detect_test ", Category: "TECHNICAL", Severity: "LOW",
|
|
Fingerprint: fingerprint, Title: "核心告警发现", Summary: "首次发现合成告警。",
|
|
ResourceType: "device", ResourcePublicID: resourceID, RequestID: "alerts-core-detect",
|
|
}
|
|
if err := scanner.detect(ctx, signal, now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signal.Summary = "相同指纹再次命中。"
|
|
if err := scanner.detect(ctx, signal, now.Add(time.Minute)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var status, summary string
|
|
var occurrences, rowVersion int64
|
|
if err := pool.QueryRow(ctx, `SELECT status,summary,occurrence_count,row_version FROM alert_incident WHERE tenant_id=$1::uuid AND fingerprint=$2`, tenantID, fingerprint).Scan(&status, &summary, &occurrences, &rowVersion); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status != "OPEN" || summary != signal.Summary || occurrences != 2 || rowVersion != 2 {
|
|
t.Fatalf("open status=%s summary=%q occurrences=%d version=%d", status, summary, occurrences, rowVersion)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO alert_silence(id,public_id,tenant_id,rule_code,category,resource_type,resource_public_id,matcher,reason,starts_at,ends_at,created_by)
|
|
VALUES($1::uuid,$2::uuid,$3::uuid,'CORE_DETECT_TEST','TECHNICAL','device',$4::uuid,'{}'::jsonb,'核心检测静默窗口',$5,$6,'10000000-0000-0000-0000-000000000101'::uuid)`,
|
|
silenceID, uuid.NewString(), tenantID, resourceID, now.Add(-time.Minute), now.Add(time.Hour)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signal.Summary = "静默窗口内再次命中。"
|
|
if err := scanner.detect(ctx, signal, now.Add(2*time.Minute)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `UPDATE alert_silence SET revoked_at=$2,revoked_by='10000000-0000-0000-0000-000000000101'::uuid WHERE id=$1::uuid`, silenceID, now.Add(3*time.Minute)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signal.Summary = "静默撤销后重新进入队列。"
|
|
if err := scanner.detect(ctx, signal, now.Add(4*time.Minute)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var actionCount int
|
|
if err := pool.QueryRow(ctx, `SELECT ai.status,ai.summary,ai.occurrence_count,ai.row_version,count(aa.id) FROM alert_incident ai LEFT JOIN alert_action aa ON aa.incident_id=ai.id WHERE ai.tenant_id=$1::uuid AND ai.fingerprint=$2 GROUP BY ai.id`, tenantID, fingerprint).Scan(&status, &summary, &occurrences, &rowVersion, &actionCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status != "OPEN" || summary != signal.Summary || occurrences != 4 || rowVersion != 4 || actionCount != 3 {
|
|
t.Fatalf("reopened status=%s summary=%q occurrences=%d version=%d actions=%d", status, summary, occurrences, rowVersion, actionCount)
|
|
}
|
|
|
|
cancelled, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
scanner.Run(cancelled)
|
|
if nullUUID("") != nil || nullUUID(resourceID) != resourceID {
|
|
t.Fatal("null UUID normalization failed")
|
|
}
|
|
}
|