You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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}))
|
||||
}
|
||||
Reference in New Issue
Block a user