backend/pkg/notification/usecase/delivery_test.go

378 lines
9.8 KiB
Go
Raw Normal View History

2025-10-22 13:40:31 +00:00
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: "<p>Test email</p>",
})
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: "<p>Test</p>",
})
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: "<p>Test</p>",
})
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: "<p>Test</p>",
})
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: "<p>Test</p>",
})
assert.Error(t, err)
assert.Equal(t, context.DeadlineExceeded, err)
}