实现审计覆盖门禁与外部集成日志闭环
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m19s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m19s
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
package integrationlog_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/testutil"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
func TestOutboundAttemptPersistsBeforeConditionalCompletion(t *testing.T) {
|
||||
db := testutil.NewPostgresTransaction(t)
|
||||
createTemporaryIntegrationLogTable(t, db)
|
||||
repository := integrationlog.NewRepository(db)
|
||||
|
||||
attempt, err := repository.Start(context.Background(), integrationlog.Attempt{
|
||||
IntegrationID: "integration-outbound-1",
|
||||
Provider: "gateway",
|
||||
Direction: constants.IntegrationDirectionOutbound,
|
||||
Operation: "query_card_status",
|
||||
ResourceType: "iot_card",
|
||||
ResourceID: testutil.StringPointer("1001"),
|
||||
RequestSummary: map[string]any{
|
||||
"iccid": "8986001234567890123",
|
||||
"access_token": "must-not-persist",
|
||||
"credential": "must-not-persist",
|
||||
"private_url": "https://must-not-persist.example",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("持久化外部尝试失败:%v", err)
|
||||
}
|
||||
if attempt.Result != constants.IntegrationResultPending {
|
||||
t.Fatalf("调用前必须是待处理状态,得到 %q", attempt.Result)
|
||||
}
|
||||
if strings.Contains(string(attempt.RequestSummary), "must-not-persist") {
|
||||
t.Fatalf("请求摘要泄露禁止字段:%s", attempt.RequestSummary)
|
||||
}
|
||||
|
||||
auditEventID := uint(77)
|
||||
completed, err := repository.Complete(context.Background(), attempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess,
|
||||
HTTPStatus: 200,
|
||||
ProviderCode: "0",
|
||||
ProviderMessage: "查询成功",
|
||||
StateChanged: true,
|
||||
AuditEventID: &auditEventID,
|
||||
ResponseSummary: map[string]any{"status": "active", "secret": "must-not-persist"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("终结外部尝试失败:%v", err)
|
||||
}
|
||||
if completed.Result != constants.IntegrationResultSuccess || !completed.StateChanged ||
|
||||
completed.AuditEventID == nil || *completed.AuditEventID != auditEventID {
|
||||
t.Fatalf("外部尝试终态错误:%+v", completed)
|
||||
}
|
||||
if completed.ProviderMessage == nil || strings.Contains(*completed.ProviderMessage, "查询成功") {
|
||||
t.Fatalf("不可信渠道消息必须转换为不可逆摘要:%+v", completed.ProviderMessage)
|
||||
}
|
||||
|
||||
if _, err := repository.Complete(context.Background(), attempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultFailed,
|
||||
}); err == nil {
|
||||
t.Fatal("既有终态不得被重复完成改写")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationResultNamesCoverEveryPublicTerminalState(t *testing.T) {
|
||||
for _, result := range []string{
|
||||
constants.IntegrationResultSuccess, constants.IntegrationResultFailed, constants.IntegrationResultUnknown,
|
||||
constants.IntegrationResultNotFound, constants.IntegrationResultInvalidPayload, constants.IntegrationResultIgnored,
|
||||
constants.IntegrationResultMerged, constants.IntegrationResultRateLimited, constants.IntegrationResultCompleted,
|
||||
constants.IntegrationResultCancelled,
|
||||
} {
|
||||
if constants.IntegrationResultName(result) == "" {
|
||||
t.Fatalf("公开终态 %q 缺少中文名称", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInboundAttemptIsIdempotentAndNeverPersistsRawPayload(t *testing.T) {
|
||||
db := testutil.NewPostgresTransaction(t)
|
||||
createTemporaryIntegrationLogTable(t, db)
|
||||
repository := integrationlog.NewRepository(db)
|
||||
|
||||
input := integrationlog.InboundAttempt{
|
||||
IntegrationID: "integration-inbound-1",
|
||||
IdempotencyKey: "wechat:callback:transaction-1",
|
||||
Provider: "wechat",
|
||||
Operation: "payment_callback",
|
||||
ExternalID: "transaction-1",
|
||||
RawPayload: []byte(`{"sign":"raw-signature","ciphertext":"raw-ciphertext"}`),
|
||||
ContentType: "application/json",
|
||||
}
|
||||
first, created, err := repository.RecordInbound(context.Background(), input)
|
||||
if err != nil || !created {
|
||||
t.Fatalf("首次保存入站尝试失败:created=%v err=%v", created, err)
|
||||
}
|
||||
second, created, err := repository.RecordInbound(context.Background(), input)
|
||||
if err != nil || created {
|
||||
t.Fatalf("重复入站尝试应返回既有事实:created=%v err=%v", created, err)
|
||||
}
|
||||
if first.ID != second.ID || first.ContentHash == "" {
|
||||
t.Fatalf("重复回调未复用稳定事实或缺少内容哈希:first=%+v second=%+v", first, second)
|
||||
}
|
||||
serialized := string(first.RequestSummary)
|
||||
if strings.Contains(serialized, "raw-signature") || strings.Contains(serialized, "raw-ciphertext") {
|
||||
t.Fatalf("入站摘要泄露原始载荷:%s", serialized)
|
||||
}
|
||||
conflict := input
|
||||
conflict.IntegrationID = "integration-inbound-conflict"
|
||||
conflict.RawPayload = []byte(`{"different":true}`)
|
||||
if _, _, err := repository.RecordInbound(context.Background(), conflict); err == nil {
|
||||
t.Fatal("相同入站幂等标识对应不同载荷时必须拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownResultRequiresExplicitRecoveryAndUnsentAttemptIsTerminal(t *testing.T) {
|
||||
db := testutil.NewPostgresTransaction(t)
|
||||
createTemporaryIntegrationLogTable(t, db)
|
||||
repository := integrationlog.NewRepository(db)
|
||||
|
||||
pending, err := repository.Start(context.Background(), integrationlog.Attempt{
|
||||
IntegrationID: "integration-unknown-1", Provider: "wecom",
|
||||
Direction: constants.IntegrationDirectionOutbound, Operation: "submit_approval",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("准备结果未知尝试失败:%v", err)
|
||||
}
|
||||
if _, err := repository.Complete(context.Background(), pending.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultUnknown,
|
||||
}); err == nil {
|
||||
t.Fatal("结果未知必须记录明确恢复策略")
|
||||
}
|
||||
unknown, err := repository.Complete(context.Background(), pending.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultUnknown, RecoveryStrategy: "按原外部单号查询结果,禁止盲目重发",
|
||||
})
|
||||
if err != nil || unknown.RecoveryStrategy == nil {
|
||||
t.Fatalf("保存结果未知及恢复策略失败:log=%+v err=%v", unknown, err)
|
||||
}
|
||||
|
||||
for index, terminal := range []string{
|
||||
constants.IntegrationResultIgnored, constants.IntegrationResultMerged, constants.IntegrationResultRateLimited,
|
||||
constants.IntegrationResultCompleted, constants.IntegrationResultCancelled,
|
||||
} {
|
||||
unsent, err := repository.Start(context.Background(), integrationlog.Attempt{
|
||||
IntegrationID: fmt.Sprintf("integration-unsent-%d", index), Provider: "gateway",
|
||||
Direction: constants.IntegrationDirectionOutbound, Operation: "query_card_status", InitialResult: terminal,
|
||||
})
|
||||
if err != nil || unsent.Result != terminal || unsent.HTTPStatus != nil {
|
||||
t.Fatalf("未发送终态不得伪造 HTTP 结果:log=%+v err=%v", unsent, err)
|
||||
}
|
||||
if _, err := repository.Complete(context.Background(), unsent.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess,
|
||||
}); err == nil {
|
||||
t.Fatal("未发送终态不得被后续完成改写")
|
||||
}
|
||||
}
|
||||
if _, err := repository.Start(context.Background(), integrationlog.Attempt{
|
||||
Provider: "gateway", Direction: constants.IntegrationDirectionOutbound,
|
||||
Operation: "query_card_status", InitialResult: constants.IntegrationResultSuccess,
|
||||
}); err == nil {
|
||||
t.Fatal("实际调用结果不得绕过 pending 直接写终态")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExplicitFailureAndConcurrentCompletionKeepFirstTerminalFact(t *testing.T) {
|
||||
db := testutil.NewPostgresDatabase(t)
|
||||
integrationIDs := []string{"integration-failed-1", "integration-concurrent-1"}
|
||||
t.Cleanup(func() {
|
||||
if err := db.Where("integration_id IN ?", integrationIDs).Delete(&model.IntegrationLog{}).Error; err != nil {
|
||||
t.Errorf("清理 Integration Log 并发测试数据失败:%v", err)
|
||||
}
|
||||
})
|
||||
if err := db.Where("integration_id IN ?", integrationIDs).Delete(&model.IntegrationLog{}).Error; err != nil {
|
||||
t.Fatalf("准备 Integration Log 并发测试隔离数据失败:%v", err)
|
||||
}
|
||||
repository := integrationlog.NewRepository(db)
|
||||
failedAttempt, err := repository.Start(context.Background(), integrationlog.Attempt{
|
||||
IntegrationID: "integration-failed-1", Provider: "gateway",
|
||||
Direction: constants.IntegrationDirectionOutbound, Operation: "query_card_status",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("准备明确失败尝试失败:%v", err)
|
||||
}
|
||||
failed, err := repository.Complete(context.Background(), failedAttempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultFailed, ProviderCode: "UPSTREAM_REJECTED", ProviderMessage: "上游明确拒绝",
|
||||
})
|
||||
if err != nil || failed.Result != constants.IntegrationResultFailed {
|
||||
t.Fatalf("明确失败未保存为失败终态:log=%+v err=%v", failed, err)
|
||||
}
|
||||
|
||||
attempt, err := repository.Start(context.Background(), integrationlog.Attempt{
|
||||
IntegrationID: "integration-concurrent-1", Provider: "gateway",
|
||||
Direction: constants.IntegrationDirectionOutbound, Operation: "query_card_status",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("准备并发终结尝试失败:%v", err)
|
||||
}
|
||||
|
||||
results := make(chan error, 2)
|
||||
var group sync.WaitGroup
|
||||
for _, terminal := range []string{constants.IntegrationResultSuccess, constants.IntegrationResultFailed} {
|
||||
group.Add(1)
|
||||
go func(result string) {
|
||||
defer group.Done()
|
||||
_, completeErr := repository.Complete(context.Background(), attempt.IntegrationID, integrationlog.Completion{Result: result})
|
||||
results <- completeErr
|
||||
}(terminal)
|
||||
}
|
||||
group.Wait()
|
||||
close(results)
|
||||
successes := 0
|
||||
for completeErr := range results {
|
||||
if completeErr == nil {
|
||||
successes++
|
||||
}
|
||||
}
|
||||
if successes != 1 {
|
||||
t.Fatalf("并发终结必须且只能一个成功,实际成功 %d 次", successes)
|
||||
}
|
||||
var saved model.IntegrationLog
|
||||
if err := db.Where("integration_id = ?", attempt.IntegrationID).First(&saved).Error; err != nil {
|
||||
t.Fatalf("读取并发终态失败:%v", err)
|
||||
}
|
||||
if saved.Result != constants.IntegrationResultSuccess && saved.Result != constants.IntegrationResultFailed {
|
||||
t.Fatalf("并发终结未保存明确成功或失败:%+v", saved)
|
||||
}
|
||||
}
|
||||
|
||||
func createTemporaryIntegrationLogTable(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
if err := db.Exec(`CREATE TEMP TABLE tb_integration_log (
|
||||
id bigserial PRIMARY KEY,
|
||||
integration_id varchar(64) NOT NULL UNIQUE,
|
||||
idempotency_key varchar(160),
|
||||
provider varchar(32) NOT NULL,
|
||||
direction varchar(16) NOT NULL,
|
||||
operation varchar(64) NOT NULL,
|
||||
external_id varchar(128),
|
||||
resource_type varchar(64),
|
||||
resource_id varchar(128),
|
||||
resource_key varchar(128),
|
||||
trigger_source varchar(32),
|
||||
trigger_scene varchar(128),
|
||||
trigger_series varchar(64),
|
||||
scheduled_at timestamptz,
|
||||
started_at timestamptz,
|
||||
attempt integer NOT NULL DEFAULT 1,
|
||||
result varchar(20) NOT NULL,
|
||||
http_status integer,
|
||||
provider_code varchar(64),
|
||||
provider_message varchar(500),
|
||||
request_summary jsonb,
|
||||
response_summary jsonb,
|
||||
content_hash varchar(64),
|
||||
duration_ms bigint NOT NULL DEFAULT 0,
|
||||
state_changed boolean NOT NULL DEFAULT false,
|
||||
metadata jsonb,
|
||||
recovery_strategy varchar(255),
|
||||
request_id varchar(64),
|
||||
correlation_id varchar(64),
|
||||
audit_event_id bigint,
|
||||
created_at timestamptz NOT NULL DEFAULT NOW(),
|
||||
updated_at timestamptz NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_test_integration_idempotency UNIQUE (provider, operation, idempotency_key)
|
||||
) ON COMMIT DROP`).Error; err != nil {
|
||||
t.Fatalf("创建 Integration Log 测试表失败:%v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user