1 package notmain
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "testing"
11 "text/template"
12 "time"
13
14 "github.com/jmhodges/clock"
15
16 "github.com/letsencrypt/boulder/db"
17 blog "github.com/letsencrypt/boulder/log"
18 "github.com/letsencrypt/boulder/mocks"
19 "github.com/letsencrypt/boulder/test"
20 )
21
22 func TestIntervalOK(t *testing.T) {
23
24
25 okCases := []struct {
26 testInterval interval
27 }{
28 {interval{}},
29 {interval{start: "aa", end: "\xFF"}},
30 {interval{end: "aa"}},
31 {interval{start: "aa", end: "bb"}},
32 }
33 for _, testcase := range okCases {
34 err := testcase.testInterval.ok()
35 test.AssertNotError(t, err, "valid interval produced ok() error")
36 }
37
38 badInterval := interval{start: "bb", end: "aa"}
39 err := badInterval.ok()
40 test.AssertError(t, err, "bad interval was considered ok")
41 }
42
43 func setupMakeRecipientList(t *testing.T, contents string) string {
44 entryFile, err := os.CreateTemp("", "")
45 test.AssertNotError(t, err, "couldn't create temp file")
46
47 _, err = entryFile.WriteString(contents)
48 test.AssertNotError(t, err, "couldn't write contents to temp file")
49
50 err = entryFile.Close()
51 test.AssertNotError(t, err, "couldn't close temp file")
52 return entryFile.Name()
53 }
54
55 func TestReadRecipientList(t *testing.T) {
56 contents := `id, domainName, date
57 10,example.com,2018-11-21
58 23,example.net,2018-11-22`
59
60 entryFile := setupMakeRecipientList(t, contents)
61 defer os.Remove(entryFile)
62
63 list, _, err := readRecipientsList(entryFile, ',')
64 test.AssertNotError(t, err, "received an error for a valid CSV file")
65
66 expected := []recipient{
67 {id: 10, Data: map[string]string{"date": "2018-11-21", "domainName": "example.com"}},
68 {id: 23, Data: map[string]string{"date": "2018-11-22", "domainName": "example.net"}},
69 }
70 test.AssertDeepEquals(t, list, expected)
71
72 contents = `id domainName date
73 10 example.com 2018-11-21
74 23 example.net 2018-11-22`
75
76 entryFile = setupMakeRecipientList(t, contents)
77 defer os.Remove(entryFile)
78
79 list, _, err = readRecipientsList(entryFile, '\t')
80 test.AssertNotError(t, err, "received an error for a valid TSV file")
81 test.AssertDeepEquals(t, list, expected)
82 }
83
84 func TestReadRecipientListNoExtraColumns(t *testing.T) {
85 contents := `id
86 10
87 23`
88
89 entryFile := setupMakeRecipientList(t, contents)
90 defer os.Remove(entryFile)
91
92 _, _, err := readRecipientsList(entryFile, ',')
93 test.AssertNotError(t, err, "received an error for a valid CSV file")
94 }
95
96 func TestReadRecipientsListFileNoExist(t *testing.T) {
97 _, _, err := readRecipientsList("doesNotExist", ',')
98 test.AssertError(t, err, "expected error for a file that doesn't exist")
99 }
100
101 func TestReadRecipientListWithEmptyColumnInHeader(t *testing.T) {
102 contents := `id, domainName,,date
103 10,example.com,2018-11-21
104 23,example.net`
105
106 entryFile := setupMakeRecipientList(t, contents)
107 defer os.Remove(entryFile)
108
109 _, _, err := readRecipientsList(entryFile, ',')
110 test.AssertError(t, err, "failed to error on CSV file with trailing delimiter in header")
111 test.AssertDeepEquals(t, err, errors.New("header contains an empty column"))
112 }
113
114 func TestReadRecipientListWithProblems(t *testing.T) {
115 contents := `id, domainName, date
116 10,example.com,2018-11-21
117 23,example.net,
118 10,example.com,2018-11-22
119 42,example.net,
120 24,example.com,2018-11-21
121 24,example.com,2018-11-21
122 `
123
124 entryFile := setupMakeRecipientList(t, contents)
125 defer os.Remove(entryFile)
126
127 recipients, probs, err := readRecipientsList(entryFile, ',')
128 test.AssertNotError(t, err, "received an error for a valid CSV file")
129 test.AssertEquals(t, probs, "ID(s) [23 42] contained empty columns and ID(s) [10 24] were skipped as duplicates")
130 test.AssertEquals(t, len(recipients), 4)
131
132
133 contents = `id, domainName, date
134 23,example.net,
135 10,example.com,2018-11-21
136 42,example.net,
137 `
138
139 entryFile = setupMakeRecipientList(t, contents)
140 defer os.Remove(entryFile)
141
142 _, probs, err = readRecipientsList(entryFile, ',')
143 test.AssertNotError(t, err, "received an error for a valid CSV file")
144 test.AssertEquals(t, probs, "ID(s) [23 42] contained empty columns")
145 }
146
147 func TestReadRecipientListWithEmptyLine(t *testing.T) {
148 contents := `id, domainName, date
149 10,example.com,2018-11-21
150
151 23,example.net,2018-11-22`
152
153 entryFile := setupMakeRecipientList(t, contents)
154 defer os.Remove(entryFile)
155
156 _, _, err := readRecipientsList(entryFile, ',')
157 test.AssertNotError(t, err, "received an error for a valid CSV file")
158 }
159
160 func TestReadRecipientListWithMismatchedColumns(t *testing.T) {
161 contents := `id, domainName, date
162 10,example.com,2018-11-21
163 23,example.net`
164
165 entryFile := setupMakeRecipientList(t, contents)
166 defer os.Remove(entryFile)
167
168 _, _, err := readRecipientsList(entryFile, ',')
169 test.AssertError(t, err, "failed to error on CSV file with mismatched columns")
170 }
171
172 func TestReadRecipientListWithDuplicateIDs(t *testing.T) {
173 contents := `id, domainName, date
174 10,example.com,2018-11-21
175 10,example.net,2018-11-22`
176
177 entryFile := setupMakeRecipientList(t, contents)
178 defer os.Remove(entryFile)
179
180 _, _, err := readRecipientsList(entryFile, ',')
181 test.AssertNotError(t, err, "received an error for a valid CSV file")
182 }
183
184 func TestReadRecipientListWithUnparsableID(t *testing.T) {
185 contents := `id, domainName, date
186 10,example.com,2018-11-21
187 twenty,example.net,2018-11-22`
188
189 entryFile := setupMakeRecipientList(t, contents)
190 defer os.Remove(entryFile)
191
192 _, _, err := readRecipientsList(entryFile, ',')
193 test.AssertError(t, err, "expected error for CSV file that contains an unparsable registration ID")
194 }
195
196 func TestReadRecipientListWithoutIDHeader(t *testing.T) {
197 contents := `notId, domainName, date
198 10,example.com,2018-11-21
199 twenty,example.net,2018-11-22`
200
201 entryFile := setupMakeRecipientList(t, contents)
202 defer os.Remove(entryFile)
203
204 _, _, err := readRecipientsList(entryFile, ',')
205 test.AssertError(t, err, "expected error for CSV file missing header field `id`")
206 }
207
208 func TestReadRecipientListWithNoRecords(t *testing.T) {
209 contents := `id, domainName, date
210 `
211 entryFile := setupMakeRecipientList(t, contents)
212 defer os.Remove(entryFile)
213
214 _, _, err := readRecipientsList(entryFile, ',')
215 test.AssertError(t, err, "expected error for CSV file containing only a header")
216 }
217
218 func TestReadRecipientListWithNoHeaderOrRecords(t *testing.T) {
219 contents := ``
220 entryFile := setupMakeRecipientList(t, contents)
221 defer os.Remove(entryFile)
222
223 _, _, err := readRecipientsList(entryFile, ',')
224 test.AssertError(t, err, "expected error for CSV file containing only a header")
225 test.AssertErrorIs(t, err, io.EOF)
226 }
227
228 func TestMakeMessageBody(t *testing.T) {
229 emailTemplate := `{{range . }}
230 {{ .Data.date }}
231 {{ .Data.domainName }}
232 {{end}}`
233
234 m := &mailer{
235 log: blog.UseMock(),
236 mailer: &mocks.Mailer{},
237 emailTemplate: template.Must(template.New("email").Parse(emailTemplate)).Option("missingkey=error"),
238 sleepInterval: 0,
239 targetRange: interval{end: "\xFF"},
240 clk: clock.NewFake(),
241 recipients: nil,
242 dbMap: mockEmailResolver{},
243 }
244
245 recipients := []recipient{
246 {id: 10, Data: map[string]string{"date": "2018-11-21", "domainName": "example.com"}},
247 {id: 23, Data: map[string]string{"date": "2018-11-22", "domainName": "example.net"}},
248 }
249
250 expectedMessageBody := `
251 2018-11-21
252 example.com
253
254 2018-11-22
255 example.net
256 `
257
258
259
260 messageBody, err := m.makeMessageBody(recipients)
261 test.AssertNotError(t, err, "failed to execute a valid template")
262 test.AssertEquals(t, messageBody, expectedMessageBody)
263
264
265 recipients = []recipient{}
266 _, err = m.makeMessageBody(recipients)
267 test.AssertError(t, err, "should have errored on empty body")
268
269
270 recipients = []recipient{{id: 10, Data: map[string]string{"domainName": "example.com"}}}
271 _, err = m.makeMessageBody(recipients)
272 test.AssertEquals(t, err.Error(), "template: email:2:8: executing \"email\" at <.Data.date>: map has no entry for key \"date\"")
273 }
274
275 func TestSleepInterval(t *testing.T) {
276 const sleepLen = 10
277 mc := &mocks.Mailer{}
278 dbMap := mockEmailResolver{}
279 tmpl := template.Must(template.New("letter").Parse("an email body"))
280 recipients := []recipient{{id: 1}, {id: 2}, {id: 3}}
281
282
283 m := &mailer{
284 log: blog.UseMock(),
285 mailer: mc,
286 emailTemplate: tmpl,
287 sleepInterval: sleepLen * time.Second,
288 parallelSends: 1,
289 targetRange: interval{start: "", end: "\xFF"},
290 clk: clock.NewFake(),
291 recipients: recipients,
292 dbMap: dbMap,
293 }
294
295
296
297
298 err := m.run(context.Background())
299 test.AssertNotError(t, err, "error calling mailer run()")
300 expectedEnd := clock.NewFake()
301 expectedEnd.Add(time.Second * time.Duration(sleepLen*len(recipients)))
302 test.AssertEquals(t, m.clk.Now(), expectedEnd.Now())
303
304
305 m = &mailer{
306 log: blog.UseMock(),
307 mailer: mc,
308 emailTemplate: tmpl,
309 sleepInterval: 0,
310 targetRange: interval{end: "\xFF"},
311 clk: clock.NewFake(),
312 recipients: recipients,
313 dbMap: dbMap,
314 }
315
316
317
318 err = m.run(context.Background())
319 test.AssertNotError(t, err, "error calling mailer run()")
320 expectedEnd = clock.NewFake()
321 test.AssertEquals(t, m.clk.Now(), expectedEnd.Now())
322 }
323
324 func TestMailIntervals(t *testing.T) {
325 const testSubject = "Test Subject"
326 dbMap := mockEmailResolver{}
327
328 tmpl := template.Must(template.New("letter").Parse("an email body"))
329 recipients := []recipient{{id: 1}, {id: 2}, {id: 3}}
330
331 mc := &mocks.Mailer{}
332
333
334
335 m := &mailer{
336 log: blog.UseMock(),
337 mailer: mc,
338 dbMap: dbMap,
339 subject: testSubject,
340 recipients: recipients,
341 emailTemplate: tmpl,
342 targetRange: interval{start: "\xFF", end: "\xFF\xFF"},
343 sleepInterval: 0,
344 clk: clock.NewFake(),
345 }
346
347
348 mc.Clear()
349 err := m.run(context.Background())
350 test.AssertError(t, err, "expected error")
351 test.AssertEquals(t, len(mc.Messages), 0)
352
353
354 m = &mailer{
355 log: blog.UseMock(),
356 mailer: mc,
357 dbMap: dbMap,
358 subject: testSubject,
359 recipients: recipients,
360 emailTemplate: tmpl,
361 targetRange: interval{},
362 sleepInterval: -10,
363 clk: clock.NewFake(),
364 }
365
366
367 mc.Clear()
368 err = m.run(context.Background())
369 test.AssertEquals(t, len(mc.Messages), 0)
370 test.AssertEquals(t, err.Error(), "sleep interval (-10) is < 0")
371
372
373
374 m = &mailer{
375 log: blog.UseMock(),
376 mailer: mc,
377 dbMap: dbMap,
378 subject: testSubject,
379 recipients: []recipient{{id: 1}, {id: 2}, {id: 3}, {id: 4}},
380 emailTemplate: tmpl,
381 targetRange: interval{start: "test-example-updated@letsencrypt.org", end: "\xFF"},
382 sleepInterval: 0,
383 clk: clock.NewFake(),
384 }
385
386
387
388
389 mc.Clear()
390 err = m.run(context.Background())
391 test.AssertNotError(t, err, "run() produced an error")
392 test.AssertEquals(t, len(mc.Messages), 2)
393 test.AssertEquals(t, mocks.MailerMessage{
394 To: "test-example-updated@letsencrypt.org",
395 Subject: testSubject,
396 Body: "an email body",
397 }, mc.Messages[0])
398 test.AssertEquals(t, mocks.MailerMessage{
399 To: "test-test-test@letsencrypt.org",
400 Subject: testSubject,
401 Body: "an email body",
402 }, mc.Messages[1])
403
404
405
406 m = &mailer{
407 log: blog.UseMock(),
408 mailer: mc,
409 dbMap: dbMap,
410 subject: testSubject,
411 recipients: []recipient{{id: 1}, {id: 2}, {id: 3}, {id: 4}},
412 emailTemplate: tmpl,
413 targetRange: interval{end: "test-example-updated@letsencrypt.org"},
414 sleepInterval: 0,
415 clk: clock.NewFake(),
416 }
417
418
419
420 mc.Clear()
421 err = m.run(context.Background())
422 test.AssertNotError(t, err, "run() produced an error")
423 test.AssertEquals(t, len(mc.Messages), 2)
424 test.AssertEquals(t, mocks.MailerMessage{
425 To: "example-example-example@letsencrypt.org",
426 Subject: testSubject,
427 Body: "an email body",
428 }, mc.Messages[0])
429 test.AssertEquals(t, mocks.MailerMessage{
430 To: "example@letsencrypt.org",
431 Subject: testSubject,
432 Body: "an email body",
433 }, mc.Messages[1])
434 }
435
436 func TestParallelism(t *testing.T) {
437 const testSubject = "Test Subject"
438 dbMap := mockEmailResolver{}
439
440 tmpl := template.Must(template.New("letter").Parse("an email body"))
441 recipients := []recipient{{id: 1}, {id: 2}, {id: 3}, {id: 4}}
442
443 mc := &mocks.Mailer{}
444
445
446 m := &mailer{
447 log: blog.UseMock(),
448 mailer: mc,
449 dbMap: dbMap,
450 subject: testSubject,
451 recipients: recipients,
452 emailTemplate: tmpl,
453 targetRange: interval{end: "\xFF"},
454 sleepInterval: 0,
455 parallelSends: 10,
456 clk: clock.NewFake(),
457 }
458
459 mc.Clear()
460 err := m.run(context.Background())
461 test.AssertNotError(t, err, "run() produced an error")
462
463
464
465 expectedEnd := clock.NewFake()
466 expectedEnd.Add(9 * time.Second)
467 test.AssertEquals(t, m.clk.Now(), expectedEnd.Now())
468
469
470 test.AssertEquals(t, len(mc.Messages), 4)
471 expectedAddresses := []string{
472 "example@letsencrypt.org",
473 "test-example-updated@letsencrypt.org",
474 "test-test-test@letsencrypt.org",
475 "example-example-example@letsencrypt.org",
476 }
477 for _, msg := range mc.Messages {
478 test.AssertSliceContains(t, expectedAddresses, msg.To)
479 }
480 }
481
482 func TestMessageContentStatic(t *testing.T) {
483
484 const (
485 testSubject = "Test Subject"
486 )
487 dbMap := mockEmailResolver{}
488 mc := &mocks.Mailer{}
489 m := &mailer{
490 log: blog.UseMock(),
491 mailer: mc,
492 dbMap: dbMap,
493 subject: testSubject,
494 recipients: []recipient{{id: 1}},
495 emailTemplate: template.Must(template.New("letter").Parse("an email body")),
496 targetRange: interval{end: "\xFF"},
497 sleepInterval: 0,
498 clk: clock.NewFake(),
499 }
500
501
502
503 err := m.run(context.Background())
504 test.AssertNotError(t, err, "error calling mailer run()")
505 test.AssertEquals(t, len(mc.Messages), 1)
506 test.AssertEquals(t, mocks.MailerMessage{
507 To: "example@letsencrypt.org",
508 Subject: testSubject,
509 Body: "an email body",
510 }, mc.Messages[0])
511 }
512
513
514 func TestMessageContentInterpolated(t *testing.T) {
515 recipients := []recipient{
516 {
517 id: 1,
518 Data: map[string]string{
519 "validationMethod": "eyeballing it",
520 },
521 },
522 }
523 dbMap := mockEmailResolver{}
524 mc := &mocks.Mailer{}
525 m := &mailer{
526 log: blog.UseMock(),
527 mailer: mc,
528 dbMap: dbMap,
529 subject: "Test Subject",
530 recipients: recipients,
531 emailTemplate: template.Must(template.New("letter").Parse(
532 `issued by {{range .}}{{ .Data.validationMethod }}{{end}}`)),
533 targetRange: interval{end: "\xFF"},
534 sleepInterval: 0,
535 clk: clock.NewFake(),
536 }
537
538
539
540 err := m.run(context.Background())
541 test.AssertNotError(t, err, "error calling mailer run()")
542 test.AssertEquals(t, len(mc.Messages), 1)
543 test.AssertEquals(t, mocks.MailerMessage{
544 To: "example@letsencrypt.org",
545 Subject: "Test Subject",
546 Body: "issued by eyeballing it",
547 }, mc.Messages[0])
548 }
549
550
551
552 func TestMessageContentInterpolatedMultiple(t *testing.T) {
553 recipients := []recipient{
554 {
555 id: 200,
556 Data: map[string]string{
557 "domain": "blog.example.com",
558 },
559 },
560 {
561 id: 201,
562 Data: map[string]string{
563 "domain": "nas.example.net",
564 },
565 },
566 {
567 id: 202,
568 Data: map[string]string{
569 "domain": "mail.example.org",
570 },
571 },
572 {
573 id: 203,
574 Data: map[string]string{
575 "domain": "panel.example.net",
576 },
577 },
578 }
579 dbMap := mockEmailResolver{}
580 mc := &mocks.Mailer{}
581 m := &mailer{
582 log: blog.UseMock(),
583 mailer: mc,
584 dbMap: dbMap,
585 subject: "Test Subject",
586 recipients: recipients,
587 emailTemplate: template.Must(template.New("letter").Parse(
588 `issued for:
589 {{range .}}{{ .Data.domain }}
590 {{end}}Thanks`)),
591 targetRange: interval{end: "\xFF"},
592 sleepInterval: 0,
593 clk: clock.NewFake(),
594 }
595
596
597
598 err := m.run(context.Background())
599 test.AssertNotError(t, err, "error calling mailer run()")
600 test.AssertEquals(t, len(mc.Messages), 1)
601 test.AssertEquals(t, mocks.MailerMessage{
602 To: "gotta.lotta.accounts@letsencrypt.org",
603 Subject: "Test Subject",
604 Body: `issued for:
605 blog.example.com
606 nas.example.net
607 mail.example.org
608 panel.example.net
609 Thanks`,
610 }, mc.Messages[0])
611 }
612
613
614
615
616 type mockEmailResolver struct{}
617
618
619
620 func (bs mockEmailResolver) SelectOne(ctx context.Context, output interface{}, _ string, args ...interface{}) error {
621
622 dbList := []contactQueryResult{
623 {
624 ID: 1,
625 Contact: []byte(`["mailto:example@letsencrypt.org"]`),
626 },
627 {
628 ID: 2,
629 Contact: []byte(`["mailto:test-example-updated@letsencrypt.org"]`),
630 },
631 {
632 ID: 3,
633 Contact: []byte(`["mailto:test-test-test@letsencrypt.org"]`),
634 },
635 {
636 ID: 4,
637 Contact: []byte(`["mailto:example-example-example@letsencrypt.org"]`),
638 },
639 {
640 ID: 5,
641 Contact: []byte(`["mailto:youve.got.mail@letsencrypt.org"]`),
642 },
643 {
644 ID: 6,
645 Contact: []byte(`["mailto:mail@letsencrypt.org"]`),
646 },
647 {
648 ID: 7,
649 Contact: []byte(`["mailto:***********"]`),
650 },
651 {
652 ID: 200,
653 Contact: []byte(`["mailto:gotta.lotta.accounts@letsencrypt.org"]`),
654 },
655 {
656 ID: 201,
657 Contact: []byte(`["mailto:gotta.lotta.accounts@letsencrypt.org"]`),
658 },
659 {
660 ID: 202,
661 Contact: []byte(`["mailto:gotta.lotta.accounts@letsencrypt.org"]`),
662 },
663 {
664 ID: 203,
665 Contact: []byte(`["mailto:gotta.lotta.accounts@letsencrypt.org"]`),
666 },
667 {
668 ID: 204,
669 Contact: []byte(`["mailto:gotta.lotta.accounts@letsencrypt.org"]`),
670 },
671 }
672
673
674
675 argsRaw := args[0]
676 argsMap, ok := argsRaw.(map[string]interface{})
677 if !ok {
678 return fmt.Errorf("incorrect args type %T", args)
679 }
680 idRaw := argsMap["id"]
681 id, ok := idRaw.(int64)
682 if !ok {
683 return fmt.Errorf("incorrect args ID type %T", id)
684 }
685
686
687
688 outputPtr, ok := output.(*contactQueryResult)
689 if !ok {
690 return fmt.Errorf("incorrect output type %T", output)
691 }
692
693 for _, v := range dbList {
694 if v.ID == id {
695 *outputPtr = v
696 }
697 }
698 if outputPtr.ID == 0 {
699 return db.ErrDatabaseOp{
700 Op: "select one",
701 Table: "registrations",
702 Err: sql.ErrNoRows,
703 }
704 }
705 return nil
706 }
707
708 func TestResolveEmails(t *testing.T) {
709
710
711
712
713 recipients := []recipient{
714 {
715 id: 1,
716 },
717 {
718 id: 2,
719 },
720 {
721 id: 3,
722 },
723
724
725 {
726 id: 999,
727 },
728
729
730 {
731 id: 7,
732 },
733 {
734 id: 200,
735 },
736 {
737 id: 201,
738 },
739 {
740 id: 202,
741 },
742 {
743 id: 203,
744 },
745 {
746 id: 204,
747 },
748 }
749
750 tmpl := template.Must(template.New("letter").Parse("an email body"))
751
752 dbMap := mockEmailResolver{}
753 mc := &mocks.Mailer{}
754 m := &mailer{
755 log: blog.UseMock(),
756 mailer: mc,
757 dbMap: dbMap,
758 subject: "Test",
759 recipients: recipients,
760 emailTemplate: tmpl,
761 targetRange: interval{end: "\xFF"},
762 sleepInterval: 0,
763 clk: clock.NewFake(),
764 }
765
766 addressesToRecipients, err := m.resolveAddresses(context.Background())
767 test.AssertNotError(t, err, "failed to resolveEmailAddresses")
768
769 expected := []string{
770 "example@letsencrypt.org",
771 "test-example-updated@letsencrypt.org",
772 "test-test-test@letsencrypt.org",
773 "gotta.lotta.accounts@letsencrypt.org",
774 }
775
776 test.AssertEquals(t, len(addressesToRecipients), len(expected))
777 for _, address := range expected {
778 if _, ok := addressesToRecipients[address]; !ok {
779 t.Errorf("missing entry in addressesToRecipients: %q", address)
780 }
781 }
782 }
783
View as plain text