Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions signing/watch/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package watch

import (
"os"
"path/filepath"
"testing"

"github.com/opensourceways/app-cla-server/signing/infrastructure/emailtmpl"
)

// TestMain 在运行本包测试前,将工作目录切到模块根目录并加载邮件模板,
// 使依赖 emailtmpl.GenEmailMsg 的测试用例能够渲染模板(Init 读取 ./conf/email-template/*.tmpl)。
// 模板加载失败仅记录、不中断,不影响模板无关的用例。
func TestMain(m *testing.M) {
if root := findModuleRoot(); root != "" {
_ = os.Chdir(root)
}

if err := emailtmpl.Init(); err != nil {
// 不 fail 整个包,仅记录;模板相关用例会自行暴露问题。
os.Exit(m.Run())
}

os.Exit(m.Run())
}

// findModuleRoot 从本测试源码所在目录向上查找包含 go.mod 的目录。
func findModuleRoot() string {
dir, err := os.Getwd()
if err != nil {
return ""
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
return ""
}
dir = parent
}
}
11 changes: 11 additions & 0 deletions signing/watch/notify_corp_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ func (impl *notifyAdminWatchImpl) handleSendEmail(link *repository.LinkCLA, corp

worker.GetEmailWorker().SendSimpleMessage(link.Email.Platform, &emailMsg)

// [audit] 记录企业 CLA 变更通知的发信意图(4W + CLA 上下文),便于审计追溯。
// 此时 corp.CLANotify 已在 handleCorpSigning 中更新为最新 CLA id。
logs.Info("[audit] cla_notify_email: scene=corp, link_id=%s, org=%s, corp_signing_id=%s, recipient=%s, from=%s, subject=%s, new_cla_id=%s, signed_cla_id=%s, notify_count=%d",
link.Id, link.Org.Alias, corp.Id, corp.Admin.EmailAddr.EmailAddr(),
emailMsg.From, emailMsg.Subject, corp.CLANotify, corp.Link.CLAInfo.CLAId, corp.ClaNotifyCount)

// Sending email is done in goroutine.
// Prevent the concurrency from being too high, which would cause the email server refused to serve.
time.Sleep(impl.config.genSendEmailInterval())
Expand Down Expand Up @@ -451,6 +457,11 @@ func (impl *notifyAdminWatchImpl) handleSendIndividualEmail(link *repository.Lin

worker.GetEmailWorker().SendSimpleMessage(link.Email.Platform, &emailMsg)

// [audit] 记录个人 CLA 变更通知的发信意图(4W + CLA 上下文),便于审计追溯。
logs.Info("[audit] cla_notify_email: scene=individual, link_id=%s, org=%s, recipient=%s, from=%s, subject=%s, new_cla_id=%s, signed_cla_id=%s, notify_count=%d",
link.Id, link.Org.Alias, is.Rep.EmailAddr.EmailAddr(),
emailMsg.From, emailMsg.Subject, latestCLA.Id, is.Link.CLAInfo.CLAId, is.ClaNotifyCount)

time.Sleep(impl.config.genSendEmailInterval())

return nil
Expand Down
109 changes: 109 additions & 0 deletions signing/watch/notify_corp_admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package watch

import (
"testing"
"time"

"github.com/opensourceways/app-cla-server/models"
"github.com/opensourceways/app-cla-server/signing/domain"
"github.com/opensourceways/app-cla-server/signing/domain/dp"
"github.com/opensourceways/app-cla-server/signing/domain/repository"
"github.com/opensourceways/app-cla-server/worker"
)

func newTestImpl() *notifyAdminWatchImpl {
Expand Down Expand Up @@ -110,3 +113,109 @@ func TestHandleIndividualSigningNoLatestCLA(t *testing.T) {

impl.handleIndividualSigning(link, is)
}

// fakeEmailWorker 用于在测试中替换 worker.GetEmailWorker(),记录发信调用。
type fakeEmailWorker struct {
sendCount int
platform string
to []string
subject string
}

func (f *fakeEmailWorker) SendSimpleMessage(platform string, msg *worker.EmailMessage) {
f.sendCount++
f.platform = platform
if msg != nil {
f.to = msg.To
f.subject = msg.Subject
}
}

func (f *fakeEmailWorker) GenCLAPDFForCorporationAndSendIt(string, *models.OrgInfo, *models.CLAInfo, *models.CorporationSigning) {
}

func (f *fakeEmailWorker) Shutdown() {}

// TestHandleSendEmailAuditLogCorp 注入假 worker 并直接调用 handleSendEmail,
// 验证企业 CLA 变更通知的发信(含 [audit] cla_notify_email: scene=corp 审计日志)路径被走到、不 panic。
func TestHandleSendEmailAuditLogCorp(t *testing.T) {
fw := &fakeEmailWorker{}
restore := worker.SetEmailWorker(fw)
defer restore()

impl := newTestImpl()

link := &repository.LinkCLA{
Id: "link1",
Org: domain.OrgInfo{Alias: "test-org", ProjectURL: "http://project.example.com"},
Email: domain.EmailInfo{
Addr: dp.CreateEmailAddr("from@example.com"),
Platform: "plat1",
},
}
corp := &repository.CorpSigningSummary{
Id: "corp1",
Admin: domain.Manager{Representative: domain.Representative{
Name: dp.CreateName("admin"),
EmailAddr: dp.CreateEmailAddr("admin@example.com"),
}},
CLANotify: "new-cla-id",
Link: domain.LinkInfo{CLAInfo: domain.CLAInfo{CLAId: "old-cla-id"}},
ClaNotifyCount: 2,
}

if err := impl.handleSendEmail(link, corp); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if fw.sendCount != 1 {
t.Fatalf("expected SendSimpleMessage called once, got %d", fw.sendCount)
}
if fw.subject != "CLA has been updated" {
t.Fatalf("unexpected subject: %s", fw.subject)
}
if len(fw.to) != 1 || fw.to[0] != "admin@example.com" {
t.Fatalf("unexpected recipient: %v", fw.to)
}
}

// TestHandleSendIndividualEmailAuditLogIndividual 注入假 worker 并直接调用 handleSendIndividualEmail,
// 验证个人 CLA 变更通知的发信(含 [audit] cla_notify_email: scene=individual 审计日志)路径被走到、不 panic。
func TestHandleSendIndividualEmailAuditLogIndividual(t *testing.T) {
fw := &fakeEmailWorker{}
restore := worker.SetEmailWorker(fw)
defer restore()

impl := newTestImpl()
impl.claPlatformURL = "http://cla.example.com/sign/"

link := &repository.LinkCLA{
Id: "link1",
Org: domain.OrgInfo{Alias: "test-org", ProjectURL: "http://project.example.com"},
Email: domain.EmailInfo{
Addr: dp.CreateEmailAddr("from@example.com"),
Platform: "plat1",
},
}
is := &domain.IndividualSigning{
Rep: domain.Representative{
Name: dp.CreateName("alice"),
EmailAddr: dp.CreateEmailAddr("alice@example.com"),
},
Link: domain.LinkInfo{CLAInfo: domain.CLAInfo{CLAId: "old-cla-id"}},
ClaNotifyCount: 3,
}
latestCLA := &domain.CLA{Id: "new-cla-id", UpdatedAt: time.Now().Unix()}

if err := impl.handleSendIndividualEmail(link, is, latestCLA); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if fw.sendCount != 1 {
t.Fatalf("expected SendSimpleMessage called once, got %d", fw.sendCount)
}
if fw.subject != "CLA has been updated - Action Required" {
t.Fatalf("unexpected subject: %s", fw.subject)
}
if len(fw.to) != 1 || fw.to[0] != "alice@example.com" {
t.Fatalf("unexpected recipient: %v", fw.to)
}
}
24 changes: 20 additions & 4 deletions worker/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ func GetEmailWorker() IEmailWorker {
return worker
}

// SetEmailWorker 注入一个 email worker 实例(主要用于测试中替换为假 worker),
// 返回还原函数以恢复原值。调用方应在测试结束后调用还原函数。
func SetEmailWorker(w IEmailWorker) func() {
prev := worker
worker = w
return func() { worker = prev }
}

func Init(g pdf.IPDFGenerator) {
pdfGenerator = g

Expand Down Expand Up @@ -104,14 +112,20 @@ func (w *emailWorker) SendSimpleMessage(emailPlatform string, msg *EmailMessage)
return nil
}

w.tryToSendEmail(action)
// [audit] 据发信最终结果记录审计日志(成功 Info / 失败 Error),覆盖所有走 SendSimpleMessage 的邮件。
if w.tryToSendEmail(action) {
logs.Info("[audit] email_result: outcome=sent, platform=%s, to=%v, subject=%s", platform, msg1.To, msg1.Subject)
} else {
logs.Error("[audit] email_result: outcome=failed, platform=%s, to=%v, subject=%s", platform, msg1.To, msg1.Subject)
}
}

w.wg.Add(1)
go f(emailPlatform, *msg)
}

func (w *emailWorker) tryToSendEmail(action func() error) {
// tryToSendEmail 执行带重试的发信动作,返回是否最终发送成功(true=成功,false=重试耗尽或被停止)。
func (w *emailWorker) tryToSendEmail(action func() error) bool {
t := time.NewTimer(1 * time.Minute)
defer t.Stop()

Expand All @@ -125,7 +139,7 @@ func (w *emailWorker) tryToSendEmail(action func() error) {
for i := 0; i < 10; i++ {
err := action()
if err == nil {
break
return true
}

logs.Error(err)
Expand All @@ -134,8 +148,10 @@ func (w *emailWorker) tryToSendEmail(action func() error) {

select {
case <-w.stop:
return
return false
case <-t.C:
}
}

return false
}
87 changes: 87 additions & 0 deletions worker/worker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package worker

import (
"errors"
"testing"

"github.com/opensourceways/app-cla-server/signing/domain/emailservice"
)

// fakeEmailService 注册到 emailservice,替代真实 SMTP,记录调用次数并可注入返回错误。
type fakeEmailService struct {
err error
calls int
}

func (f *fakeEmailService) SendEmail(msg *emailservice.EmailMessage) error {
f.calls++
return f.err
}

// TestTryToSendEmailSuccess 验证 action 首次即成功时返回 true,且 action 仅被调用一次。
func TestTryToSendEmailSuccess(t *testing.T) {
w := &emailWorker{stop: make(chan struct{})}

called := 0
action := func() error {
called++
return nil
}

if ok := w.tryToSendEmail(action); !ok {
t.Fatalf("expected true when action succeeds, got false")
}
if called != 1 {
t.Fatalf("expected action to be called once, got %d", called)
}
}

// TestTryToSendEmailFailureReturnsFalse 验证 action 持续失败时最终返回 false。
// 生产代码每次重试间隔为 1 分钟、最多 10 次,单测若等待耗尽需 ~9 分钟不可接受,
// 故预先关闭 stop 通道使其在首次失败后经 stop 分支快速返回 false(与重试耗尽语义等价)。
func TestTryToSendEmailFailureReturnsFalse(t *testing.T) {
w := &emailWorker{stop: make(chan struct{})}
close(w.stop)

action := func() error {
return errors.New("send failed")
}

if ok := w.tryToSendEmail(action); ok {
t.Fatalf("expected false when action keeps failing, got true")
}
}

// TestSendSimpleMessageSuccess 通过注册假 emailservice 使发信成功,
// 覆盖 SendSimpleMessage 中 tryToSendEmail 返回 true 的审计分支(outcome=sent)。
func TestSendSimpleMessageSuccess(t *testing.T) {
fake := &fakeEmailService{}
emailservice.Register("test-plat-success", fake)

w := &emailWorker{stop: make(chan struct{})}
msg := &EmailMessage{To: []string{"a@example.com"}, Subject: "hi"}

w.SendSimpleMessage("test-plat-success", msg)
w.Shutdown() // 关闭 stop 并等待 goroutine 结束(成功路径不依赖 stop)

if fake.calls != 1 {
t.Fatalf("expected email service called once, got %d", fake.calls)
}
}

// TestSendSimpleMessageFailure 通过注册总是失败的假 emailservice,
// 再由 Shutdown 关闭 stop 使重试快速终止,覆盖审计分支(outcome=failed)。
func TestSendSimpleMessageFailure(t *testing.T) {
fake := &fakeEmailService{err: errors.New("smtp down")}
emailservice.Register("test-plat-fail", fake)

w := &emailWorker{stop: make(chan struct{})}
msg := &EmailMessage{To: []string{"a@example.com"}, Subject: "hi"}

w.SendSimpleMessage("test-plat-fail", msg)
w.Shutdown() // 关闭 stop -> 首次失败后经 stop 分支快速返回 false

if fake.calls < 1 {
t.Fatalf("expected email service called at least once, got %d", fake.calls)
}
}
Loading