Skip to content

Commit f607a96

Browse files
committed
* edit email_worker: add Sender and OrcidService interfaces for tests
* add test for successful ProcessPending flow
1 parent bb506c3 commit f607a96

4 files changed

Lines changed: 188 additions & 63 deletions

File tree

src/email_worker.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,25 @@ import (
88
"net/textproto"
99
)
1010

11+
type Sender interface {
12+
Send(to string, subject string, bodyText string, bodyHTML string) error
13+
}
14+
15+
type OrcidService interface {
16+
GetEmail(ctx context.Context, orcid string) (string, error)
17+
}
18+
1119
type EmailWorker struct {
1220
emailQueueRepo EmailQueueRepository
13-
emailSender *EmailSender
21+
emailSender Sender
22+
orcidService OrcidService
1423
}
1524

16-
func NewEmailWorker(emailQueueRepo EmailQueueRepository, emailSender *EmailSender) *EmailWorker {
25+
func NewEmailWorker(emailQueueRepo EmailQueueRepository, emailSender Sender, orcidService OrcidService) *EmailWorker {
1726
return &EmailWorker{
1827
emailQueueRepo: emailQueueRepo,
1928
emailSender: emailSender,
29+
orcidService: orcidService,
2030
}
2131
}
2232

@@ -66,7 +76,7 @@ func (w *EmailWorker) ProcessPending(ctx context.Context, limit int) error {
6676
}
6777

6878
for _, pending := range pendingEmails {
69-
recipientEmail, err := GetEmail(ctx, pending.RecipientOrcid)
79+
recipientEmail, err := w.orcidService.GetEmail(ctx, pending.RecipientOrcid)
7080
if err != nil {
7181
if markErr := w.retryOrFail(ctx, pending, "recipient email resolution", err); markErr != nil {
7282
return markErr

src/email_worker_test.go

Lines changed: 167 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package main
22

33
import (
4+
"context"
45
"fmt"
56
"net/textproto"
67
"testing"
7-
"context"
88
)
99

1010
const smtpPermanentErr = 530
@@ -15,37 +15,72 @@ type MockEmailQueueRepository struct {
1515
markForRetryCalled bool
1616
id int64
1717
lastError string
18+
getPendingCalled bool
19+
pendingToReturn []EmailQueue
20+
markAsSentCalled bool
1821
}
1922

23+
type MockEmailSender struct {
24+
sendCalled bool
25+
subject string
26+
bodyText string
27+
bodyHTML string
28+
to string
29+
}
30+
31+
type MockOrcidService struct {
32+
getEmailCalled bool
33+
orcidReceived string
34+
emailToReturn string
35+
errToReturn error
36+
}
2037

2138
func (m *MockEmailQueueRepository) Enqueue(ctx context.Context, item *EmailQueue) (*EmailQueue, error) {
2239
return nil, nil
2340
}
2441

2542
func (m *MockEmailQueueRepository) GetPending(ctx context.Context, limit int) ([]EmailQueue, error) {
26-
return []EmailQueue{}, nil
43+
m.getPendingCalled = true
44+
return m.pendingToReturn, nil
2745
}
2846

2947
func (m *MockEmailQueueRepository) MarkAsSent(ctx context.Context, id int64) error {
48+
m.markAsSentCalled = true
49+
m.id = id
3050
return nil
3151
}
3252

3353
func (m *MockEmailQueueRepository) MarkAsFailed(ctx context.Context, id int64, errMsg string) error {
3454
m.markAsFailedCalled = true
35-
m.id = id
36-
m.lastError = errMsg
55+
m.id = id
56+
m.lastError = errMsg
3757
return nil
3858
}
3959

4060
func (m *MockEmailQueueRepository) MarkForRetry(ctx context.Context, id int64, errMsg string) error {
4161
m.markForRetryCalled = true
42-
m.id = id
43-
m.lastError = errMsg
62+
m.id = id
63+
m.lastError = errMsg
4464
return nil
4565
}
4666

67+
func (m *MockEmailSender) Send(to string, subject string, bodyText string, bodyHTML string) error {
68+
m.sendCalled = true
69+
m.to = to
70+
m.subject = subject
71+
m.bodyText = bodyText
72+
m.bodyHTML = bodyHTML
73+
return nil
74+
}
75+
76+
func (m *MockOrcidService) GetEmail(ctx context.Context, orcid string) (string, error) {
77+
m.getEmailCalled = true
78+
m.orcidReceived = orcid
79+
return m.emailToReturn, m.errToReturn
80+
}
81+
4782
func TestIsSMTPPermanentErr(t *testing.T) {
48-
err := fmt.Errorf("error: %w", &textproto.Error{
83+
err := fmt.Errorf("error: %w", &textproto.Error{
4984
Code: smtpPermanentErr,
5085
Msg: "authentication required",
5186
})
@@ -57,7 +92,7 @@ func TestIsSMTPPermanentErr(t *testing.T) {
5792
}
5893

5994
func TestIsSMTPTemporaryErr(t *testing.T) {
60-
err := fmt.Errorf("error: %w", &textproto.Error{
95+
err := fmt.Errorf("error: %w", &textproto.Error{
6196
Code: smtpTemporaryErr,
6297
Msg: "domain not found",
6398
})
@@ -68,77 +103,154 @@ func TestIsSMTPTemporaryErr(t *testing.T) {
68103
}
69104
}
70105

71-
72106
func TestRetryOrFailSMTPPermanentError(t *testing.T) {
73-
ctx := context.Background()
107+
ctx := context.Background()
74108

75-
mockRepo := &MockEmailQueueRepository{}
109+
mockRepo := &MockEmailQueueRepository{}
76110

77-
worker := &EmailWorker{
78-
emailQueueRepo: mockRepo,
79-
}
111+
worker := &EmailWorker{
112+
emailQueueRepo: mockRepo,
113+
}
80114

81-
pending := EmailQueue{
82-
Id: 2,
83-
Attempts: 0,
84-
}
115+
pending := EmailQueue{
116+
Id: 2,
117+
Attempts: 0,
118+
}
85119

86-
smtpErr := fmt.Errorf("error: %w", &textproto.Error{
120+
smtpErr := fmt.Errorf("error: %w", &textproto.Error{
87121
Code: smtpPermanentErr,
88122
Msg: "authentication required",
89123
})
90124

91-
err := worker.retryOrFail(ctx, pending, "send", smtpErr)
92-
if err != nil {
93-
t.Fatalf("expected retryOrFail to succeed, got error: %v", err)
94-
}
125+
err := worker.retryOrFail(ctx, pending, "send", smtpErr)
126+
if err != nil {
127+
t.Fatalf("expected retryOrFail to succeed, got error: %v", err)
128+
}
95129

96-
if !mockRepo.markAsFailedCalled {
97-
t.Fatal("expected MarkAsFailed to be called")
98-
}
130+
if !mockRepo.markAsFailedCalled {
131+
t.Fatal("expected MarkAsFailed to be called")
132+
}
99133

100-
if mockRepo.markForRetryCalled {
101-
t.Fatal("expected MarkForRetry not to be called")
102-
}
134+
if mockRepo.markForRetryCalled {
135+
t.Fatal("expected MarkForRetry not to be called")
136+
}
103137

104-
if mockRepo.id != pending.Id {
105-
t.Fatalf("expected queue id %d, got %d", pending.Id, mockRepo.id)
106-
}
138+
if mockRepo.id != pending.Id {
139+
t.Fatalf("expected queue id: %d, got: %d", pending.Id, mockRepo.id)
140+
}
107141
}
108142

109143
func TestRetryOrFailSMTPTemporaryError(t *testing.T) {
110-
ctx := context.Background()
144+
ctx := context.Background()
111145

112-
mockRepo := &MockEmailQueueRepository{}
146+
mockRepo := &MockEmailQueueRepository{}
113147

114-
worker := &EmailWorker{
115-
emailQueueRepo: mockRepo,
116-
}
148+
worker := &EmailWorker{
149+
emailQueueRepo: mockRepo,
150+
}
117151

118-
pending := EmailQueue{
119-
Id: 2,
120-
Attempts: 0,
121-
}
152+
pending := EmailQueue{
153+
Id: 2,
154+
Attempts: 0,
155+
}
122156

123-
smtpErr := fmt.Errorf("error: %w", &textproto.Error{
157+
smtpErr := fmt.Errorf("error: %w", &textproto.Error{
124158
Code: smtpTemporaryErr,
125159
Msg: "domain not found",
126160
})
127161

128-
err := worker.retryOrFail(ctx, pending, "send", smtpErr)
129-
if err != nil {
130-
t.Fatalf("expected retryOrFail to succeed, got error: %v", err)
131-
}
162+
err := worker.retryOrFail(ctx, pending, "send", smtpErr)
163+
if err != nil {
164+
t.Fatalf("expected retryOrFail to succeed, got error: %v", err)
165+
}
166+
167+
if !mockRepo.markForRetryCalled {
168+
t.Fatal("expected MarkForRetry to be called")
169+
}
170+
171+
if mockRepo.markAsFailedCalled {
172+
t.Fatal("expected MarkAsFailed not to be called")
173+
}
174+
175+
if mockRepo.id != pending.Id {
176+
t.Fatalf("expected queue id: %d, got: %d", pending.Id, mockRepo.id)
177+
}
178+
}
132179

133-
if !mockRepo.markForRetryCalled {
134-
t.Fatal("expected MarkForRetry to be called")
135-
}
180+
func TestProcessPendingMarkAsSentWhenSuccess(t *testing.T) {
181+
ctx := context.Background()
136182

137-
if mockRepo.markAsFailedCalled {
138-
t.Fatal("expected MarkAsFailed not to be called")
139-
}
183+
mockRepo := &MockEmailQueueRepository{}
184+
mockSender := &MockEmailSender{}
185+
mockOrcid := &MockOrcidService{}
140186

141-
if mockRepo.id != pending.Id {
142-
t.Fatalf("expected queue id %d, got %d", pending.Id, mockRepo.id)
143-
}
187+
worker := &EmailWorker{
188+
emailQueueRepo: mockRepo,
189+
emailSender: mockSender,
190+
orcidService: mockOrcid,
191+
}
192+
193+
pending := EmailQueue{
194+
Id: 2,
195+
RecipientOrcid: "0000-0000-0000-0000",
196+
Subject: "Test",
197+
BodyText: "Body test",
198+
BodyHTML: "<p>Body test</p>",
199+
}
200+
201+
mockRepo.pendingToReturn = []EmailQueue{pending}
202+
mockOrcid.emailToReturn = "test@test.com"
203+
204+
err := worker.ProcessPending(ctx, 1)
205+
if err != nil {
206+
t.Fatalf("expected ProcessPending to succeed, got error: %v", err)
207+
}
208+
209+
if !mockRepo.getPendingCalled {
210+
t.Fatal("expected GetPending to be called")
211+
}
212+
213+
if mockRepo.id != pending.Id {
214+
t.Fatalf("expected queue id: %d, got: %d", pending.Id, mockRepo.id)
215+
}
216+
217+
if !mockOrcid.getEmailCalled {
218+
t.Fatal("expected getEmail to be called")
219+
}
220+
221+
if mockOrcid.orcidReceived != pending.RecipientOrcid {
222+
t.Fatalf("expected orcid: %q, got: %q", pending.RecipientOrcid, mockOrcid.orcidReceived)
223+
}
224+
225+
if !mockSender.sendCalled {
226+
t.Fatal("expected Send to be called")
227+
}
228+
229+
if mockSender.to != mockOrcid.emailToReturn {
230+
t.Fatalf("expected email: %q, got: %q", mockOrcid.emailToReturn, mockSender.to)
231+
}
232+
233+
if mockSender.subject != pending.Subject {
234+
t.Fatalf("expected subject: %q, got: %q", pending.Subject, mockSender.subject)
235+
}
236+
237+
if mockSender.bodyText != pending.BodyText {
238+
t.Fatalf("expected bodyText: %q, got: %q", pending.BodyText, mockSender.bodyText)
239+
}
240+
241+
if mockSender.bodyHTML != pending.BodyHTML {
242+
t.Fatalf("expected bodyHTML: %q, got: %q", pending.BodyHTML, mockSender.bodyHTML)
243+
}
244+
245+
if !mockRepo.markAsSentCalled {
246+
t.Fatal("expected MarkAsSent to be called")
247+
}
248+
249+
if mockRepo.markForRetryCalled {
250+
t.Fatal("expected MarkForRetry not to be called")
251+
}
252+
253+
if mockRepo.markAsFailedCalled {
254+
t.Fatal("expected MarkAsFailed not to be called")
255+
}
144256
}

src/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,8 @@ func main() {
474474
commentRepo := NewPostgresCommentRepository(db)
475475
notificationService := NewNotificationService(adminRepo, emailQueueRepo, commentRepo)
476476
emailSender := NewEmailSender()
477-
emailWorker := NewEmailWorker(emailQueueRepo, emailSender)
477+
orcidClient := NewOrcidClient()
478+
emailWorker := NewEmailWorker(emailQueueRepo, emailSender, orcidClient)
478479
moderationRepo := NewPostgresModerationRepository(db, categoryRepo, rorRepo)
479480
moderationHandler := NewModerationHandler(moderationRepo, adminRepo, notificationService, recordRepo)
480481
commentHandler := NewCommentHandler(commentRepo, recordRepo, adminRepo, notificationService)

src/orcid_service.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import (
99
"time"
1010
)
1111

12-
type OrcidService interface {
13-
GetEmail(ctx context.Context, orcid string) (string, error)
14-
}
12+
type OrcidClient struct{}
1513

1614
type HTTPStatusError struct {
1715
StatusCode int
@@ -23,6 +21,10 @@ type EmailUnavailable struct {
2321
Message string
2422
}
2523

24+
func NewOrcidClient() *OrcidClient {
25+
return &OrcidClient{}
26+
}
27+
2628
const orcidService = "orcid service"
2729

2830
var orcidHTTPClient = &http.Client{
@@ -38,7 +40,7 @@ func (e *EmailUnavailable) Error() string {
3840
}
3941

4042
// https://info.orcid.org/documentation/api-tutorials/api-tutorial-read-data-on-a-record
41-
func GetEmail(ctx context.Context, orcid string) (string, error) {
43+
func (c *OrcidClient) GetEmail(ctx context.Context, orcid string) (string, error) {
4244
if strings.TrimSpace(orcid) == "" {
4345
return "", fmt.Errorf("%s: orcid parameter is empty", orcidService)
4446
}

0 commit comments

Comments
 (0)