2026-08-26 06:41:07 +08:00
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 )
}
2026-09-12 08:57:29 +08:00
principal := identity . Principal { Issuer : "http://localhost:58180/realms/voice-local" , Subject : "00000000-0000-0000-0000-000000000104" }
2026-08-26 06:41:07 +08:00
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 ())
}
}