template-monorepo/internal/model/notification/usecase/notifier_usecase_test.go

231 lines
7.9 KiB
Go
Raw Permalink Normal View History

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