template-monorepo/internal/model/notification/provider/email/mock_sender.go

156 lines
3.9 KiB
Go
Raw Normal View History

package email
import (
"context"
"fmt"
2026-05-20 09:32:22 +00:00
"strings"
"sync"
2026-05-20 09:32:22 +00:00
"github.com/zeromicro/go-zero/core/logx"
)
2026-05-26 06:05:33 +00:00
// MockRedisHook is the minimal Redis surface needed to persist the last mock
// email body for dev/k6 inspection. *github.com/zeromicro/go-zero/core/stores/redis.Redis
// satisfies it (SetexCtx).
type MockRedisHook interface {
SetexCtx(ctx context.Context, key, value string, seconds int) error
}
// MockSender records calls and returns configurable results (for tests and local dev).
type MockSender struct {
name string
sort int
mu sync.Mutex
calls []*Message
Err error
MessageID string
SendHook func(ctx context.Context, msg *Message) (string, error)
2026-05-26 06:05:33 +00:00
// Optional Redis hook for dev/k6: every successful Send writes the body
// to "dev:notification:last:email:<recipient>" for each To address.
redis MockRedisHook
redisKeyTTL int
}
type MockSenderOption func(*MockSender)
func WithMockName(name string) MockSenderOption {
return func(m *MockSender) { m.name = name }
}
func WithMockSort(sort int) MockSenderOption {
return func(m *MockSender) { m.sort = sort }
}
func WithMockError(err error) MockSenderOption {
return func(m *MockSender) { m.Err = err }
}
func WithMockMessageID(id string) MockSenderOption {
return func(m *MockSender) { m.MessageID = id }
}
2026-05-26 06:05:33 +00:00
// WithMockRedis mirrors every outbound mock email body into Redis at key
// "dev:notification:last:email:<recipient>" (one key per To address).
// Primary OTP transport for k6 is still MailHog HTTP API; this hook is a
// fallback so the SMTP-disabled mock mode is also k6-friendly.
func WithMockRedis(r MockRedisHook, ttlSeconds int) MockSenderOption {
return func(m *MockSender) {
m.redis = r
if ttlSeconds <= 0 {
ttlSeconds = 600
}
m.redisKeyTTL = ttlSeconds
}
}
func NewMockSender(opts ...MockSenderOption) *MockSender {
m := &MockSender{
2026-05-26 06:05:33 +00:00
name: "mock",
sort: 0,
MessageID: "mock-email-id",
redisKeyTTL: 600,
}
for _, opt := range opts {
opt(m)
}
return m
}
func (m *MockSender) Name() string { return m.name }
func (m *MockSender) Sort() int { return m.sort }
2026-05-26 06:05:33 +00:00
// MockEmailRedisKeyPrefix is the key prefix written by the Redis hook.
const MockEmailRedisKeyPrefix = "dev:notification:last:email:"
func (m *MockSender) Send(ctx context.Context, msg *Message) (string, error) {
m.mu.Lock()
m.calls = append(m.calls, msg)
m.mu.Unlock()
if m.SendHook != nil {
return m.SendHook(ctx, msg)
}
if m.Err != nil {
return "", m.Err
}
2026-05-20 09:32:22 +00:00
if msg != nil {
logx.Infof("[notification mock email] from=%s to=%s subject=%q body=%s message_id=%s",
msg.From, strings.Join(msg.To, ","), msg.Subject, truncateForLog(msg.Body, 500), m.MessageID)
2026-05-26 06:05:33 +00:00
if m.redis != nil {
for _, to := range msg.To {
if to == "" {
continue
}
key := MockEmailRedisKeyPrefix + to
if err := m.redis.SetexCtx(ctx, key, msg.Body, m.redisKeyTTL); err != nil {
logx.Errorf("[notification mock email] redis hook setex %s: %v", key, err)
}
}
}
2026-05-20 09:32:22 +00:00
}
return m.MessageID, nil
}
2026-05-20 13:03:59 +00:00
func truncateForLog(s string, maxLen int) string {
if maxLen <= 0 || len(s) <= maxLen {
2026-05-20 09:32:22 +00:00
return s
}
2026-05-20 13:03:59 +00:00
return s[:maxLen] + "…(truncated)"
2026-05-20 09:32:22 +00:00
}
func (m *MockSender) Calls() []*Message {
m.mu.Lock()
defer m.mu.Unlock()
out := make([]*Message, len(m.calls))
copy(out, m.calls)
return out
}
func (m *MockSender) Reset() {
m.mu.Lock()
defer m.mu.Unlock()
m.calls = nil
}
// ErrSender is a Sender that always fails (helper for tests).
func ErrSender(name string, sort int, err error) Sender {
return &staticErrSender{name: name, sort: sort, err: err}
}
type staticErrSender struct {
name string
sort int
err error
}
func (s *staticErrSender) Name() string { return s.name }
func (s *staticErrSender) Sort() int { return s.sort }
func (s *staticErrSender) Send(context.Context, *Message) (string, error) {
if s.err != nil {
return "", s.err
}
return "", fmt.Errorf("%s: send failed", s.name)
}