2026-08-26 06:41:07 +08:00
package httpapi
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/concurrency"
"data-terminal-cloud/internal/identity"
"data-terminal-cloud/internal/opsconfig"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestOpsConfigHandlersPublishAndRollback ( 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 := opsconfig . NewService ( pool , audit . NewRepository ())
if err != nil {
t . Fatal ( err )
}
2026-09-12 08:57:29 +08:00
admin := identity . Principal { Issuer : "http://localhost:58180/realms/voice-local" , Subject : "00000000-0000-0000-0000-000000000104" }
approver := identity . Principal { Issuer : admin . Issuer , Subject : "00000000-0000-0000-0000-000000000105" }
2026-08-26 06:41:07 +08:00
tenantID := "10000000-0000-0000-0000-000000000010"
configKey := "http.integration.routing." + uuid . NewString ()
createKey , evaluationKey , rollbackKey := "http-ops-create-" + uuid . NewString (), "http-ops-eval-" + uuid . NewString (), "http-ops-rollback-" + uuid . NewString ()
defer func () {
_ , _ = pool . Exec ( ctx , `DELETE FROM audit_event WHERE request_id LIKE 'http-ops-%'` )
_ , _ = pool . Exec ( ctx , `DELETE FROM idempotency_record WHERE idempotency_key=ANY($1::text[])` , [] string { createKey , evaluationKey , rollbackKey })
_ , _ = pool . Exec ( ctx , `DELETE FROM runtime_config_rollout WHERE config_key=$1` , configKey )
_ , _ = pool . Exec ( ctx , `DELETE FROM ops_config_evaluation WHERE config_version_id IN (SELECT id FROM runtime_config_version WHERE config_key=$1)` , configKey )
_ , _ = pool . Exec ( ctx , `DELETE FROM runtime_config_version WHERE config_key=$1` , configKey )
}()
withRequestID := func ( request * http . Request , id string ) * http . Request {
return request . WithContext ( context . WithValue ( request . Context (), requestIDKey , id ))
}
setOpsHeaders := func ( request * http . Request , key string ) {
request . Header . Set ( "X-Tenant-Id" , tenantID )
if key != "" {
request . Header . Set ( "Idempotency-Key" , key )
}
}
configBody , _ := json . Marshal ( map [ string ] any { "config_key" : configKey , "display_name" : "HTTP路由配置" , "config_kind" : "ROUTING" , "payload" : map [ string ] any { "default_engine_mode" : "s2s_hybrid" , "routes" : [] any {}}, "secret_refs" : map [ string ] string {}, "change_summary" : "HTTP配置状态机" })
request := withRequestID ( authenticatedRequest ( http . MethodPost , "/api/v1/ops/config-versions" , string ( configBody ), admin ), "http-ops-create" )
setOpsHeaders ( request , createKey )
recorder := httptest . NewRecorder ()
CreateOpsConfigVersionHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusCreated || recorder . Header (). Get ( "ETag" ) != concurrency . FormatETag ( 1 ) || recorder . Header (). Get ( "Cache-Control" ) != "no-store" {
t . Fatalf ( "create config status=%d headers=%v body=%s" , recorder . Code , recorder . Header (), recorder . Body . String ())
}
var configResponse struct {
Data opsconfig . Version `json:"data"`
}
if err := json . Unmarshal ( recorder . Body . Bytes (), & configResponse ); err != nil {
t . Fatal ( err )
}
version := configResponse . Data
request = withRequestID ( authenticatedRequest ( http . MethodPost , "/api/v1/ops/config-versions" , string ( configBody ), admin ), "http-ops-create-replay" )
setOpsHeaders ( request , createKey )
recorder = httptest . NewRecorder ()
CreateOpsConfigVersionHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK || recorder . Header (). Get ( "Idempotency-Replayed" ) != "true" {
t . Fatalf ( "replay config status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
request = authenticatedRequest ( http . MethodGet , "/api/v1/ops/config-versions?config_key=" + configKey , "" , approver )
setOpsHeaders ( request , "" )
recorder = httptest . NewRecorder ()
ListOpsConfigVersionsHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK || ! strings . Contains ( recorder . Body . String (), version . ID ) {
t . Fatalf ( "list config status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
request = withRequestID ( authenticatedRequest ( http . MethodPost , "/api/v1/ops/config-versions/" + version . ID + "/submit" , "" , admin ), "http-ops-submit" )
request . SetPathValue ( "id" , version . ID )
setOpsHeaders ( request , "" )
recorder = httptest . NewRecorder ()
SubmitOpsConfigVersionHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusPreconditionRequired {
t . Fatalf ( "missing submit ETag status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
request . Header . Set ( "If-Match" , concurrency . FormatETag ( 1 ))
recorder = httptest . NewRecorder ()
SubmitOpsConfigVersionHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK || recorder . Header (). Get ( "ETag" ) != concurrency . FormatETag ( 2 ) {
t . Fatalf ( "submit config status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
publishBody := `{"reason":"HTTP评测通过发布","rollout":{"scope_type":"TENANT","rollout_percent":100,"max_concurrent_calls":10}}`
request = withRequestID ( authenticatedRequest ( http . MethodPost , "/api/v1/ops/config-versions/" + version . ID + "/publish" , publishBody , approver ), "http-ops-publish-no-eval" )
request . SetPathValue ( "id" , version . ID )
setOpsHeaders ( request , "" )
request . Header . Set ( "If-Match" , concurrency . FormatETag ( 2 ))
recorder = httptest . NewRecorder ()
PublishOpsConfigVersionHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusConflict || ! strings . Contains ( recorder . Body . String (), "CONFIG_PUBLISH_GATE_FAILED" ) {
t . Fatalf ( "publish gate status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
evaluationBody := `{"suite_version":"http-suite-v1","result":"PASSED","metrics":{"cases":8,"passed":8},"report_ref":"integration://http-ops/report"}`
request = withRequestID ( authenticatedRequest ( http . MethodPost , "/api/v1/ops/config-versions/" + version . ID + "/evaluations" , evaluationBody , admin ), "http-ops-evaluation" )
request . SetPathValue ( "id" , version . ID )
setOpsHeaders ( request , evaluationKey )
recorder = httptest . NewRecorder ()
CreateOpsConfigEvaluationHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusCreated {
t . Fatalf ( "create evaluation status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
request = authenticatedRequest ( http . MethodGet , "/api/v1/ops/config-versions/" + version . ID + "/evaluations" , "" , approver )
request . SetPathValue ( "id" , version . ID )
setOpsHeaders ( request , "" )
recorder = httptest . NewRecorder ()
ListOpsConfigEvaluationsHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK || ! strings . Contains ( recorder . Body . String (), "http-suite-v1" ) {
t . Fatalf ( "list evaluations status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
request = withRequestID ( authenticatedRequest ( http . MethodPost , "/api/v1/ops/config-versions/" + version . ID + "/publish" , publishBody , approver ), "http-ops-publish" )
request . SetPathValue ( "id" , version . ID )
setOpsHeaders ( request , "" )
request . Header . Set ( "If-Match" , concurrency . FormatETag ( 2 ))
recorder = httptest . NewRecorder ()
PublishOpsConfigVersionHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK || recorder . Header (). Get ( "ETag" ) != concurrency . FormatETag ( 3 ) {
t . Fatalf ( "publish config status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
request = authenticatedRequest ( http . MethodGet , "/api/v1/ops/config-rollouts?config_key=" + configKey , "" , approver )
setOpsHeaders ( request , "" )
recorder = httptest . NewRecorder ()
ListOpsConfigRolloutsHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK || ! strings . Contains ( recorder . Body . String (), version . ID ) {
t . Fatalf ( "list rollouts status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
var rolloutResponse struct {
Data struct {
Items [] opsconfig . Rollout `json:"items"`
} `json:"data"`
}
if err = json . Unmarshal ( recorder . Body . Bytes (), & rolloutResponse ); err != nil || len ( rolloutResponse . Data . Items ) != 1 {
t . Fatalf ( "decode rollouts=%+v err=%v" , rolloutResponse , err )
}
activeRollout := rolloutResponse . Data . Items [ 0 ]
pauseBody := `{"reason":"HTTP纵向冒烟暂停"}`
request = withRequestID ( authenticatedRequest ( http . MethodPatch , "/api/v1/ops/config-rollouts/" + activeRollout . ID + "/pause" , pauseBody , approver ), "http-ops-rollout-pause" )
request . SetPathValue ( "id" , activeRollout . ID )
setOpsHeaders ( request , "" )
recorder = httptest . NewRecorder ()
PauseOpsConfigRolloutHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusPreconditionRequired {
t . Fatalf ( "missing rollout ETag status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
request . Header . Set ( "If-Match" , concurrency . FormatETag ( activeRollout . RowVersion ))
recorder = httptest . NewRecorder ()
PauseOpsConfigRolloutHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK || recorder . Header (). Get ( "ETag" ) != concurrency . FormatETag ( 2 ) || ! strings . Contains ( recorder . Body . String (), `"status":"PAUSED"` ) {
t . Fatalf ( "pause rollout status=%d headers=%v body=%s" , recorder . Code , recorder . Header (), recorder . Body . String ())
}
resumeBody := `{"reason":"HTTP纵向冒烟恢复"}`
request = withRequestID ( authenticatedRequest ( http . MethodPatch , "/api/v1/ops/config-rollouts/" + activeRollout . ID + "/resume" , resumeBody , approver ), "http-ops-rollout-resume-stale" )
request . SetPathValue ( "id" , activeRollout . ID )
setOpsHeaders ( request , "" )
request . Header . Set ( "If-Match" , concurrency . FormatETag ( 1 ))
recorder = httptest . NewRecorder ()
ResumeOpsConfigRolloutHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusPreconditionFailed || ! strings . Contains ( recorder . Body . String (), "VERSION_CONFLICT" ) {
t . Fatalf ( "stale rollout resume status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
request = withRequestID ( authenticatedRequest ( http . MethodPatch , "/api/v1/ops/config-rollouts/" + activeRollout . ID + "/resume" , resumeBody , approver ), "http-ops-rollout-resume" )
request . SetPathValue ( "id" , activeRollout . ID )
setOpsHeaders ( request , "" )
request . Header . Set ( "If-Match" , concurrency . FormatETag ( 2 ))
recorder = httptest . NewRecorder ()
ResumeOpsConfigRolloutHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusOK || recorder . Header (). Get ( "ETag" ) != concurrency . FormatETag ( 3 ) || ! strings . Contains ( recorder . Body . String (), `"status":"ACTIVE"` ) {
t . Fatalf ( "resume rollout status=%d headers=%v body=%s" , recorder . Code , recorder . Header (), recorder . Body . String ())
}
rollbackBody := `{"reason":"HTTP回滚验证","rollout":{"scope_type":"TENANT","rollout_percent":100,"max_concurrent_calls":10}}`
request = withRequestID ( authenticatedRequest ( http . MethodPost , "/api/v1/ops/config-versions/" + version . ID + "/rollback" , rollbackBody , approver ), "http-ops-rollback" )
request . SetPathValue ( "id" , version . ID )
setOpsHeaders ( request , rollbackKey )
recorder = httptest . NewRecorder ()
RollbackOpsConfigVersionHandler ( service ). ServeHTTP ( recorder , request )
if recorder . Code != http . StatusCreated || ! strings . Contains ( recorder . Body . String (), `"version_no":2` ) {
t . Fatalf ( "rollback status=%d body=%s" , recorder . Code , recorder . Body . String ())
}
}