...

Source file src/github.com/letsencrypt/boulder/cmd/notify-mailer/main_test.go

Documentation: github.com/letsencrypt/boulder/cmd/notify-mailer

     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  	// Test a number of intervals know to be OK, ensure that no error is
    24  	// produced when calling `ok()`.
    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  	// Ensure trailing " and " is trimmed from single problem.
   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  	// Ensure that a very basic template with 2 recipients can be successfully
   259  	// executed.
   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  	// With no recipients we should get an empty body error.
   265  	recipients = []recipient{}
   266  	_, err = m.makeMessageBody(recipients)
   267  	test.AssertError(t, err, "should have errored on empty body")
   268  
   269  	// With a missing key we should get an informative templating error.
   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  	// Set up a mock mailer that sleeps for `sleepLen` seconds and only has one
   282  	// goroutine to process results
   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  	// Call run() - this should sleep `sleepLen` per destination address
   296  	// After it returns, we expect (sleepLen * number of destinations) seconds has
   297  	// elapsed
   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  	// Set up a mock mailer that doesn't sleep at all
   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  	// Call run() - this should blast through all destinations without sleep
   317  	// After it returns, we expect no clock time to have elapsed on the fake clock
   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  	// Create a mailer with a checkpoint interval larger than any of the
   334  	// destination email addresses.
   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  	// Run the mailer. It should produce an error about the interval start
   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  	// Create a mailer with a negative sleep interval
   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  	// Run the mailer. It should produce an error about the sleep interval
   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  	// Create a mailer with an interval starting with a specific email address.
   373  	// It should send email to that address and others alphabetically higher.
   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  	// Run the mailer. Two messages should have been produced, one to
   387  	// test-example-updated@letsencrypt.org (beginning of the range),
   388  	// and one to test-test-test@letsencrypt.org.
   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  	// Create a mailer with a checkpoint interval ending before
   405  	// "test-example-updated@letsencrypt.org"
   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  	// Run the mailer. Two messages should have been produced, one to
   419  	// example@letsencrypt.org (ID 1), one to example-example-example@example.com (ID 2)
   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  	// Create a mailer with 10 parallel workers.
   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  	// The fake clock should have advanced 9 seconds, one for each parallel
   464  	// goroutine after the first doing its polite 1-second sleep at startup.
   465  	expectedEnd := clock.NewFake()
   466  	expectedEnd.Add(9 * time.Second)
   467  	test.AssertEquals(t, m.clk.Now(), expectedEnd.Now())
   468  
   469  	// A message should have been sent to all four addresses.
   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  	// Create a mailer with fixed content
   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  	// Run the mailer, one message should have been created with the content
   502  	// expected
   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  // Send mail with a variable interpolated.
   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  	// Run the mailer, one message should have been created with the content
   539  	// expected
   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  // Send mail with a variable interpolated multiple times for accounts that share
   551  // an email address.
   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  	// Run the mailer, one message should have been created with the content
   597  	// expected
   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  // the `mockEmailResolver` implements the `dbSelector` interface from
   614  // `notify-mailer/main.go` to allow unit testing without using a backing
   615  // database
   616  type mockEmailResolver struct{}
   617  
   618  // the `mockEmailResolver` select method treats the requested reg ID as an index
   619  // into a list of anonymous structs
   620  func (bs mockEmailResolver) SelectOne(ctx context.Context, output interface{}, _ string, args ...interface{}) error {
   621  	// The "dbList" is just a list of contact records in memory
   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  	// Play the type cast game so that we can dig into the arguments map and get
   674  	// out an int64 `id` parameter.
   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  	// Play the type cast game to get a `*contactQueryResult` so we can write
   687  	// the result from the db list.
   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  	// Start with three reg. IDs. Note: the IDs have been matched with fake
   710  	// results in the `db` slice in `mockEmailResolver`'s `SelectOne`. If you add
   711  	// more test cases here you must also add the corresponding DB result in the
   712  	// mock.
   713  	recipients := []recipient{
   714  		{
   715  			id: 1,
   716  		},
   717  		{
   718  			id: 2,
   719  		},
   720  		{
   721  			id: 3,
   722  		},
   723  		// This registration ID deliberately doesn't exist in the mock data to make
   724  		// sure this case is handled gracefully
   725  		{
   726  			id: 999,
   727  		},
   728  		// This registration ID deliberately returns an invalid email to make sure any
   729  		// invalid contact info that slipped into the DB once upon a time will be ignored
   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