package usecase import ( "backend/pkg/notification/config" "backend/pkg/notification/domain/entity" "backend/pkg/notification/domain/repository" "backend/pkg/notification/domain/usecase" "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" ) // mockSMSRepository 模擬 SMS Repository type mockSMSRepository struct { sendFunc func(ctx context.Context, req repository.SMSMessageRequest) error } func (m *mockSMSRepository) SendSMS(ctx context.Context, req repository.SMSMessageRequest) error { if m.sendFunc != nil { return m.sendFunc(ctx, req) } return nil } // mockMailRepository 模擬 Mail Repository type mockMailRepository struct { sendFunc func(ctx context.Context, req repository.MailReq) error } func (m *mockMailRepository) SendMail(ctx context.Context, req repository.MailReq) error { if m.sendFunc != nil { return m.sendFunc(ctx, req) } return nil } // mockHistoryRepository 模擬 History Repository type mockHistoryRepository struct { histories []entity.DeliveryHistory attempts map[string][]entity.DeliveryAttempt } func (m *mockHistoryRepository) CreateHistory(ctx context.Context, history *entity.DeliveryHistory) error { m.histories = append(m.histories, *history) return nil } func (m *mockHistoryRepository) UpdateHistory(ctx context.Context, history *entity.DeliveryHistory) error { for i := range m.histories { if m.histories[i].ID == history.ID { m.histories[i] = *history return nil } } return nil } func (m *mockHistoryRepository) GetHistory(ctx context.Context, id string) (*entity.DeliveryHistory, error) { for i := range m.histories { if m.histories[i].ID == id { return &m.histories[i], nil } } return nil, errors.New("not found") } func (m *mockHistoryRepository) AddAttempt(ctx context.Context, historyID string, attempt entity.DeliveryAttempt) error { if m.attempts == nil { m.attempts = make(map[string][]entity.DeliveryAttempt) } m.attempts[historyID] = append(m.attempts[historyID], attempt) return nil } func (m *mockHistoryRepository) ListHistory(ctx context.Context, filter repository.HistoryFilter) ([]*entity.DeliveryHistory, error) { var result []*entity.DeliveryHistory for i := range m.histories { result = append(result, &m.histories[i]) } return result, nil } func TestDeliveryUseCase_SendEmail_Success(t *testing.T) { mockMail := &mockMailRepository{ sendFunc: func(ctx context.Context, req repository.MailReq) error { return nil // 成功 }, } mockHistory := &mockHistoryRepository{} uc := MustDeliveryUseCase(DeliveryUseCaseParam{ EmailProviders: []usecase.EmailProvider{ {Sort: 1, Repo: mockMail}, }, DeliveryConfig: config.DeliveryConfig{ MaxRetries: 3, InitialDelay: 10 * time.Millisecond, BackoffFactor: 2.0, MaxDelay: 100 * time.Millisecond, Timeout: 5 * time.Second, EnableHistory: true, }, HistoryRepo: mockHistory, }) ctx := context.Background() err := uc.SendEmail(ctx, usecase.MailReq{ From: "test@example.com", To: []string{"user@example.com"}, Subject: "Test", Body: "
Test email
", }) assert.NoError(t, err) // 驗證歷史記錄 assert.Equal(t, 1, len(mockHistory.histories)) assert.Equal(t, entity.DeliveryStatusSuccess, mockHistory.histories[0].Status) } func TestDeliveryUseCase_SendEmail_RetryAndSuccess(t *testing.T) { attemptCount := 0 mockMail := &mockMailRepository{ sendFunc: func(ctx context.Context, req repository.MailReq) error { attemptCount++ if attemptCount < 3 { return errors.New("temporary error") } return nil // 第三次成功 }, } mockHistory := &mockHistoryRepository{} uc := MustDeliveryUseCase(DeliveryUseCaseParam{ EmailProviders: []usecase.EmailProvider{ {Sort: 1, Repo: mockMail}, }, DeliveryConfig: config.DeliveryConfig{ MaxRetries: 3, InitialDelay: 10 * time.Millisecond, BackoffFactor: 2.0, MaxDelay: 100 * time.Millisecond, Timeout: 5 * time.Second, EnableHistory: true, }, HistoryRepo: mockHistory, }) ctx := context.Background() err := uc.SendEmail(ctx, usecase.MailReq{ From: "test@example.com", To: []string{"user@example.com"}, Subject: "Test", Body: "Test
", }) assert.NoError(t, err) assert.Equal(t, 3, attemptCount) // 重試了 3 次 assert.Equal(t, 1, len(mockHistory.histories)) assert.Equal(t, entity.DeliveryStatusSuccess, mockHistory.histories[0].Status) assert.Equal(t, 3, mockHistory.histories[0].AttemptCount) } func TestDeliveryUseCase_SendEmail_AllRetries_Failed(t *testing.T) { mockMail := &mockMailRepository{ sendFunc: func(ctx context.Context, req repository.MailReq) error { return errors.New("persistent error") }, } mockHistory := &mockHistoryRepository{} uc := MustDeliveryUseCase(DeliveryUseCaseParam{ EmailProviders: []usecase.EmailProvider{ {Sort: 1, Repo: mockMail}, }, DeliveryConfig: config.DeliveryConfig{ MaxRetries: 3, InitialDelay: 10 * time.Millisecond, BackoffFactor: 2.0, MaxDelay: 100 * time.Millisecond, Timeout: 5 * time.Second, EnableHistory: true, }, HistoryRepo: mockHistory, }) ctx := context.Background() err := uc.SendEmail(ctx, usecase.MailReq{ From: "test@example.com", To: []string{"user@example.com"}, Subject: "Test", Body: "Test
", }) assert.Error(t, err) assert.Contains(t, err.Error(), "Failed to send Email") assert.Equal(t, 1, len(mockHistory.histories)) assert.Equal(t, entity.DeliveryStatusFailed, mockHistory.histories[0].Status) assert.Equal(t, 3, mockHistory.histories[0].AttemptCount) // 嘗試了 3 次 } func TestDeliveryUseCase_SendEmail_Failover(t *testing.T) { mockMail1 := &mockMailRepository{ sendFunc: func(ctx context.Context, req repository.MailReq) error { return errors.New("provider 1 failed") }, } mockMail2 := &mockMailRepository{ sendFunc: func(ctx context.Context, req repository.MailReq) error { return nil // 備援成功 }, } mockHistory := &mockHistoryRepository{} uc := MustDeliveryUseCase(DeliveryUseCaseParam{ EmailProviders: []usecase.EmailProvider{ {Sort: 1, Repo: mockMail1}, // 主要供應商 {Sort: 2, Repo: mockMail2}, // 備援供應商 }, DeliveryConfig: config.DeliveryConfig{ MaxRetries: 2, InitialDelay: 10 * time.Millisecond, BackoffFactor: 2.0, MaxDelay: 100 * time.Millisecond, Timeout: 5 * time.Second, EnableHistory: true, }, HistoryRepo: mockHistory, }) ctx := context.Background() err := uc.SendEmail(ctx, usecase.MailReq{ From: "test@example.com", To: []string{"user@example.com"}, Subject: "Test", Body: "Test
", }) assert.NoError(t, err) // 驗證使用了備援供應商 assert.Equal(t, 1, len(mockHistory.histories)) assert.Equal(t, entity.DeliveryStatusSuccess, mockHistory.histories[0].Status) // 總共嘗試次數:provider1 重試 2 次 + provider2 成功 1 次 = 3 次 assert.Equal(t, 3, mockHistory.histories[0].AttemptCount) } func TestDeliveryUseCase_SendSMS_Success(t *testing.T) { mockSMS := &mockSMSRepository{ sendFunc: func(ctx context.Context, req repository.SMSMessageRequest) error { return nil }, } mockHistory := &mockHistoryRepository{} uc := MustDeliveryUseCase(DeliveryUseCaseParam{ SMSProviders: []usecase.SMSProvider{ {Sort: 1, Repo: mockSMS}, }, DeliveryConfig: config.DeliveryConfig{ MaxRetries: 3, InitialDelay: 10 * time.Millisecond, BackoffFactor: 2.0, MaxDelay: 100 * time.Millisecond, Timeout: 5 * time.Second, EnableHistory: true, }, HistoryRepo: mockHistory, }) ctx := context.Background() err := uc.SendMessage(ctx, usecase.SMSMessageRequest{ PhoneNumber: "+886912345678", RecipientName: "Test User", MessageContent: "Your code: 123456", }) assert.NoError(t, err) assert.Equal(t, 1, len(mockHistory.histories)) assert.Equal(t, entity.DeliveryStatusSuccess, mockHistory.histories[0].Status) } func TestDeliveryUseCase_CalculateDelay(t *testing.T) { uc := &DeliveryUseCase{ param: DeliveryUseCaseParam{ DeliveryConfig: config.DeliveryConfig{ InitialDelay: 100 * time.Millisecond, BackoffFactor: 2.0, MaxDelay: 1 * time.Second, }, }, } tests := []struct { name string attempt int expected time.Duration }{ { name: "第 0 次重試", attempt: 0, expected: 100 * time.Millisecond, }, { name: "第 1 次重試", attempt: 1, expected: 200 * time.Millisecond, }, { name: "第 2 次重試", attempt: 2, expected: 400 * time.Millisecond, }, { name: "第 3 次重試", attempt: 3, expected: 800 * time.Millisecond, }, { name: "第 10 次重試(達到 MaxDelay)", attempt: 10, expected: 1 * time.Second, // 受限於 MaxDelay }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { delay := uc.calculateDelay(tt.attempt) assert.Equal(t, tt.expected, delay) }) } } func TestDeliveryUseCase_ContextCancellation(t *testing.T) { mockMail := &mockMailRepository{ sendFunc: func(ctx context.Context, req repository.MailReq) error { // 模擬慢速操作 time.Sleep(100 * time.Millisecond) return errors.New("should not reach here") }, } uc := MustDeliveryUseCase(DeliveryUseCaseParam{ EmailProviders: []usecase.EmailProvider{ {Sort: 1, Repo: mockMail}, }, DeliveryConfig: config.DeliveryConfig{ MaxRetries: 3, InitialDelay: 50 * time.Millisecond, BackoffFactor: 2.0, MaxDelay: 500 * time.Millisecond, Timeout: 1 * time.Second, EnableHistory: false, }, }) // 創建會被取消的 context ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) defer cancel() err := uc.SendEmail(ctx, usecase.MailReq{ From: "test@example.com", To: []string{"user@example.com"}, Subject: "Test", Body: "Test
", }) assert.Error(t, err) assert.Equal(t, context.DeadlineExceeded, err) }