Files
2026-06-22 08:55:57 +08:00

196 lines
6.1 KiB
Go

package notify
import (
"context"
"errors"
"net/smtp"
"strings"
"sync"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
// fakePrefsRepo 是 PrefsRepository 的内存实现,供 notifier 测试断言偏好过滤。
type fakePrefsRepo struct {
mu sync.Mutex
m map[uuid.UUID]Prefs
err error
}
func newFakePrefs() *fakePrefsRepo { return &fakePrefsRepo{m: map[uuid.UUID]Prefs{}} }
func (f *fakePrefsRepo) set(p Prefs) {
f.mu.Lock()
defer f.mu.Unlock()
f.m[p.UserID] = p
}
func (f *fakePrefsRepo) Get(_ context.Context, userID uuid.UUID) (Prefs, error) {
if f.err != nil {
return Prefs{}, f.err
}
f.mu.Lock()
defer f.mu.Unlock()
if p, ok := f.m[userID]; ok {
return p, nil
}
return DefaultPrefs(userID), nil
}
func (f *fakePrefsRepo) Upsert(_ context.Context, p Prefs) error {
if f.err != nil {
return f.err
}
f.set(p)
return nil
}
// captureTransport 记录最后一次 SMTP 发送,便于断言收件人 / 报文形态。
type captureTransport struct {
mu sync.Mutex
calls int
addr string
from string
to []string
msg []byte
auth smtp.Auth
}
func (c *captureTransport) Send(addr string, auth smtp.Auth, from string, to []string, msg []byte) error {
c.mu.Lock()
defer c.mu.Unlock()
c.calls++
c.addr = addr
c.from = from
c.to = to
c.msg = msg
c.auth = auth
return nil
}
// staticLookup 总是返回固定收件邮箱。
type staticLookup struct {
email string
err error
}
func (s staticLookup) EmailForUser(context.Context, uuid.UUID) (string, error) {
return s.email, s.err
}
func emailCfg() EmailConfig {
return EmailConfig{Enabled: true, SMTPHost: "smtp.test", Port: 587, From: "noreply@test"}
}
func TestEmailNotifier_SendsWhenEnabledAndSeverityMet(t *testing.T) {
uid := uuid.New()
prefs := newFakePrefs()
prefs.set(Prefs{UserID: uid, EmailEnabled: true, MinSeverity: SeverityWarning})
tr := &captureTransport{}
n := NewEmailNotifier(emailCfg(), prefs, staticLookup{email: "user@test"}, tr, newTestLogger())
err := n.Send(context.Background(), Message{
UserID: uid, Topic: "acp.permission_pending", Severity: SeverityWarning,
Title: "Approval needed", Body: "session X wants to run rm", Link: "/approvals",
})
require.NoError(t, err)
require.Equal(t, 1, tr.calls)
require.Equal(t, "smtp.test:587", tr.addr)
require.Equal(t, "noreply@test", tr.from)
require.Equal(t, []string{"user@test"}, tr.to)
require.Nil(t, tr.auth, "无 username 时不应携带 auth")
body := string(tr.msg)
require.Contains(t, body, "To: user@test")
require.Contains(t, body, "Subject: [WARNING] Approval needed")
require.Contains(t, body, "session X wants to run rm")
require.Contains(t, body, "/approvals")
}
func TestEmailNotifier_DisabledChannelSkipped(t *testing.T) {
uid := uuid.New()
prefs := newFakePrefs()
prefs.set(Prefs{UserID: uid, EmailEnabled: false, MinSeverity: SeverityInfo})
tr := &captureTransport{}
n := NewEmailNotifier(emailCfg(), prefs, staticLookup{email: "user@test"}, tr, newTestLogger())
require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
require.Equal(t, 0, tr.calls, "email_enabled=false 应跳过发送")
}
func TestEmailNotifier_BelowMinSeveritySkipped(t *testing.T) {
uid := uuid.New()
prefs := newFakePrefs()
prefs.set(Prefs{UserID: uid, EmailEnabled: true, MinSeverity: SeverityError})
tr := &captureTransport{}
n := NewEmailNotifier(emailCfg(), prefs, staticLookup{email: "user@test"}, tr, newTestLogger())
require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityWarning}))
require.Equal(t, 0, tr.calls, "低于 min_severity 应跳过")
require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
require.Equal(t, 1, tr.calls, "达到 min_severity 应发送")
}
func TestEmailNotifier_GloballyDisabledSkipped(t *testing.T) {
uid := uuid.New()
prefs := newFakePrefs()
prefs.set(Prefs{UserID: uid, EmailEnabled: true, MinSeverity: SeverityInfo})
tr := &captureTransport{}
cfg := emailCfg()
cfg.Enabled = false
n := NewEmailNotifier(cfg, prefs, staticLookup{email: "user@test"}, tr, newTestLogger())
require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
require.Equal(t, 0, tr.calls)
}
func TestEmailNotifier_NoRecipientSkipped(t *testing.T) {
uid := uuid.New()
prefs := newFakePrefs()
prefs.set(Prefs{UserID: uid, EmailEnabled: true, MinSeverity: SeverityInfo})
tr := &captureTransport{}
n := NewEmailNotifier(emailCfg(), prefs, staticLookup{email: " "}, tr, newTestLogger())
require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError}))
require.Equal(t, 0, tr.calls, "空收件人应静默跳过")
}
func TestEmailNotifier_UsesPlainAuthWhenUsernameSet(t *testing.T) {
uid := uuid.New()
prefs := newFakePrefs()
prefs.set(Prefs{UserID: uid, EmailEnabled: true, MinSeverity: SeverityInfo})
tr := &captureTransport{}
cfg := emailCfg()
cfg.Username = "u"
cfg.Password = "p"
n := NewEmailNotifier(cfg, prefs, staticLookup{email: "user@test"}, tr, newTestLogger())
require.NoError(t, n.Send(context.Background(), Message{UserID: uid, Severity: SeverityError, Title: "x"}))
require.Equal(t, 1, tr.calls)
require.NotNil(t, tr.auth, "配置 username 时应携带 PlainAuth")
}
func TestEmailNotifier_NameIsEmailChannel(t *testing.T) {
n := NewEmailNotifier(emailCfg(), newFakePrefs(), staticLookup{}, &captureTransport{}, newTestLogger())
require.Equal(t, ChannelEmail, n.Name())
}
func TestBuildEmailMessage_Headers(t *testing.T) {
body := string(buildEmailMessage("from@test", "to@test", Message{
Topic: "t", Severity: SeverityError, Title: "Title", Body: "Body", Link: "http://l",
}))
require.True(t, strings.HasPrefix(body, "From: from@test\r\n"))
require.Contains(t, body, "Subject: [ERROR] Title")
require.Contains(t, body, "Content-Type: text/plain")
}
func TestEmailNotifier_PrefsErrorPropagates(t *testing.T) {
prefs := newFakePrefs()
prefs.err = errors.New("db down")
n := NewEmailNotifier(emailCfg(), prefs, staticLookup{email: "u@t"}, &captureTransport{}, newTestLogger())
require.Error(t, n.Send(context.Background(), Message{UserID: uuid.New(), Severity: SeverityError}))
}