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)
|
|||
|
|
}
|