Files
junhong_cmp_fiber/internal/infrastructure/integrationlog/repository_integration_test.go
break ff44305d0e
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m19s
实现审计覆盖门禁与外部集成日志闭环
2026-07-23 21:31:22 +09:00

278 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}