170 lines
9.0 KiB
Go
170 lines
9.0 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"os"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/alerts"
|
||
|
|
"data-terminal-cloud/internal/audit"
|
||
|
|
"data-terminal-cloud/internal/concurrency"
|
||
|
|
"data-terminal-cloud/internal/identity"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestAlertHandlersWriteAndReadLifecycle(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()
|
||
|
|
service, err := alerts.NewService(pool, audit.NewRepository())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
principal := identity.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000101"}
|
||
|
|
tenantID := "10000000-0000-0000-0000-000000000010"
|
||
|
|
routeKey := "http-alert-route-" + uuid.NewString()
|
||
|
|
deliveryTestKey := "http-alert-delivery-test-" + uuid.NewString()
|
||
|
|
silenceKey := "http-alert-silence-" + uuid.NewString()
|
||
|
|
incidentID, incidentPublicID := uuid.NewString(), uuid.NewString()
|
||
|
|
createdRouteID := "00000000-0000-0000-0000-000000000000"
|
||
|
|
defer func() {
|
||
|
|
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE request_id LIKE 'http-alert-%'`)
|
||
|
|
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE action='alert.delivery_test.create' AND purpose='HTTP联通测试'`)
|
||
|
|
_, _ = pool.Exec(ctx, `DELETE FROM idempotency_record WHERE idempotency_key=ANY($1::text[])`, []string{routeKey, deliveryTestKey, silenceKey})
|
||
|
|
_, _ = pool.Exec(ctx, `DELETE FROM outbox_event WHERE event_type='alert.webhook_test_requested' AND payload->>'routing_policy_id'=$1`, createdRouteID)
|
||
|
|
_, _ = pool.Exec(ctx, `DELETE FROM alert_incident WHERE id=$1::uuid`, incidentID)
|
||
|
|
_, _ = pool.Exec(ctx, `DELETE FROM alert_silence WHERE reason='HTTP集成维护窗口'`)
|
||
|
|
_, _ = pool.Exec(ctx, `DELETE FROM alert_routing_policy WHERE category='BUSINESS' AND severity='LOW'`)
|
||
|
|
}()
|
||
|
|
|
||
|
|
request := authenticatedRequest(http.MethodPost, "/api/v1/ops/alert-routing-policies", `{}`, principal)
|
||
|
|
recorder := httptest.NewRecorder()
|
||
|
|
CreateOpsAlertRoutingPolicyHandler(service).ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusBadRequest {
|
||
|
|
t.Fatalf("missing ops headers status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
|
|
}
|
||
|
|
routeBody := `{"category":"BUSINESS","severity":"LOW","receiver_group":"http-oncall","response_minutes":30,"escalation_after_minutes":60,"escalation_receiver_group":"http-command","runbook_ref":"runbook://http/alert","change_summary":"HTTP告警路由"}`
|
||
|
|
request = authenticatedRequest(http.MethodPost, "/api/v1/ops/alert-routing-policies", routeBody, principal)
|
||
|
|
request.Header.Set("X-Tenant-Id", tenantID)
|
||
|
|
request.Header.Set("Idempotency-Key", routeKey)
|
||
|
|
recorder = httptest.NewRecorder()
|
||
|
|
CreateOpsAlertRoutingPolicyHandler(service).ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusCreated || recorder.Header().Get("ETag") != concurrency.FormatETag(1) {
|
||
|
|
t.Fatalf("create route status=%d headers=%v body=%s", recorder.Code, recorder.Header(), recorder.Body.String())
|
||
|
|
}
|
||
|
|
var routeResponse struct {
|
||
|
|
Data alerts.RoutingPolicy `json:"data"`
|
||
|
|
}
|
||
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &routeResponse); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
createdRouteID = routeResponse.Data.ID
|
||
|
|
request = authenticatedRequest(http.MethodGet, "/api/v1/ops/alert-routing-policies", "", principal)
|
||
|
|
request.Header.Set("X-Tenant-Id", tenantID)
|
||
|
|
recorder = httptest.NewRecorder()
|
||
|
|
ListOpsAlertRoutingPoliciesHandler(service).ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusOK || recorder.Header().Get("Cache-Control") != "no-store" {
|
||
|
|
t.Fatalf("list routes status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
|
|
}
|
||
|
|
request = authenticatedRequest(http.MethodPost, "/api/v1/ops/alert-routing-policies/"+routeResponse.Data.ID+"/delivery-tests", `{"reason":"HTTP联通测试"}`, principal)
|
||
|
|
request.SetPathValue("id", routeResponse.Data.ID)
|
||
|
|
request.Header.Set("X-Tenant-Id", tenantID)
|
||
|
|
request.Header.Set("Idempotency-Key", deliveryTestKey)
|
||
|
|
recorder = httptest.NewRecorder()
|
||
|
|
CreateOpsAlertDeliveryTestHandler(service).ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusCreated || !strings.Contains(recorder.Body.String(), `"status":"PENDING"`) {
|
||
|
|
t.Fatalf("create delivery test status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
|
|
}
|
||
|
|
request = authenticatedRequest(http.MethodGet, "/api/v1/ops/alert-delivery-tests", "", principal)
|
||
|
|
request.Header.Set("X-Tenant-Id", tenantID)
|
||
|
|
recorder = httptest.NewRecorder()
|
||
|
|
ListOpsAlertDeliveryTestsHandler(service).ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), routeResponse.Data.ID) {
|
||
|
|
t.Fatalf("list delivery tests status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
|
|
}
|
||
|
|
|
||
|
|
startsAt, endsAt := time.Now().UTC().Add(-time.Minute), time.Now().UTC().Add(time.Hour)
|
||
|
|
silenceBody, _ := json.Marshal(map[string]any{"rule_code": "HTTP_INTEGRATION_ALERT", "matcher": map[string]any{"source": "http"}, "reason": "HTTP集成维护窗口", "starts_at": startsAt, "ends_at": endsAt})
|
||
|
|
request = authenticatedRequest(http.MethodPost, "/api/v1/ops/alert-silences", string(silenceBody), principal)
|
||
|
|
request.Header.Set("X-Tenant-Id", tenantID)
|
||
|
|
request.Header.Set("Idempotency-Key", silenceKey)
|
||
|
|
recorder = httptest.NewRecorder()
|
||
|
|
CreateOpsAlertSilenceHandler(service).ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusCreated {
|
||
|
|
t.Fatalf("create silence status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
|
|
}
|
||
|
|
var silenceResponse struct {
|
||
|
|
Data alerts.Silence `json:"data"`
|
||
|
|
}
|
||
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &silenceResponse); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
request = authenticatedRequest(http.MethodGet, "/api/v1/ops/alert-silences", "", principal)
|
||
|
|
request.Header.Set("X-Tenant-Id", tenantID)
|
||
|
|
recorder = httptest.NewRecorder()
|
||
|
|
ListOpsAlertSilencesHandler(service).ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusOK {
|
||
|
|
t.Fatalf("list silences status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
|
|
}
|
||
|
|
request = authenticatedRequest(http.MethodPost, "/api/v1/ops/alert-silences/"+silenceResponse.Data.ID+"/revoke", `{"reason":"维护结束"}`, principal)
|
||
|
|
request.SetPathValue("id", silenceResponse.Data.ID)
|
||
|
|
request.Header.Set("X-Tenant-Id", tenantID)
|
||
|
|
recorder = httptest.NewRecorder()
|
||
|
|
RevokeOpsAlertSilenceHandler(service).ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusPreconditionRequired {
|
||
|
|
t.Fatalf("missing silence ETag status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
|
|
}
|
||
|
|
request.Header.Set("If-Match", concurrency.FormatETag(1))
|
||
|
|
recorder = httptest.NewRecorder()
|
||
|
|
RevokeOpsAlertSilenceHandler(service).ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusOK || recorder.Header().Get("ETag") != concurrency.FormatETag(2) {
|
||
|
|
t.Fatalf("revoke silence status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
|
|
}
|
||
|
|
|
||
|
|
var routeInternalID string
|
||
|
|
if err := pool.QueryRow(ctx, `SELECT id::text FROM alert_routing_policy WHERE public_id=$1::uuid`, routeResponse.Data.ID).Scan(&routeInternalID); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := pool.Exec(ctx, `INSERT INTO alert_incident(id,public_id,tenant_id,rule_code,category,severity,status,fingerprint,title,summary,routing_policy_id,first_detected_at,last_detected_at) VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001','HTTP_INTEGRATION_ALERT','BUSINESS','LOW','OPEN',$3,'HTTP告警','HTTP告警接口集成',$4::uuid,now(),now())`, incidentID, incidentPublicID, "http-alert-"+incidentPublicID, routeInternalID); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
request = authenticatedRequest(http.MethodGet, "/api/v1/ops/alerts?status=OPEN", "", principal)
|
||
|
|
request.Header.Set("X-Tenant-Id", tenantID)
|
||
|
|
recorder = httptest.NewRecorder()
|
||
|
|
ListOpsAlertsHandler(service).ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusOK {
|
||
|
|
t.Fatalf("list alerts status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
|
|
}
|
||
|
|
request = authenticatedRequest(http.MethodGet, "/api/v1/ops/alerts/"+incidentPublicID, "", principal)
|
||
|
|
request.SetPathValue("id", incidentPublicID)
|
||
|
|
request.Header.Set("X-Tenant-Id", tenantID)
|
||
|
|
recorder = httptest.NewRecorder()
|
||
|
|
GetOpsAlertHandler(service).ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusOK || recorder.Header().Get("ETag") != concurrency.FormatETag(1) {
|
||
|
|
t.Fatalf("get alert status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
|
|
}
|
||
|
|
request = authenticatedRequest(http.MethodPost, "/api/v1/ops/alerts/"+incidentPublicID+"/acknowledge", `{"note":"HTTP确认告警"}`, principal)
|
||
|
|
request.SetPathValue("id", incidentPublicID)
|
||
|
|
request.Header.Set("X-Tenant-Id", tenantID)
|
||
|
|
request.Header.Set("If-Match", concurrency.FormatETag(1))
|
||
|
|
recorder = httptest.NewRecorder()
|
||
|
|
TransitionOpsAlertHandler(service, "ACKNOWLEDGE").ServeHTTP(recorder, request)
|
||
|
|
if recorder.Code != http.StatusOK || recorder.Header().Get("ETag") != concurrency.FormatETag(2) || !strings.Contains(recorder.Body.String(), "ACKNOWLEDGED") {
|
||
|
|
t.Fatalf("ack alert status=%d body=%s", recorder.Code, recorder.Body.String())
|
||
|
|
}
|
||
|
|
}
|