实现审计覆盖门禁与外部集成日志闭环
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m19s

This commit is contained in:
2026-07-23 21:31:22 +09:00
parent 7e0171a8b4
commit ff44305d0e
21 changed files with 12770 additions and 75 deletions

View File

@@ -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)
}
}