You've already forked agentic-coding-workflow
196 lines
6.1 KiB
Go
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}))
|
|
}
|