backend/pkg/notification/usecase/delivery_test.go

378 lines
9.8 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 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)
}