231 lines
7.9 KiB
Go
231 lines
7.9 KiB
Go
package usecase_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
errs "gateway/internal/library/errors"
|
|
"gateway/internal/model/notification"
|
|
"gateway/internal/model/notification/config"
|
|
domentity "gateway/internal/model/notification/domain/entity"
|
|
"gateway/internal/model/notification/domain/enum"
|
|
domrepo "gateway/internal/model/notification/domain/repository"
|
|
domtpl "gateway/internal/model/notification/domain/template"
|
|
domusecase "gateway/internal/model/notification/domain/usecase"
|
|
mocknotifrepo "gateway/internal/model/notification/mock/repository"
|
|
"gateway/internal/model/notification/provider/email"
|
|
"gateway/internal/model/notification/provider/sms"
|
|
"gateway/internal/model/notification/repository"
|
|
"gateway/internal/model/notification/template"
|
|
"gateway/internal/model/notification/usecase"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
const (
|
|
testTenantID = "tenant-1"
|
|
testEmailFrom = "noreply@test.com"
|
|
testEmailTarget = "testEmailTarget"
|
|
testTenantName = "Acme"
|
|
varTenantName = "tenant_name"
|
|
)
|
|
|
|
func newTestNotifier(t *testing.T, repo domrepo.NotificationRepository, queue domrepo.RetryQueue) domusecase.NotifierUseCase {
|
|
t.Helper()
|
|
return usecase.MustNotifierUseCase(usecase.NotifierUseCaseParam{
|
|
Repo: repo,
|
|
Idempotency: repository.NewMemoryIdempotencyCache(),
|
|
Quota: repository.NewMemoryQuotaCounter(),
|
|
RetryQueue: queue,
|
|
Renderer: template.NewRenderer(template.DefaultRegistry(), domtpl.LocaleZhTW, domtpl.LocaleEnUS),
|
|
Email: email.NewChain(email.NewMockSender(
|
|
email.WithMockName("mock"),
|
|
email.WithMockMessageID("email-msg-1"),
|
|
)),
|
|
SMS: sms.NewChain(sms.NewMockSender(
|
|
sms.WithMockName("mock"),
|
|
sms.WithMockMessageID("sms-msg-1"),
|
|
)),
|
|
Config: config.Config{
|
|
DefaultLocale: domtpl.LocaleZhTW,
|
|
RatePerTenant: config.RatePerTenantConfig{Email: 100, SMS: 100},
|
|
Email: config.EmailConfig{From: testEmailFrom},
|
|
},
|
|
})
|
|
}
|
|
|
|
func expectIdempotencyMiss(repo *mocknotifrepo.MockNotificationRepository, kind enum.NotifyKind, idemKey string) {
|
|
repo.EXPECT().
|
|
FindByIdempotency(gomock.Any(), testTenantID, kind, idemKey).
|
|
Return(nil, notification.ErrNotFound)
|
|
}
|
|
|
|
func expectInsertAssignsID(repo *mocknotifrepo.MockNotificationRepository) {
|
|
repo.EXPECT().Insert(gomock.Any(), gomock.Any()).DoAndReturn(
|
|
func(_ context.Context, data *domentity.Notification) error {
|
|
if data.ID.IsZero() {
|
|
data.ID = bson.NewObjectID()
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestNotifier_Send_EmailSuccess(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
repo := mocknotifrepo.NewMockNotificationRepository(ctrl)
|
|
|
|
expectIdempotencyMiss(repo, enum.NotifyVerifyEmail, "challenge-1")
|
|
expectInsertAssignsID(repo)
|
|
repo.EXPECT().UpdateDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
|
|
|
|
uc := newTestNotifier(t, repo, nil)
|
|
dto, err := uc.Send(context.Background(), &domusecase.SendRequest{
|
|
TenantID: testTenantID,
|
|
UID: "uid-1",
|
|
Channel: enum.ChannelEmail,
|
|
Kind: enum.NotifyVerifyEmail,
|
|
Target: testEmailTarget,
|
|
Locale: domtpl.LocaleZhTW,
|
|
Data: map[string]any{domtpl.VarCode: "123456", domtpl.VarExpiresIn: 300},
|
|
IdempotencyKey: "challenge-1",
|
|
Severity: enum.SeverityInfo,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, enum.NotifyStatusSent, dto.Status)
|
|
assert.Equal(t, "mock", dto.Provider)
|
|
assert.Equal(t, "email-msg-1", dto.ProviderMessageID)
|
|
assert.NotEmpty(t, dto.TargetHash)
|
|
}
|
|
|
|
func TestNotifier_Send_IdempotentReplay(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
repo := mocknotifrepo.NewMockNotificationRepository(ctrl)
|
|
|
|
expectIdempotencyMiss(repo, enum.NotifyVerifyPhone, "challenge-2")
|
|
expectInsertAssignsID(repo)
|
|
repo.EXPECT().UpdateDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
|
|
|
|
uc := newTestNotifier(t, repo, nil)
|
|
req := &domusecase.SendRequest{
|
|
TenantID: testTenantID,
|
|
Channel: enum.ChannelSMS,
|
|
Kind: enum.NotifyVerifyPhone,
|
|
Target: "+886912345678",
|
|
Data: map[string]any{domtpl.VarCode: "111111", domtpl.VarExpiresIn: 300},
|
|
IdempotencyKey: "challenge-2",
|
|
}
|
|
|
|
first, err := uc.Send(context.Background(), req)
|
|
require.NoError(t, err)
|
|
|
|
second, err := uc.Send(context.Background(), req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, first.ID, second.ID)
|
|
}
|
|
|
|
func TestNotifier_Send_DoNotPersistBody(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
repo := mocknotifrepo.NewMockNotificationRepository(ctrl)
|
|
|
|
expectIdempotencyMiss(repo, enum.NotifyStepUpEmail, "challenge-3")
|
|
expectInsertAssignsID(repo)
|
|
repo.EXPECT().UpdateDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
|
|
func(_ context.Context, _, _ string, update *domrepo.NotificationDeliveryUpdate) error {
|
|
assert.Empty(t, update.Body)
|
|
return nil
|
|
},
|
|
)
|
|
|
|
uc := newTestNotifier(t, repo, nil)
|
|
_, err := uc.Send(context.Background(), &domusecase.SendRequest{
|
|
TenantID: testTenantID,
|
|
Channel: enum.ChannelEmail,
|
|
Kind: enum.NotifyStepUpEmail,
|
|
Target: "testEmailTarget",
|
|
Data: map[string]any{domtpl.VarCode: "999999", domtpl.VarExpiresIn: 300},
|
|
IdempotencyKey: "challenge-3",
|
|
DoNotPersistBody: true,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestNotifier_Enqueue_Pending(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
repo := mocknotifrepo.NewMockNotificationRepository(ctrl)
|
|
queue := mocknotifrepo.NewMockRetryQueue(ctrl)
|
|
|
|
expectIdempotencyMiss(repo, enum.NotifyTenantWelcome, "welcome-1")
|
|
expectInsertAssignsID(repo)
|
|
queue.EXPECT().Schedule(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
|
|
|
|
uc := newTestNotifier(t, repo, queue)
|
|
dto, err := uc.Enqueue(context.Background(), &domusecase.SendRequest{
|
|
TenantID: testTenantID,
|
|
Channel: enum.ChannelEmail,
|
|
Kind: enum.NotifyTenantWelcome,
|
|
Target: "admin@example.com",
|
|
Data: map[string]any{varTenantName: testTenantName},
|
|
IdempotencyKey: "welcome-1",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, enum.NotifyStatusPending, dto.Status)
|
|
}
|
|
|
|
func TestNotifier_Send_QuotaExceeded(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
repo := mocknotifrepo.NewMockNotificationRepository(ctrl)
|
|
|
|
gomock.InOrder(
|
|
repo.EXPECT().FindByIdempotency(gomock.Any(), testTenantID, enum.NotifyVerifyEmail, "q1").
|
|
Return(nil, notification.ErrNotFound),
|
|
repo.EXPECT().Insert(gomock.Any(), gomock.Any()).DoAndReturn(
|
|
func(_ context.Context, data *domentity.Notification) error {
|
|
if data.ID.IsZero() {
|
|
data.ID = bson.NewObjectID()
|
|
}
|
|
return nil
|
|
},
|
|
),
|
|
repo.EXPECT().UpdateDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil),
|
|
repo.EXPECT().FindByIdempotency(gomock.Any(), testTenantID, enum.NotifyVerifyEmail, "q2").
|
|
Return(nil, notification.ErrNotFound),
|
|
)
|
|
|
|
uc := usecase.MustNotifierUseCase(usecase.NotifierUseCaseParam{
|
|
Repo: repo,
|
|
Quota: repository.NewMemoryQuotaCounter(),
|
|
Renderer: template.NewRenderer(template.DefaultRegistry(), domtpl.LocaleZhTW),
|
|
Email: email.NewChain(email.NewMockSender()),
|
|
Config: config.Config{
|
|
RatePerTenant: config.RatePerTenantConfig{Email: 1},
|
|
Email: config.EmailConfig{From: testEmailFrom},
|
|
},
|
|
})
|
|
|
|
_, err := uc.Send(context.Background(), &domusecase.SendRequest{
|
|
TenantID: testTenantID,
|
|
Channel: enum.ChannelEmail,
|
|
Kind: enum.NotifyVerifyEmail,
|
|
Target: "a@test.com",
|
|
Data: map[string]any{domtpl.VarCode: "1", domtpl.VarExpiresIn: 60},
|
|
IdempotencyKey: "q1",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = uc.Send(context.Background(), &domusecase.SendRequest{
|
|
TenantID: testTenantID,
|
|
Channel: enum.ChannelEmail,
|
|
Kind: enum.NotifyVerifyEmail,
|
|
Target: "b@test.com",
|
|
Data: map[string]any{domtpl.VarCode: "2", domtpl.VarExpiresIn: 60},
|
|
IdempotencyKey: "q2",
|
|
})
|
|
require.Error(t, err)
|
|
var appErr *errs.Error
|
|
require.ErrorAs(t, err, &appErr)
|
|
}
|