package idempotency_test import ( "context" "sync" "testing" "github.com/break/junhong_cmp_fiber/internal/testutil" "github.com/break/junhong_cmp_fiber/pkg/idempotency" ) func TestPostgresUniqueConstraintArbitratesConcurrentFirstSubmission(t *testing.T) { db := testutil.NewPostgresTransaction(t) if err := db.Exec(` CREATE TEMP TABLE test_command_idempotency ( subject varchar(100) NOT NULL, operation varchar(100) NOT NULL, request_id varchar(100) NOT NULL, fingerprint varchar(128) NOT NULL, result_id bigint NOT NULL, UNIQUE (subject, operation, request_id) ) ON COMMIT DROP`).Error; err != nil { t.Fatalf("创建幂等契约测试表失败:%v", err) } fingerprint, err := idempotency.Fingerprint(map[string]any{"amount": 100}) if err != nil { t.Fatalf("计算指纹失败:%v", err) } start := make(chan struct{}) results := make(chan int64, 2) errs := make(chan error, 2) var wait sync.WaitGroup for _, resultID := range []int64{101, 102} { wait.Add(1) go func(id int64) { defer wait.Done() <-start result := db.WithContext(context.Background()).Exec(` INSERT INTO test_command_idempotency (subject, operation, request_id, fingerprint, result_id) VALUES (?, ?, ?, ?, ?) ON CONFLICT (subject, operation, request_id) DO NOTHING`, "account:7", "agent-recharge.create", "request-1", fingerprint.Value, id, ) results <- result.RowsAffected errs <- result.Error }(resultID) } close(start) wait.Wait() close(results) close(errs) var inserted int64 for resultErr := range errs { if resultErr != nil { t.Fatalf("并发写入失败:%v", resultErr) } } for affected := range results { inserted += affected } if inserted != 1 { t.Fatalf("并发首写必须只有一个数据库事实,实际插入:%d", inserted) } var count int64 if err := db.Table("test_command_idempotency").Count(&count).Error; err != nil { t.Fatalf("统计幂等事实失败:%v", err) } if count != 1 { t.Fatalf("幂等事实数量错误:%d", count) } } func TestIdempotencyScopeDoesNotDependOnRedis(t *testing.T) { t.Parallel() scope := idempotency.Scope{Subject: "account:7", Operation: "example.create"} if !idempotency.ValidateScope(scope, "request-1") { t.Fatal("完整作用域和请求 ID 应通过校验") } // 公共幂等契约没有 Redis 参数;Redis 仅可由业务作为削峰优化,不能参与最终裁决。 }