...

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

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

     1  package notmain
     2  
     3  import (
     4  	"context"
     5  	"crypto/ecdsa"
     6  	"crypto/elliptic"
     7  	"crypto/rand"
     8  	"crypto/x509"
     9  	"errors"
    10  	"fmt"
    11  	"math/big"
    12  	"net"
    13  	"strings"
    14  	"testing"
    15  	"text/template"
    16  	"time"
    17  
    18  	"github.com/jmhodges/clock"
    19  	"github.com/letsencrypt/boulder/core"
    20  	corepb "github.com/letsencrypt/boulder/core/proto"
    21  	"github.com/letsencrypt/boulder/db"
    22  	berrors "github.com/letsencrypt/boulder/errors"
    23  	blog "github.com/letsencrypt/boulder/log"
    24  	bmail "github.com/letsencrypt/boulder/mail"
    25  	"github.com/letsencrypt/boulder/metrics"
    26  	"github.com/letsencrypt/boulder/mocks"
    27  	"github.com/letsencrypt/boulder/sa"
    28  	sapb "github.com/letsencrypt/boulder/sa/proto"
    29  	"github.com/letsencrypt/boulder/sa/satest"
    30  	"github.com/letsencrypt/boulder/test"
    31  	isa "github.com/letsencrypt/boulder/test/inmem/sa"
    32  	"github.com/letsencrypt/boulder/test/vars"
    33  	"github.com/prometheus/client_golang/prometheus"
    34  	io_prometheus_client "github.com/prometheus/client_model/go"
    35  	"google.golang.org/grpc"
    36  )
    37  
    38  type fakeRegStore struct {
    39  	RegByID map[int64]*corepb.Registration
    40  }
    41  
    42  func (f fakeRegStore) GetRegistration(ctx context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*corepb.Registration, error) {
    43  	r, ok := f.RegByID[req.Id]
    44  	if !ok {
    45  		return r, berrors.NotFoundError("no registration found for %q", req.Id)
    46  	}
    47  	return r, nil
    48  }
    49  
    50  func newFakeRegStore() fakeRegStore {
    51  	return fakeRegStore{RegByID: make(map[int64]*corepb.Registration)}
    52  }
    53  
    54  const testTmpl = `hi, cert for DNS names {{.DNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})`
    55  const testEmailSubject = `email subject for test`
    56  const emailARaw = "rolandshoemaker@gmail.com"
    57  const emailBRaw = "test@gmail.com"
    58  
    59  var (
    60  	emailA   = "mailto:" + emailARaw
    61  	emailB   = "mailto:" + emailBRaw
    62  	jsonKeyA = []byte(`{
    63    "kty":"RSA",
    64    "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
    65    "e":"AQAB"
    66  }`)
    67  	jsonKeyB = []byte(`{
    68    "kty":"RSA",
    69    "n":"z8bp-jPtHt4lKBqepeKF28g_QAEOuEsCIou6sZ9ndsQsEjxEOQxQ0xNOQezsKa63eogw8YS3vzjUcPP5BJuVzfPfGd5NVUdT-vSSwxk3wvk_jtNqhrpcoG0elRPQfMVsQWmxCAXCVRz3xbcFI8GTe-syynG3l-g1IzYIIZVNI6jdljCZML1HOMTTW4f7uJJ8mM-08oQCeHbr5ejK7O2yMSSYxW03zY-Tj1iVEebROeMv6IEEJNFSS4yM-hLpNAqVuQxFGetwtwjDMC1Drs1dTWrPuUAAjKGrP151z1_dE74M5evpAhZUmpKv1hY-x85DC6N0hFPgowsanmTNNiV75w",
    70    "e":"AAEAAQ"
    71  }`)
    72  	jsonKeyC = []byte(`{
    73    "kty":"RSA",
    74    "n":"rFH5kUBZrlPj73epjJjyCxzVzZuV--JjKgapoqm9pOuOt20BUTdHqVfC2oDclqM7HFhkkX9OSJMTHgZ7WaVqZv9u1X2yjdx9oVmMLuspX7EytW_ZKDZSzL-sCOFCuQAuYKkLbsdcA3eHBK_lwc4zwdeHFMKIulNvLqckkqYB9s8GpgNXBDIQ8GjR5HuJke_WUNjYHSd8jY1LU9swKWsLQe2YoQUz_ekQvBvBCoaFEtrtRaSJKNLIVDObXFr2TLIiFiM0Em90kK01-eQ7ZiruZTKomll64bRFPoNo4_uwubddg3xTqur2vdF3NyhTrYdvAgTem4uC0PFjEQ1bK_djBQ",
    75    "e":"AQAB"
    76  }`)
    77  	tmpl     = template.Must(template.New("expiry-email").Parse(testTmpl))
    78  	subjTmpl = template.Must(template.New("expiry-email-subject").Parse("Testing: " + defaultExpirationSubject))
    79  )
    80  
    81  func TestSendNagsManyCerts(t *testing.T) {
    82  	mc := mocks.Mailer{}
    83  	rs := newFakeRegStore()
    84  	fc := clock.NewFake()
    85  
    86  	staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject))
    87  	tmpl := template.Must(template.New("expiry-email").Parse(
    88  		`cert for DNS names {{.TruncatedDNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})`))
    89  
    90  	m := mailer{
    91  		log:            blog.NewMock(),
    92  		mailer:         &mc,
    93  		emailTemplate:  tmpl,
    94  		addressLimiter: &limiter{clk: fc, limit: 4},
    95  		// Explicitly override the default subject to use testEmailSubject
    96  		subjectTemplate: staticTmpl,
    97  		rs:              rs,
    98  		clk:             fc,
    99  		stats:           initStats(metrics.NoopRegisterer),
   100  	}
   101  
   102  	var certs []*x509.Certificate
   103  	for i := 0; i < 101; i++ {
   104  		certs = append(certs, &x509.Certificate{
   105  			SerialNumber: big.NewInt(0x0304),
   106  			NotAfter:     fc.Now().AddDate(0, 0, 2),
   107  			DNSNames:     []string{fmt.Sprintf("example-%d.com", i)},
   108  		})
   109  	}
   110  
   111  	conn, err := m.mailer.Connect()
   112  	test.AssertNotError(t, err, "connecting SMTP")
   113  	err = m.sendNags(conn, []string{emailA}, certs)
   114  	test.AssertNotError(t, err, "sending mail")
   115  
   116  	test.AssertEquals(t, len(mc.Messages), 1)
   117  	if len(strings.Split(mc.Messages[0].Body, "\n")) > 100 {
   118  		t.Errorf("Expected mailed message to truncate after 100 domains, got: %q", mc.Messages[0].Body)
   119  	}
   120  }
   121  
   122  func TestSendNags(t *testing.T) {
   123  	mc := mocks.Mailer{}
   124  	rs := newFakeRegStore()
   125  	fc := clock.NewFake()
   126  
   127  	staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject))
   128  
   129  	log := blog.NewMock()
   130  	m := mailer{
   131  		log:            log,
   132  		mailer:         &mc,
   133  		emailTemplate:  tmpl,
   134  		addressLimiter: &limiter{clk: fc, limit: 4},
   135  		// Explicitly override the default subject to use testEmailSubject
   136  		subjectTemplate: staticTmpl,
   137  		rs:              rs,
   138  		clk:             fc,
   139  		stats:           initStats(metrics.NoopRegisterer),
   140  	}
   141  
   142  	cert := &x509.Certificate{
   143  		SerialNumber: big.NewInt(0x0304),
   144  		NotAfter:     fc.Now().AddDate(0, 0, 2),
   145  		DNSNames:     []string{"example.com"},
   146  	}
   147  
   148  	conn, err := m.mailer.Connect()
   149  	test.AssertNotError(t, err, "connecting SMTP")
   150  	err = m.sendNags(conn, []string{emailA}, []*x509.Certificate{cert})
   151  	test.AssertNotError(t, err, "Failed to send warning messages")
   152  	test.AssertEquals(t, len(mc.Messages), 1)
   153  	test.AssertEquals(t, mc.Messages[0], mocks.MailerMessage{
   154  		To:      emailARaw,
   155  		Subject: testEmailSubject,
   156  		Body:    fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)),
   157  	})
   158  
   159  	mc.Clear()
   160  	conn, err = m.mailer.Connect()
   161  	test.AssertNotError(t, err, "connecting SMTP")
   162  	err = m.sendNags(conn, []string{emailA, emailB}, []*x509.Certificate{cert})
   163  	test.AssertNotError(t, err, "Failed to send warning messages")
   164  	test.AssertEquals(t, len(mc.Messages), 2)
   165  	test.AssertEquals(t, mc.Messages[0], mocks.MailerMessage{
   166  		To:      emailARaw,
   167  		Subject: testEmailSubject,
   168  		Body:    fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)),
   169  	})
   170  	test.AssertEquals(t, mc.Messages[1], mocks.MailerMessage{
   171  		To:      emailBRaw,
   172  		Subject: testEmailSubject,
   173  		Body:    fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)),
   174  	})
   175  
   176  	mc.Clear()
   177  	conn, err = m.mailer.Connect()
   178  	test.AssertNotError(t, err, "connecting SMTP")
   179  	err = m.sendNags(conn, []string{}, []*x509.Certificate{cert})
   180  	test.AssertNotError(t, err, "Not an error to pass no email contacts")
   181  	test.AssertEquals(t, len(mc.Messages), 0)
   182  
   183  	sendLogs := log.GetAllMatching("INFO: attempting send JSON=.*")
   184  	if len(sendLogs) != 2 {
   185  		t.Errorf("expected 2 'attempting send' log line, got %d: %s", len(sendLogs), strings.Join(sendLogs, "\n"))
   186  	}
   187  	if !strings.Contains(sendLogs[0], `"Rcpt":["rolandshoemaker@gmail.com"]`) {
   188  		t.Errorf("expected first 'attempting send' log line to have one address, got %q", sendLogs[0])
   189  	}
   190  	if !strings.Contains(sendLogs[0], `"TruncatedSerials":["000000000000000000000000000000000304"]`) {
   191  		t.Errorf("expected first 'attempting send' log line to have one serial, got %q", sendLogs[0])
   192  	}
   193  	if !strings.Contains(sendLogs[0], `"DaysToExpiration":2`) {
   194  		t.Errorf("expected first 'attempting send' log line to have 2 days to expiration, got %q", sendLogs[0])
   195  	}
   196  	if !strings.Contains(sendLogs[0], `"TruncatedDNSNames":["example.com"]`) {
   197  		t.Errorf("expected first 'attempting send' log line to have 1 domain, 'example.com', got %q", sendLogs[0])
   198  	}
   199  }
   200  
   201  func TestSendNagsAddressLimited(t *testing.T) {
   202  	mc := mocks.Mailer{}
   203  	rs := newFakeRegStore()
   204  	fc := clock.NewFake()
   205  
   206  	staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject))
   207  
   208  	log := blog.NewMock()
   209  	m := mailer{
   210  		log:            log,
   211  		mailer:         &mc,
   212  		emailTemplate:  tmpl,
   213  		addressLimiter: &limiter{clk: fc, limit: 1},
   214  		// Explicitly override the default subject to use testEmailSubject
   215  		subjectTemplate: staticTmpl,
   216  		rs:              rs,
   217  		clk:             fc,
   218  		stats:           initStats(metrics.NoopRegisterer),
   219  	}
   220  
   221  	m.addressLimiter.inc(emailARaw)
   222  
   223  	cert := &x509.Certificate{
   224  		SerialNumber: big.NewInt(0x0304),
   225  		NotAfter:     fc.Now().AddDate(0, 0, 2),
   226  		DNSNames:     []string{"example.com"},
   227  	}
   228  
   229  	conn, err := m.mailer.Connect()
   230  	test.AssertNotError(t, err, "connecting SMTP")
   231  
   232  	// Try sending a message to an over-the-limit address
   233  	err = m.sendNags(conn, []string{emailA}, []*x509.Certificate{cert})
   234  	test.AssertNotError(t, err, "sending warning messages")
   235  	// Expect that no messages were sent because this address was over the limit
   236  	test.AssertEquals(t, len(mc.Messages), 0)
   237  
   238  	// Try sending a message to an over-the-limit address and an under-the-limit
   239  	// one. It should only go to the under-the-limit one.
   240  	err = m.sendNags(conn, []string{emailA, emailB}, []*x509.Certificate{cert})
   241  	test.AssertNotError(t, err, "sending warning messages to two addresses")
   242  	test.AssertEquals(t, len(mc.Messages), 1)
   243  	test.AssertEquals(t, mc.Messages[0], mocks.MailerMessage{
   244  		To:      emailBRaw,
   245  		Subject: testEmailSubject,
   246  		Body:    fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)),
   247  	})
   248  }
   249  
   250  var serial1 = big.NewInt(0x1336)
   251  var serial2 = big.NewInt(0x1337)
   252  var serial3 = big.NewInt(0x1338)
   253  var serial4 = big.NewInt(0x1339)
   254  var serial4String = core.SerialToString(serial4)
   255  var serial5 = big.NewInt(0x1340)
   256  var serial5String = core.SerialToString(serial5)
   257  var serial6 = big.NewInt(0x1341)
   258  var serial7 = big.NewInt(0x1342)
   259  var serial8 = big.NewInt(0x1343)
   260  var serial9 = big.NewInt(0x1344)
   261  
   262  var testKey *ecdsa.PrivateKey
   263  
   264  func init() {
   265  	var err error
   266  	testKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   267  	if err != nil {
   268  		panic(err)
   269  	}
   270  }
   271  
   272  func TestProcessCerts(t *testing.T) {
   273  	expiresIn := time.Hour * 24 * 7
   274  	testCtx := setup(t, []time.Duration{expiresIn})
   275  
   276  	certs := addExpiringCerts(t, testCtx)
   277  	err := testCtx.m.processCerts(context.Background(), certs, expiresIn)
   278  	test.AssertNotError(t, err, "processing certs")
   279  	// Test that the lastExpirationNagSent was updated for the certificate
   280  	// corresponding to serial4, which is set up as "already renewed" by
   281  	// addExpiringCerts.
   282  	if len(testCtx.log.GetAllMatching("UPDATE certificateStatus.*000000000000000000000000000000001339")) != 1 {
   283  		t.Errorf("Expected an update to certificateStatus, got these log lines:\n%s",
   284  			strings.Join(testCtx.log.GetAll(), "\n"))
   285  	}
   286  }
   287  
   288  // There's an account with an expiring certificate but no email address. We shouldn't examine
   289  // that certificate repeatedly; we should mark it as if it had an email sent already.
   290  func TestNoContactCertIsNotRenewed(t *testing.T) {
   291  	expiresIn := time.Hour * 24 * 7
   292  	testCtx := setup(t, []time.Duration{expiresIn})
   293  
   294  	reg, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, nil)
   295  	test.AssertNotError(t, err, "Couldn't store regA")
   296  
   297  	cert, err := makeCertificate(
   298  		reg.Id,
   299  		serial1,
   300  		[]string{"example-a.com"},
   301  		23*time.Hour,
   302  		testCtx.fc)
   303  	test.AssertNotError(t, err, "creating cert A")
   304  
   305  	err = insertCertificate(cert, time.Time{})
   306  	test.AssertNotError(t, err, "inserting certificate")
   307  
   308  	err = testCtx.m.findExpiringCertificates(context.Background())
   309  	test.AssertNotError(t, err, "finding expired certificates")
   310  
   311  	// We should have sent no mail, because there was no contact address
   312  	test.AssertEquals(t, len(testCtx.mc.Messages), 0)
   313  
   314  	// We should have examined exactly one certificate
   315  	certsExamined := testCtx.m.stats.certificatesExamined
   316  	test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0)
   317  
   318  	certsAlreadyRenewed := testCtx.m.stats.certificatesAlreadyRenewed
   319  	test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 0.0)
   320  
   321  	// Run findExpiringCertificates again. The count of examined certificates
   322  	// should not increase again.
   323  	err = testCtx.m.findExpiringCertificates(context.Background())
   324  	test.AssertNotError(t, err, "finding expired certificates")
   325  	test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0)
   326  	test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 0.0)
   327  }
   328  
   329  // An account with no contact info has a certificate that is expiring but has been renewed.
   330  // We should only examine that certificate once.
   331  func TestNoContactCertIsRenewed(t *testing.T) {
   332  	ctx := context.Background()
   333  
   334  	testCtx := setup(t, []time.Duration{time.Hour * 24 * 7})
   335  
   336  	reg, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{})
   337  	test.AssertNotError(t, err, "Couldn't store regA")
   338  
   339  	names := []string{"example-a.com"}
   340  	cert, err := makeCertificate(
   341  		reg.Id,
   342  		serial1,
   343  		names,
   344  		23*time.Hour,
   345  		testCtx.fc)
   346  	test.AssertNotError(t, err, "creating cert A")
   347  
   348  	expires := testCtx.fc.Now().Add(23 * time.Hour)
   349  
   350  	err = insertCertificate(cert, time.Time{})
   351  	test.AssertNotError(t, err, "inserting certificate")
   352  
   353  	setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
   354  	test.AssertNotError(t, err, "setting up DB")
   355  	err = setupDBMap.Insert(ctx, &core.FQDNSet{
   356  		SetHash: core.HashNames(names),
   357  		Serial:  core.SerialToString(serial2),
   358  		Issued:  testCtx.fc.Now().Add(time.Hour),
   359  		Expires: expires.Add(time.Hour),
   360  	})
   361  	test.AssertNotError(t, err, "inserting FQDNSet for renewal")
   362  
   363  	err = testCtx.m.findExpiringCertificates(ctx)
   364  	test.AssertNotError(t, err, "finding expired certificates")
   365  
   366  	// We should have examined exactly one certificate
   367  	certsExamined := testCtx.m.stats.certificatesExamined
   368  	test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0)
   369  
   370  	certsAlreadyRenewed := testCtx.m.stats.certificatesAlreadyRenewed
   371  	test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 1.0)
   372  
   373  	// Run findExpiringCertificates again. The count of examined certificates
   374  	// should not increase again.
   375  	err = testCtx.m.findExpiringCertificates(ctx)
   376  	test.AssertNotError(t, err, "finding expired certificates")
   377  	test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0)
   378  	test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 1.0)
   379  }
   380  
   381  func TestProcessCertsParallel(t *testing.T) {
   382  	expiresIn := time.Hour * 24 * 7
   383  	testCtx := setup(t, []time.Duration{expiresIn})
   384  
   385  	testCtx.m.parallelSends = 2
   386  	certs := addExpiringCerts(t, testCtx)
   387  	err := testCtx.m.processCerts(context.Background(), certs, expiresIn)
   388  	test.AssertNotError(t, err, "processing certs")
   389  	// Test that the lastExpirationNagSent was updated for the certificate
   390  	// corresponding to serial4, which is set up as "already renewed" by
   391  	// addExpiringCerts.
   392  	if len(testCtx.log.GetAllMatching("UPDATE certificateStatus.*000000000000000000000000000000001339")) != 1 {
   393  		t.Errorf("Expected an update to certificateStatus, got these log lines:\n%s",
   394  			strings.Join(testCtx.log.GetAll(), "\n"))
   395  	}
   396  }
   397  
   398  type erroringMailClient struct{}
   399  
   400  func (e erroringMailClient) Connect() (bmail.Conn, error) {
   401  	return nil, errors.New("whoopsie-doo")
   402  }
   403  
   404  func TestProcessCertsConnectError(t *testing.T) {
   405  	expiresIn := time.Hour * 24 * 7
   406  	testCtx := setup(t, []time.Duration{expiresIn})
   407  
   408  	testCtx.m.mailer = erroringMailClient{}
   409  	certs := addExpiringCerts(t, testCtx)
   410  	// Checking that this terminates rather than deadlocks
   411  	err := testCtx.m.processCerts(context.Background(), certs, expiresIn)
   412  	test.AssertError(t, err, "processing certs")
   413  }
   414  
   415  func TestFindExpiringCertificates(t *testing.T) {
   416  	testCtx := setup(t, []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7})
   417  
   418  	addExpiringCerts(t, testCtx)
   419  
   420  	err := testCtx.m.findExpiringCertificates(context.Background())
   421  	test.AssertNotError(t, err, "Failed on no certificates")
   422  	test.AssertEquals(t, len(testCtx.log.GetAllMatching("Searching for certificates that expire between.*")), 3)
   423  
   424  	err = testCtx.m.findExpiringCertificates(context.Background())
   425  	test.AssertNotError(t, err, "Failed to find expiring certs")
   426  	// Should get 001 and 003
   427  	if len(testCtx.mc.Messages) != 2 {
   428  		builder := new(strings.Builder)
   429  		for _, m := range testCtx.mc.Messages {
   430  			fmt.Fprintf(builder, "%s\n", m)
   431  		}
   432  		t.Fatalf("Expected two messages when finding expiring certificates, got:\n%s",
   433  			builder.String())
   434  	}
   435  
   436  	test.AssertEquals(t, testCtx.mc.Messages[0], mocks.MailerMessage{
   437  		To: emailARaw,
   438  		// A certificate with only one domain should have only one domain listed in
   439  		// the subject
   440  		Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"example-a.com\"",
   441  		Body:    "hi, cert for DNS names example-a.com is going to expire in 0 days (1970-01-01)",
   442  	})
   443  	test.AssertEquals(t, testCtx.mc.Messages[1], mocks.MailerMessage{
   444  		To: emailBRaw,
   445  		// A certificate with two domains should have only one domain listed and an
   446  		// additional count included
   447  		Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"another.example-c.com\" (and 1 more)",
   448  		Body:    "hi, cert for DNS names another.example-c.com\nexample-c.com is going to expire in 7 days (1970-01-08)",
   449  	})
   450  
   451  	// Check that regC's only certificate being renewed does not cause a log
   452  	test.AssertEquals(t, len(testCtx.log.GetAllMatching("no certs given to send nags for")), 0)
   453  
   454  	// A consecutive run shouldn't find anything
   455  	testCtx.mc.Clear()
   456  	err = testCtx.m.findExpiringCertificates(context.Background())
   457  	test.AssertNotError(t, err, "Failed to find expiring certs")
   458  	test.AssertEquals(t, len(testCtx.mc.Messages), 0)
   459  	test.AssertMetricWithLabelsEquals(t, testCtx.m.stats.sendDelay, prometheus.Labels{"nag_group": "48h0m0s"}, 90000)
   460  	test.AssertMetricWithLabelsEquals(t, testCtx.m.stats.sendDelay, prometheus.Labels{"nag_group": "192h0m0s"}, 82800)
   461  }
   462  
   463  func makeRegistration(sac sapb.StorageAuthorityClient, id int64, jsonKey []byte, contacts []string) (*corepb.Registration, error) {
   464  	var ip [4]byte
   465  	_, err := rand.Reader.Read(ip[:])
   466  	if err != nil {
   467  		return nil, err
   468  	}
   469  	ipText, err := net.IP(ip[:]).MarshalText()
   470  	if err != nil {
   471  		return nil, fmt.Errorf("formatting IP address: %s", err)
   472  	}
   473  	reg, err := sac.NewRegistration(context.Background(), &corepb.Registration{
   474  		Id:        id,
   475  		Contact:   contacts,
   476  		Key:       jsonKey,
   477  		InitialIP: ipText,
   478  	})
   479  	if err != nil {
   480  		return nil, fmt.Errorf("storing registration: %s", err)
   481  	}
   482  	return reg, nil
   483  }
   484  
   485  func makeCertificate(regID int64, serial *big.Int, dnsNames []string, expires time.Duration, fc clock.FakeClock) (certDERWithRegID, error) {
   486  	// Expires in <1d, last nag was the 4d nag
   487  	template := &x509.Certificate{
   488  		NotAfter:     fc.Now().Add(expires),
   489  		DNSNames:     dnsNames,
   490  		SerialNumber: serial,
   491  	}
   492  	certDer, err := x509.CreateCertificate(rand.Reader, template, template, &testKey.PublicKey, testKey)
   493  	if err != nil {
   494  		return certDERWithRegID{}, err
   495  	}
   496  	return certDERWithRegID{
   497  		RegID: regID,
   498  		DER:   certDer,
   499  	}, nil
   500  }
   501  
   502  func insertCertificate(cert certDERWithRegID, lastNagSent time.Time) error {
   503  	ctx := context.Background()
   504  
   505  	parsedCert, err := x509.ParseCertificate(cert.DER)
   506  	if err != nil {
   507  		return err
   508  	}
   509  
   510  	setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
   511  	if err != nil {
   512  		return err
   513  	}
   514  	err = setupDBMap.Insert(ctx, &core.Certificate{
   515  		RegistrationID: cert.RegID,
   516  		Serial:         core.SerialToString(parsedCert.SerialNumber),
   517  		Issued:         parsedCert.NotBefore,
   518  		Expires:        parsedCert.NotAfter,
   519  		DER:            cert.DER,
   520  	})
   521  	if err != nil {
   522  		return fmt.Errorf("inserting certificate: %w", err)
   523  	}
   524  
   525  	return setupDBMap.Insert(ctx, &core.CertificateStatus{
   526  		Serial:                core.SerialToString(parsedCert.SerialNumber),
   527  		LastExpirationNagSent: lastNagSent,
   528  		Status:                core.OCSPStatusGood,
   529  		NotAfter:              parsedCert.NotAfter,
   530  		OCSPLastUpdated:       time.Time{},
   531  		RevokedDate:           time.Time{},
   532  		RevokedReason:         0,
   533  	})
   534  }
   535  
   536  func addExpiringCerts(t *testing.T, ctx *testCtx) []certDERWithRegID {
   537  	// Add some expiring certificates and registrations
   538  	regA, err := makeRegistration(ctx.ssa, 1, jsonKeyA, []string{emailA})
   539  	test.AssertNotError(t, err, "Couldn't store regA")
   540  	regB, err := makeRegistration(ctx.ssa, 2, jsonKeyB, []string{emailB})
   541  	test.AssertNotError(t, err, "Couldn't store regB")
   542  	regC, err := makeRegistration(ctx.ssa, 3, jsonKeyC, []string{emailB})
   543  	test.AssertNotError(t, err, "Couldn't store regC")
   544  
   545  	// Expires in <1d, last nag was the 4d nag
   546  	certA, err := makeCertificate(
   547  		regA.Id,
   548  		serial1,
   549  		[]string{"example-a.com"},
   550  		23*time.Hour,
   551  		ctx.fc)
   552  	test.AssertNotError(t, err, "creating cert A")
   553  
   554  	// Expires in 3d, already sent 4d nag at 4.5d
   555  	certB, err := makeCertificate(
   556  		regA.Id,
   557  		serial2,
   558  		[]string{"example-b.com"},
   559  		72*time.Hour,
   560  		ctx.fc)
   561  	test.AssertNotError(t, err, "creating cert B")
   562  
   563  	// Expires in 7d and change, no nag sent at all yet
   564  	certC, err := makeCertificate(
   565  		regB.Id,
   566  		serial3,
   567  		[]string{"example-c.com", "another.example-c.com"},
   568  		(7*24+1)*time.Hour,
   569  		ctx.fc)
   570  	test.AssertNotError(t, err, "creating cert C")
   571  
   572  	// Expires in 3d, renewed
   573  	certDNames := []string{"example-d.com"}
   574  	certD, err := makeCertificate(
   575  		regC.Id,
   576  		serial4,
   577  		certDNames,
   578  		72*time.Hour,
   579  		ctx.fc)
   580  	test.AssertNotError(t, err, "creating cert D")
   581  
   582  	fqdnStatusD := &core.FQDNSet{
   583  		SetHash: core.HashNames(certDNames),
   584  		Serial:  serial4String,
   585  		Issued:  ctx.fc.Now().AddDate(0, 0, -87),
   586  		Expires: ctx.fc.Now().AddDate(0, 0, 3),
   587  	}
   588  	fqdnStatusDRenewed := &core.FQDNSet{
   589  		SetHash: core.HashNames(certDNames),
   590  		Serial:  serial5String,
   591  		Issued:  ctx.fc.Now().AddDate(0, 0, -3),
   592  		Expires: ctx.fc.Now().AddDate(0, 0, 87),
   593  	}
   594  
   595  	err = insertCertificate(certA, ctx.fc.Now().Add(-72*time.Hour))
   596  	test.AssertNotError(t, err, "inserting certA")
   597  	err = insertCertificate(certB, ctx.fc.Now().Add(-36*time.Hour))
   598  	test.AssertNotError(t, err, "inserting certB")
   599  	err = insertCertificate(certC, ctx.fc.Now().Add(-36*time.Hour))
   600  	test.AssertNotError(t, err, "inserting certC")
   601  	err = insertCertificate(certD, ctx.fc.Now().Add(-36*time.Hour))
   602  	test.AssertNotError(t, err, "inserting certD")
   603  
   604  	setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
   605  	test.AssertNotError(t, err, "setting up DB")
   606  	err = setupDBMap.Insert(context.Background(), fqdnStatusD)
   607  	test.AssertNotError(t, err, "Couldn't add fqdnStatusD")
   608  	err = setupDBMap.Insert(context.Background(), fqdnStatusDRenewed)
   609  	test.AssertNotError(t, err, "Couldn't add fqdnStatusDRenewed")
   610  	return []certDERWithRegID{certA, certB, certC, certD}
   611  }
   612  
   613  func countGroupsAtCapacity(group string, counter *prometheus.GaugeVec) int {
   614  	ch := make(chan prometheus.Metric, 10)
   615  	counter.With(prometheus.Labels{"nag_group": group}).Collect(ch)
   616  	m := <-ch
   617  	var iom io_prometheus_client.Metric
   618  	_ = m.Write(&iom)
   619  	return int(iom.Gauge.GetValue())
   620  }
   621  
   622  func TestFindCertsAtCapacity(t *testing.T) {
   623  	testCtx := setup(t, []time.Duration{time.Hour * 24})
   624  
   625  	addExpiringCerts(t, testCtx)
   626  
   627  	// Set the limit to 1 so we are "at capacity" with one result
   628  	testCtx.m.certificatesPerTick = 1
   629  
   630  	err := testCtx.m.findExpiringCertificates(context.Background())
   631  	test.AssertNotError(t, err, "Failed to find expiring certs")
   632  	test.AssertEquals(t, len(testCtx.mc.Messages), 1)
   633  
   634  	// The "48h0m0s" nag group should have its prometheus stat incremented once.
   635  	// Note: this is not the 24h0m0s nag as you would expect sending time.Hour
   636  	// * 24 to setup() for the nag duration. This is because all of the nags are
   637  	// offset by 24 hours in this test file's setup() function, to mimic a 24h
   638  	// setting for the "Frequency" field in the JSON config.
   639  	test.AssertEquals(t, countGroupsAtCapacity("48h0m0s", testCtx.m.stats.nagsAtCapacity), 1)
   640  
   641  	// A consecutive run shouldn't find anything
   642  	testCtx.mc.Clear()
   643  	err = testCtx.m.findExpiringCertificates(context.Background())
   644  	test.AssertNotError(t, err, "Failed to find expiring certs")
   645  	test.AssertEquals(t, len(testCtx.mc.Messages), 0)
   646  
   647  	// The "48h0m0s" nag group should now be reporting that it isn't at capacity
   648  	test.AssertEquals(t, countGroupsAtCapacity("48h0m0s", testCtx.m.stats.nagsAtCapacity), 0)
   649  }
   650  
   651  func TestCertIsRenewed(t *testing.T) {
   652  	testCtx := setup(t, []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7})
   653  
   654  	reg := satest.CreateWorkingRegistration(t, testCtx.ssa)
   655  
   656  	testCerts := []*struct {
   657  		Serial       *big.Int
   658  		stringSerial string
   659  		DNS          []string
   660  		NotBefore    time.Time
   661  		NotAfter     time.Time
   662  		// this field is the test assertion
   663  		IsRenewed bool
   664  	}{
   665  		{
   666  			Serial:    serial1,
   667  			DNS:       []string{"a.example.com", "a2.example.com"},
   668  			NotBefore: testCtx.fc.Now().Add((-1 * 24) * time.Hour),
   669  			NotAfter:  testCtx.fc.Now().Add((89 * 24) * time.Hour),
   670  			IsRenewed: true,
   671  		},
   672  		{
   673  			Serial:    serial2,
   674  			DNS:       []string{"a.example.com", "a2.example.com"},
   675  			NotBefore: testCtx.fc.Now().Add((0 * 24) * time.Hour),
   676  			NotAfter:  testCtx.fc.Now().Add((90 * 24) * time.Hour),
   677  			IsRenewed: false,
   678  		},
   679  		{
   680  			Serial:    serial3,
   681  			DNS:       []string{"b.example.net"},
   682  			NotBefore: testCtx.fc.Now().Add((0 * 24) * time.Hour),
   683  			NotAfter:  testCtx.fc.Now().Add((90 * 24) * time.Hour),
   684  			IsRenewed: false,
   685  		},
   686  		{
   687  			Serial:    serial4,
   688  			DNS:       []string{"c.example.org"},
   689  			NotBefore: testCtx.fc.Now().Add((-100 * 24) * time.Hour),
   690  			NotAfter:  testCtx.fc.Now().Add((-10 * 24) * time.Hour),
   691  			IsRenewed: true,
   692  		},
   693  		{
   694  			Serial:    serial5,
   695  			DNS:       []string{"c.example.org"},
   696  			NotBefore: testCtx.fc.Now().Add((-80 * 24) * time.Hour),
   697  			NotAfter:  testCtx.fc.Now().Add((10 * 24) * time.Hour),
   698  			IsRenewed: true,
   699  		},
   700  		{
   701  			Serial:    serial6,
   702  			DNS:       []string{"c.example.org"},
   703  			NotBefore: testCtx.fc.Now().Add((-75 * 24) * time.Hour),
   704  			NotAfter:  testCtx.fc.Now().Add((15 * 24) * time.Hour),
   705  			IsRenewed: true,
   706  		},
   707  		{
   708  			Serial:    serial7,
   709  			DNS:       []string{"c.example.org"},
   710  			NotBefore: testCtx.fc.Now().Add((-1 * 24) * time.Hour),
   711  			NotAfter:  testCtx.fc.Now().Add((89 * 24) * time.Hour),
   712  			IsRenewed: false,
   713  		},
   714  		{
   715  			Serial:    serial8,
   716  			DNS:       []string{"d.example.com", "d2.example.com"},
   717  			NotBefore: testCtx.fc.Now().Add((-1 * 24) * time.Hour),
   718  			NotAfter:  testCtx.fc.Now().Add((89 * 24) * time.Hour),
   719  			IsRenewed: false,
   720  		},
   721  		{
   722  			Serial:    serial9,
   723  			DNS:       []string{"d.example.com", "d2.example.com", "d3.example.com"},
   724  			NotBefore: testCtx.fc.Now().Add((0 * 24) * time.Hour),
   725  			NotAfter:  testCtx.fc.Now().Add((90 * 24) * time.Hour),
   726  			IsRenewed: false,
   727  		},
   728  	}
   729  
   730  	setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
   731  	if err != nil {
   732  		t.Fatal(err)
   733  	}
   734  
   735  	for _, testData := range testCerts {
   736  		testData.stringSerial = core.SerialToString(testData.Serial)
   737  
   738  		rawCert := x509.Certificate{
   739  			NotBefore:    testData.NotBefore,
   740  			NotAfter:     testData.NotAfter,
   741  			DNSNames:     testData.DNS,
   742  			SerialNumber: testData.Serial,
   743  		}
   744  		// Can't use makeCertificate here because we also care about NotBefore
   745  		certDer, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
   746  		if err != nil {
   747  			t.Fatal(err)
   748  		}
   749  		fqdnStatus := &core.FQDNSet{
   750  			SetHash: core.HashNames(testData.DNS),
   751  			Serial:  testData.stringSerial,
   752  			Issued:  testData.NotBefore,
   753  			Expires: testData.NotAfter,
   754  		}
   755  
   756  		err = insertCertificate(certDERWithRegID{DER: certDer, RegID: reg.Id}, time.Time{})
   757  		test.AssertNotError(t, err, fmt.Sprintf("Couldn't add cert %s", testData.stringSerial))
   758  
   759  		err = setupDBMap.Insert(context.Background(), fqdnStatus)
   760  		test.AssertNotError(t, err, fmt.Sprintf("Couldn't add fqdnStatus %s", testData.stringSerial))
   761  	}
   762  
   763  	for _, testData := range testCerts {
   764  		renewed, err := testCtx.m.certIsRenewed(context.Background(), testData.DNS, testData.NotBefore)
   765  		if err != nil {
   766  			t.Errorf("error checking renewal state for %s: %v", testData.stringSerial, err)
   767  			continue
   768  		}
   769  		if renewed != testData.IsRenewed {
   770  			t.Errorf("for %s: got %v, expected %v", testData.stringSerial, renewed, testData.IsRenewed)
   771  		}
   772  	}
   773  }
   774  
   775  func TestLifetimeOfACert(t *testing.T) {
   776  	testCtx := setup(t, []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7})
   777  	defer testCtx.cleanUp()
   778  
   779  	regA, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{emailA})
   780  	test.AssertNotError(t, err, "Couldn't store regA")
   781  
   782  	certA, err := makeCertificate(
   783  		regA.Id,
   784  		serial1,
   785  		[]string{"example-a.com"},
   786  		0,
   787  		testCtx.fc)
   788  	test.AssertNotError(t, err, "making certificate")
   789  
   790  	err = insertCertificate(certA, time.Time{})
   791  	test.AssertNotError(t, err, "unable to insert Certificate")
   792  
   793  	type lifeTest struct {
   794  		timeLeft time.Duration
   795  		numMsgs  int
   796  		context  string
   797  	}
   798  	tests := []lifeTest{
   799  		{
   800  			timeLeft: 9 * 24 * time.Hour, // 9 days before expiration
   801  
   802  			numMsgs: 0,
   803  			context: "Expected no emails sent because we are more than 7 days out.",
   804  		},
   805  		{
   806  			(7*24 + 12) * time.Hour, // 7.5 days before
   807  			1,
   808  			"Sent 1 for 7 day notice.",
   809  		},
   810  		{
   811  			7 * 24 * time.Hour,
   812  			1,
   813  			"The 7 day email was already sent.",
   814  		},
   815  		{
   816  			(4*24 - 1) * time.Hour, // <4 days before, the mailer did not run yesterday
   817  			2,
   818  			"Sent 1 for the 7 day notice, and 1 for the 4 day notice.",
   819  		},
   820  		{
   821  			36 * time.Hour, // within 1day + nagMargin
   822  			3,
   823  			"Sent 1 for the 7 day notice, 1 for the 4 day notice, and 1 for the 1 day notice.",
   824  		},
   825  		{
   826  			12 * time.Hour,
   827  			3,
   828  			"The 1 day before email was already sent.",
   829  		},
   830  		{
   831  			-2 * 24 * time.Hour, // 2 days after expiration
   832  			3,
   833  			"No expiration warning emails are sent after expiration",
   834  		},
   835  	}
   836  
   837  	for _, tt := range tests {
   838  		testCtx.fc.Add(-tt.timeLeft)
   839  		err = testCtx.m.findExpiringCertificates(context.Background())
   840  		test.AssertNotError(t, err, "error calling findExpiringCertificates")
   841  		if len(testCtx.mc.Messages) != tt.numMsgs {
   842  			t.Errorf(tt.context+" number of messages: expected %d, got %d", tt.numMsgs, len(testCtx.mc.Messages))
   843  		}
   844  		testCtx.fc.Add(tt.timeLeft)
   845  	}
   846  }
   847  
   848  func TestDontFindRevokedCert(t *testing.T) {
   849  	expiresIn := 24 * time.Hour
   850  	testCtx := setup(t, []time.Duration{expiresIn})
   851  
   852  	regA, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{"mailto:one@mail.com"})
   853  	test.AssertNotError(t, err, "Couldn't store regA")
   854  	certA, err := makeCertificate(
   855  		regA.Id,
   856  		serial1,
   857  		[]string{"example-a.com"},
   858  		expiresIn,
   859  		testCtx.fc)
   860  	test.AssertNotError(t, err, "making certificate")
   861  
   862  	err = insertCertificate(certA, time.Time{})
   863  	test.AssertNotError(t, err, "inserting certificate")
   864  
   865  	ctx := context.Background()
   866  
   867  	setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
   868  	test.AssertNotError(t, err, "sa.NewDbMap failed")
   869  	_, err = setupDBMap.ExecContext(ctx, "UPDATE certificateStatus SET status = ? WHERE serial = ?",
   870  		string(core.OCSPStatusRevoked), core.SerialToString(serial1))
   871  	test.AssertNotError(t, err, "revoking certificate")
   872  
   873  	err = testCtx.m.findExpiringCertificates(ctx)
   874  	test.AssertNotError(t, err, "err from findExpiringCertificates")
   875  
   876  	if len(testCtx.mc.Messages) != 0 {
   877  		t.Errorf("no emails should have been sent, but sent %d", len(testCtx.mc.Messages))
   878  	}
   879  }
   880  
   881  func TestDedupOnRegistration(t *testing.T) {
   882  	expiresIn := 96 * time.Hour
   883  	testCtx := setup(t, []time.Duration{expiresIn})
   884  
   885  	regA, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{emailA})
   886  	test.AssertNotError(t, err, "Couldn't store regA")
   887  	certA, err := makeCertificate(
   888  		regA.Id,
   889  		serial1,
   890  		[]string{"example-a.com", "shared-example.com"},
   891  		72*time.Hour,
   892  		testCtx.fc)
   893  	test.AssertNotError(t, err, "making certificate")
   894  	err = insertCertificate(certA, time.Time{})
   895  	test.AssertNotError(t, err, "inserting certificate")
   896  
   897  	certB, err := makeCertificate(
   898  		regA.Id,
   899  		serial2,
   900  		[]string{"example-b.com", "shared-example.com"},
   901  		48*time.Hour,
   902  		testCtx.fc)
   903  	test.AssertNotError(t, err, "making certificate")
   904  	err = insertCertificate(certB, time.Time{})
   905  	test.AssertNotError(t, err, "inserting certificate")
   906  
   907  	expires := testCtx.fc.Now().Add(48 * time.Hour)
   908  
   909  	err = testCtx.m.findExpiringCertificates(context.Background())
   910  	test.AssertNotError(t, err, "error calling findExpiringCertificates")
   911  	if len(testCtx.mc.Messages) > 1 {
   912  		t.Errorf("num of messages, want %d, got %d", 1, len(testCtx.mc.Messages))
   913  	}
   914  	if len(testCtx.mc.Messages) == 0 {
   915  		t.Fatalf("no messages sent")
   916  	}
   917  	domains := "example-a.com\nexample-b.com\nshared-example.com"
   918  	test.AssertEquals(t, testCtx.mc.Messages[0], mocks.MailerMessage{
   919  		To: emailARaw,
   920  		// A certificate with three domain names should have one in the subject and
   921  		// a count of '2 more' at the end
   922  		Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"example-a.com\" (and 2 more)",
   923  		Body: fmt.Sprintf(`hi, cert for DNS names %s is going to expire in 2 days (%s)`,
   924  			domains,
   925  			expires.Format(time.DateOnly)),
   926  	})
   927  }
   928  
   929  type testCtx struct {
   930  	dbMap   *db.WrappedMap
   931  	ssa     sapb.StorageAuthorityClient
   932  	mc      *mocks.Mailer
   933  	fc      clock.FakeClock
   934  	m       *mailer
   935  	log     *blog.Mock
   936  	cleanUp func()
   937  }
   938  
   939  func setup(t *testing.T, nagTimes []time.Duration) *testCtx {
   940  	log := blog.NewMock()
   941  
   942  	// We use the test_setup user (which has full permissions to everything)
   943  	// because the SA we return is used for inserting data to set up the test.
   944  	dbMap, err := sa.DBMapForTestWithLog(vars.DBConnSAFullPerms, log)
   945  	if err != nil {
   946  		t.Fatalf("Couldn't connect the database: %s", err)
   947  	}
   948  
   949  	fc := clock.NewFake()
   950  	ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, 1, 0, fc, log, metrics.NoopRegisterer)
   951  	if err != nil {
   952  		t.Fatalf("unable to create SQLStorageAuthority: %s", err)
   953  	}
   954  	cleanUp := test.ResetBoulderTestDatabase(t)
   955  
   956  	mc := &mocks.Mailer{}
   957  
   958  	offsetNags := make([]time.Duration, len(nagTimes))
   959  	for i, t := range nagTimes {
   960  		offsetNags[i] = t + 24*time.Hour
   961  	}
   962  
   963  	m := &mailer{
   964  		log:                 log,
   965  		mailer:              mc,
   966  		emailTemplate:       tmpl,
   967  		subjectTemplate:     subjTmpl,
   968  		dbMap:               dbMap,
   969  		rs:                  isa.SA{Impl: ssa},
   970  		nagTimes:            offsetNags,
   971  		addressLimiter:      &limiter{clk: fc, limit: 4},
   972  		certificatesPerTick: 100,
   973  		clk:                 fc,
   974  		stats:               initStats(metrics.NoopRegisterer),
   975  	}
   976  	return &testCtx{
   977  		dbMap:   dbMap,
   978  		ssa:     isa.SA{Impl: ssa},
   979  		mc:      mc,
   980  		fc:      fc,
   981  		m:       m,
   982  		log:     log,
   983  		cleanUp: cleanUp,
   984  	}
   985  }
   986  
   987  func TestLimiter(t *testing.T) {
   988  	clk := clock.NewFake()
   989  	lim := &limiter{clk: clk, limit: 4}
   990  	fooAtExample := "foo@example.com"
   991  	lim.inc(fooAtExample)
   992  	test.AssertNotError(t, lim.check(fooAtExample), "expected no error")
   993  	lim.inc(fooAtExample)
   994  	test.AssertNotError(t, lim.check(fooAtExample), "expected no error")
   995  	lim.inc(fooAtExample)
   996  	test.AssertNotError(t, lim.check(fooAtExample), "expected no error")
   997  	lim.inc(fooAtExample)
   998  	test.AssertError(t, lim.check(fooAtExample), "expected an error")
   999  
  1000  	clk.Sleep(time.Hour)
  1001  	test.AssertError(t, lim.check(fooAtExample), "expected an error")
  1002  
  1003  	// Sleep long enough to reset the limit
  1004  	clk.Sleep(24 * time.Hour)
  1005  	test.AssertNotError(t, lim.check(fooAtExample), "expected no error")
  1006  }
  1007  

View as plain text