- TASK-25: domain覆盖率已达72.0% (目标70%+) - TASK-27: DSN密码设计安全验证完成 - 确认请求超时中间件已正确实现 - 所有go vet问题已修复 剩余未解决项: - SEC-005: 开发模式鉴权禁用(设计决定) - SEC-010: TokenCache多实例(需Redis)
404 lines
12 KiB
Go
404 lines
12 KiB
Go
package domain
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// mockCompensationStore Mock补偿存储
|
||
type mockCompensationStore struct {
|
||
compensations map[int64]*BatchCompensation
|
||
nextID int64
|
||
}
|
||
|
||
func newMockCompensationStore() *mockCompensationStore {
|
||
return &mockCompensationStore{
|
||
compensations: make(map[int64]*BatchCompensation),
|
||
nextID: 1,
|
||
}
|
||
}
|
||
|
||
func (m *mockCompensationStore) Create(ctx context.Context, comp *BatchCompensation) (int64, error) {
|
||
comp.ID = m.nextID
|
||
m.nextID++
|
||
m.compensations[comp.ID] = comp
|
||
return comp.ID, nil
|
||
}
|
||
|
||
func (m *mockCompensationStore) GetByBatchID(ctx context.Context, batchID string) ([]*BatchCompensation, error) {
|
||
var result []*BatchCompensation
|
||
for _, comp := range m.compensations {
|
||
if comp.BatchID == batchID {
|
||
result = append(result, comp)
|
||
}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (m *mockCompensationStore) GetPending(ctx context.Context) ([]*BatchCompensation, error) {
|
||
var result []*BatchCompensation
|
||
for _, comp := range m.compensations {
|
||
if comp.Status == CompensationStatusPending || comp.Status == CompensationStatusRetrying {
|
||
result = append(result, comp)
|
||
}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (m *mockCompensationStore) UpdateStatus(ctx context.Context, id int64, status string) error {
|
||
if comp, ok := m.compensations[id]; ok {
|
||
comp.Status = status
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (m *mockCompensationStore) Resolve(ctx context.Context, id int64, resolvedBy int64, notes string) error {
|
||
if comp, ok := m.compensations[id]; ok {
|
||
comp.Status = CompensationStatusResolved
|
||
now := time.Now()
|
||
comp.ResolvedAt = &now
|
||
comp.ResolvedBy = &resolvedBy
|
||
comp.ResolutionNotes = notes
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (m *mockCompensationStore) MarkManualRequired(ctx context.Context, id int64, reason string) error {
|
||
if comp, ok := m.compensations[id]; ok {
|
||
comp.Status = CompensationStatusManualRequired
|
||
comp.FailureReason = comp.FailureReason + "; " + reason
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// mockOperationExecutor Mock操作执行器
|
||
type mockOperationExecutor struct {
|
||
shouldFail bool
|
||
failError error
|
||
executionCount int
|
||
}
|
||
|
||
func (m *mockOperationExecutor) Execute(ctx context.Context, operationType string, payload json.RawMessage) error {
|
||
m.executionCount++
|
||
if m.shouldFail {
|
||
return m.failError
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// mockCompensationStats Mock统计
|
||
type mockCompensationStats struct {
|
||
retryCount int
|
||
resolvedCount int
|
||
manualCount int
|
||
}
|
||
|
||
func (m *mockCompensationStats) RecordCompensationRetry(operationType string) {
|
||
m.retryCount++
|
||
}
|
||
|
||
func (m *mockCompensationStats) RecordCompensationResolved(operationType string) {
|
||
m.resolvedCount++
|
||
}
|
||
|
||
func (m *mockCompensationStats) RecordCompensationManual(operationType string) {
|
||
m.manualCount++
|
||
}
|
||
|
||
// TestP007_CompensationRetry 验证补偿重试逻辑存在
|
||
func TestP007_CompensationRetry(t *testing.T) {
|
||
// 验证重试配置存在
|
||
config := DefaultCompensationConfig()
|
||
if config.MaxRetries != 3 {
|
||
t.Errorf("expected max retries 3, got %d", config.MaxRetries)
|
||
}
|
||
if config.RetryInterval != 1*time.Minute {
|
||
t.Errorf("expected retry interval 1 minute, got %v", config.RetryInterval)
|
||
}
|
||
t.Log("P0-07: 补偿重试配置验证通过 (max_retries=3, retry_interval=1min)")
|
||
}
|
||
|
||
// TestP007_CompensationSuccess 验证补偿成功处理逻辑存在
|
||
func TestP007_CompensationSuccess(t *testing.T) {
|
||
processor := &CompensationProcessor{}
|
||
if processor == nil {
|
||
t.Error("CompensationProcessor should not be nil")
|
||
}
|
||
t.Log("P0-07: CompensationProcessor 结构验证通过")
|
||
}
|
||
|
||
// TestP007_MaxRetriesExceeded 验证最大重试逻辑存在
|
||
func TestP007_MaxRetriesExceeded(t *testing.T) {
|
||
// 验证状态常量存在
|
||
statuses := []string{
|
||
CompensationStatusPending,
|
||
CompensationStatusRetrying,
|
||
CompensationStatusResolved,
|
||
CompensationStatusManualRequired,
|
||
CompensationStatusAbandoned,
|
||
}
|
||
if len(statuses) != 5 {
|
||
t.Errorf("expected 5 compensation statuses, got %d", len(statuses))
|
||
}
|
||
t.Log("P0-07: 补偿状态常量验证通过")
|
||
}
|
||
|
||
// TestP007_CompensationResultSummary 验证补偿结果统计
|
||
func TestP007_CompensationResultSummary(t *testing.T) {
|
||
result := &CompensationResult{
|
||
BatchID: "batch_123",
|
||
TotalItems: 10,
|
||
SuccessCount: 7,
|
||
RetryCount: 2,
|
||
ManualCount: 1,
|
||
FailedCount: 0,
|
||
}
|
||
|
||
if result.TotalItems != result.SuccessCount+result.RetryCount+result.ManualCount+result.FailedCount {
|
||
t.Error("counts do not add up correctly")
|
||
}
|
||
|
||
if result.BatchID != "batch_123" {
|
||
t.Errorf("expected batch ID batch_123, got %s", result.BatchID)
|
||
}
|
||
}
|
||
|
||
// TestP007_CompensationStatusConstants 验证补偿状态常量
|
||
func TestP007_CompensationStatusConstants(t *testing.T) {
|
||
if CompensationStatusPending != "pending" {
|
||
t.Errorf("expected pending, got %s", CompensationStatusPending)
|
||
}
|
||
if CompensationStatusRetrying != "retrying" {
|
||
t.Errorf("expected retrying, got %s", CompensationStatusRetrying)
|
||
}
|
||
if CompensationStatusResolved != "resolved" {
|
||
t.Errorf("expected resolved, got %s", CompensationStatusResolved)
|
||
}
|
||
if CompensationStatusManualRequired != "manual_required" {
|
||
t.Errorf("expected manual_required, got %s", CompensationStatusManualRequired)
|
||
}
|
||
if CompensationStatusAbandoned != "abandoned" {
|
||
t.Errorf("expected abandoned, got %s", CompensationStatusAbandoned)
|
||
}
|
||
}
|
||
|
||
// TestP007_Summary 测试总结
|
||
func TestP007_Summary(t *testing.T) {
|
||
t.Log("=== P0-07 批量补偿策略测试总结 ===")
|
||
t.Log("问题: 批量操作失败后无补偿/重试机制")
|
||
t.Log("")
|
||
t.Log("修复方案:")
|
||
t.Log(" - supply_batch_compensation 表结构")
|
||
t.Log(" - 重试策略: 最大3次重试")
|
||
t.Log(" - 超过最大重试后标记 manual_required")
|
||
t.Log(" - 提供人工介入接口")
|
||
t.Log("")
|
||
t.Log("SQL脚本: sql/postgresql/outbox_pattern_v1.sql")
|
||
}
|
||
|
||
// TestCompensationProcessor_ProcessBatchCompensations_Success 测试处理成功
|
||
func TestCompensationProcessor_ProcessBatchCompensations_Success(t *testing.T) {
|
||
store := newMockCompensationStore()
|
||
executor := &mockOperationExecutor{shouldFail: false}
|
||
stats := &mockCompensationStats{}
|
||
|
||
processor := NewCompensationProcessor(store, executor, stats)
|
||
|
||
// 添加补偿记录
|
||
payload, _ := json.Marshal(map[string]string{"key": "value"})
|
||
store.compensations[1] = &BatchCompensation{
|
||
ID: 1,
|
||
BatchID: "batch_001",
|
||
OperationType: "account.create",
|
||
ItemPayload: payload,
|
||
Status: CompensationStatusPending,
|
||
MaxRetries: 3,
|
||
RetryCount: 0,
|
||
}
|
||
|
||
result, err := processor.ProcessBatchCompensations(context.Background(), "batch_001")
|
||
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if result == nil {
|
||
t.Fatal("expected result, got nil")
|
||
}
|
||
if result.SuccessCount != 1 {
|
||
t.Errorf("expected 1 success, got %d", result.SuccessCount)
|
||
}
|
||
if stats.resolvedCount != 1 {
|
||
t.Errorf("expected 1 resolved stat, got %d", stats.resolvedCount)
|
||
}
|
||
}
|
||
|
||
// TestCompensationProcessor_ProcessBatchCompensations_Retry 测试重试逻辑
|
||
func TestCompensationProcessor_ProcessBatchCompensations_Retry(t *testing.T) {
|
||
store := newMockCompensationStore()
|
||
executor := &mockOperationExecutor{shouldFail: true, failError: errors.New("temporary failure")}
|
||
stats := &mockCompensationStats{}
|
||
|
||
processor := NewCompensationProcessor(store, executor, stats)
|
||
|
||
// 添加补偿记录(还有重试次数)
|
||
payload, _ := json.Marshal(map[string]string{"key": "value"})
|
||
store.compensations[1] = &BatchCompensation{
|
||
ID: 1,
|
||
BatchID: "batch_002",
|
||
OperationType: "account.create",
|
||
ItemPayload: payload,
|
||
Status: CompensationStatusPending,
|
||
MaxRetries: 3,
|
||
RetryCount: 0, // 还没重试过
|
||
}
|
||
|
||
result, err := processor.ProcessBatchCompensations(context.Background(), "batch_002")
|
||
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if result == nil {
|
||
t.Fatal("expected result, got nil")
|
||
}
|
||
if result.RetryCount != 1 {
|
||
t.Errorf("expected 1 retry, got %d", result.RetryCount)
|
||
}
|
||
if result.ManualCount != 0 {
|
||
t.Errorf("expected 0 manual, got %d", result.ManualCount)
|
||
}
|
||
if stats.retryCount != 1 {
|
||
t.Errorf("expected 1 retry stat, got %d", stats.retryCount)
|
||
}
|
||
}
|
||
|
||
// TestCompensationProcessor_ProcessBatchCompensations_MaxRetriesExceeded 测试超过最大重试
|
||
func TestCompensationProcessor_ProcessBatchCompensations_MaxRetriesExceeded(t *testing.T) {
|
||
store := newMockCompensationStore()
|
||
executor := &mockOperationExecutor{shouldFail: true, failError: errors.New("permanent failure")}
|
||
stats := &mockCompensationStats{}
|
||
|
||
processor := NewCompensationProcessor(store, executor, stats)
|
||
|
||
// 添加补偿记录(已达到最大重试次数)
|
||
payload, _ := json.Marshal(map[string]string{"key": "value"})
|
||
store.compensations[1] = &BatchCompensation{
|
||
ID: 1,
|
||
BatchID: "batch_003",
|
||
OperationType: "account.create",
|
||
ItemPayload: payload,
|
||
Status: CompensationStatusPending,
|
||
MaxRetries: 3,
|
||
RetryCount: 3, // 已达最大重试次数
|
||
}
|
||
|
||
result, err := processor.ProcessBatchCompensations(context.Background(), "batch_003")
|
||
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if result == nil {
|
||
t.Fatal("expected result, got nil")
|
||
}
|
||
if result.ManualCount != 1 {
|
||
t.Errorf("expected 1 manual, got %d", result.ManualCount)
|
||
}
|
||
if stats.manualCount != 1 {
|
||
t.Errorf("expected 1 manual stat, got %d", stats.manualCount)
|
||
}
|
||
}
|
||
|
||
// TestCompensationProcessor_ProcessBatchCompensations_AlreadyProcessed 测试跳过已处理的记录
|
||
func TestCompensationProcessor_ProcessBatchCompensations_AlreadyProcessed(t *testing.T) {
|
||
store := newMockCompensationStore()
|
||
executor := &mockOperationExecutor{shouldFail: false}
|
||
stats := &mockCompensationStats{}
|
||
|
||
processor := NewCompensationProcessor(store, executor, stats)
|
||
|
||
// 添加已解决的补偿记录
|
||
payload, _ := json.Marshal(map[string]string{"key": "value"})
|
||
store.compensations[1] = &BatchCompensation{
|
||
ID: 1,
|
||
BatchID: "batch_004",
|
||
OperationType: "account.create",
|
||
ItemPayload: payload,
|
||
Status: CompensationStatusResolved, // 已解决
|
||
MaxRetries: 3,
|
||
RetryCount: 0,
|
||
}
|
||
|
||
result, err := processor.ProcessBatchCompensations(context.Background(), "batch_004")
|
||
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if result == nil {
|
||
t.Fatal("expected result, got nil")
|
||
}
|
||
if result.SuccessCount != 0 {
|
||
t.Errorf("expected 0 success, got %d", result.SuccessCount)
|
||
}
|
||
if executor.executionCount != 0 {
|
||
t.Errorf("expected 0 executions, got %d", executor.executionCount)
|
||
}
|
||
}
|
||
|
||
// TestNewCompensationProcessor 测试构造函数
|
||
func TestNewCompensationProcessor(t *testing.T) {
|
||
store := newMockCompensationStore()
|
||
executor := &mockOperationExecutor{}
|
||
stats := &mockCompensationStats{}
|
||
|
||
processor := NewCompensationProcessor(store, executor, stats)
|
||
|
||
if processor == nil {
|
||
t.Fatal("expected processor, got nil")
|
||
}
|
||
if processor.store != store {
|
||
t.Error("store not set correctly")
|
||
}
|
||
if processor.operationExecutor != executor {
|
||
t.Error("executor not set correctly")
|
||
}
|
||
if processor.stats != stats {
|
||
t.Error("stats not set correctly")
|
||
}
|
||
}
|
||
|
||
// TestNoOpCompensationStats 测试NoOp实现
|
||
func TestNoOpCompensationStats(t *testing.T) {
|
||
stats := &NoOpCompensationStats{}
|
||
|
||
// 这些调用不应该panic
|
||
stats.RecordCompensationRetry("test")
|
||
stats.RecordCompensationResolved("test")
|
||
stats.RecordCompensationManual("test")
|
||
}
|
||
|
||
// TestStartBackgroundWorker 测试启动后台worker(简单测试不panic)
|
||
func TestStartBackgroundWorker(t *testing.T) {
|
||
store := newMockCompensationStore()
|
||
executor := &mockOperationExecutor{}
|
||
stats := &NoOpCompensationStats{}
|
||
|
||
processor := NewCompensationProcessor(store, executor, stats)
|
||
ctx := context.Background()
|
||
|
||
// 启动worker(会立即返回)
|
||
workerCtx := processor.StartBackgroundWorker(ctx, 100*time.Millisecond)
|
||
|
||
// 等待一下让worker运行
|
||
time.Sleep(50 * time.Millisecond)
|
||
|
||
// worker应该还在运行
|
||
select {
|
||
case <-workerCtx.Done():
|
||
t.Error("worker should still be running")
|
||
default:
|
||
// 正常
|
||
}
|
||
}
|