Files
story/internal/alerts/detection_integration_test.go
T

105 lines
4.4 KiB
Go
Raw Normal View History

2026-08-26 06:41:07 +08:00
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")
}
}