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