github.com/decred/politeia@v1.4.0/politeiawww/legacy/mail/client_test.go (about)

     1  // Copyright (c) 2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package mail
     6  
     7  import (
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/decred/politeia/politeiawww/legacy/user"
    12  	"github.com/google/go-cmp/cmp"
    13  	"github.com/google/uuid"
    14  )
    15  
    16  func TestFilterRecipients(t *testing.T) {
    17  	// Setup test params
    18  	var (
    19  		rateLimit       = 3
    20  		rateLimitPeriod = 100 * time.Second
    21  
    22  		userNoHistory  = uuid.New() // No email history yet
    23  		userUnderLimit = uuid.New() // Under rate limit by more than 1
    24  		userNearLimit  = uuid.New() // Under rate limit by 1
    25  		userAtLimit    = uuid.New() // At rate limit
    26  
    27  		// userAtLimitExpired is a user that is at the email rate limit
    28  		// but the emails were sent in a previous rate limit period,
    29  		// meaning the user's email history should be reset and they
    30  		// should continue to receive emails.
    31  		userAtLimitExpired = uuid.New()
    32  
    33  		emailNoHistory      = "no_history@email.com"
    34  		emailUnderLimit     = "under_limit@email.com"
    35  		emailNearLimit      = "near_limit@email.com"
    36  		emailAtLimit        = "at_limit@email.com"
    37  		emailAtLimitExpired = "at_limit_expired@email.com"
    38  
    39  		// The following timestamps are within the current rate limit
    40  		// period.
    41  		now   = time.Now()
    42  		time1 = now.Unix() - 1 // 1 second in the past
    43  		time2 = now.Unix() - 2 // 2 seconds in the past
    44  		time3 = now.Unix() - 3 // 3 seconds in the past
    45  
    46  		// The following timestamps are expired, meaning they occurred in
    47  		// a previous rate limit period and should not be counted as part
    48  		// of the current rate limit period.
    49  		expired  = now.Add(-rateLimitPeriod)
    50  		expired1 = expired.Unix() - 1 // 1 second expired
    51  		expired2 = expired.Unix() - 2 // 2 seconds expired
    52  		expired3 = expired.Unix() - 3 // 3 seconds expired
    53  
    54  		// histories contains the emails histories that will be seeded
    55  		// in the MailerDB for the test.
    56  		histories = map[uuid.UUID]user.EmailHistory{
    57  			userUnderLimit: {
    58  				Timestamps:       []int64{time1},
    59  				LimitWarningSent: false,
    60  			},
    61  
    62  			userNearLimit: {
    63  				Timestamps:       []int64{time2, time1},
    64  				LimitWarningSent: false,
    65  			},
    66  
    67  			userAtLimit: {
    68  				Timestamps:       []int64{time3, time2, time1},
    69  				LimitWarningSent: true,
    70  			},
    71  
    72  			userAtLimitExpired: {
    73  				Timestamps:       []int64{expired3, expired2, expired1},
    74  				LimitWarningSent: true,
    75  			},
    76  		}
    77  
    78  		// emails contains the email list that will be provided to the
    79  		// filterRecipients function for the test.
    80  		emails = map[uuid.UUID]string{
    81  			userNoHistory:      emailNoHistory,
    82  			userUnderLimit:     emailUnderLimit,
    83  			userNearLimit:      emailNearLimit,
    84  			userAtLimit:        emailAtLimit,
    85  			userAtLimitExpired: emailAtLimitExpired,
    86  		}
    87  	)
    88  
    89  	// Setup test mail client
    90  	c := newTestClient(rateLimit, rateLimitPeriod, histories)
    91  
    92  	// Run test
    93  	fr, err := c.filterRecipients(emails)
    94  	if err != nil {
    95  		t.Error(err)
    96  	}
    97  
    98  	// Put the valid and warning lists into maps for easy verification
    99  	// that a value has been included.
   100  	var (
   101  		valid   = make(map[string]struct{}, len(fr.valid))
   102  		warning = make(map[string]struct{}, len(fr.warning))
   103  	)
   104  	for _, v := range fr.valid {
   105  		valid[v] = struct{}{}
   106  	}
   107  	for _, v := range fr.warning {
   108  		warning[v] = struct{}{}
   109  	}
   110  
   111  	// Verify valid emails list. This should contain the users:
   112  	// noHistory, underLimit, nearLimit, atLimitExpired.
   113  	_, ok := valid[emailNoHistory]
   114  	if !ok {
   115  		t.Errorf("user with no email history was not found in the "+
   116  			"valid emails list: %v", fr.valid)
   117  	}
   118  
   119  	_, ok = valid[emailUnderLimit]
   120  	if !ok {
   121  		t.Errorf("user with email history under the rate limit was "+
   122  			"not found in the valid emails list: %v", fr.valid)
   123  	}
   124  
   125  	_, ok = valid[emailNearLimit]
   126  	if !ok {
   127  		t.Errorf("user with email history under the rate limit by 1 "+
   128  			"was not found in the valid emails list: %v", fr.valid)
   129  	}
   130  
   131  	_, ok = valid[emailAtLimitExpired]
   132  	if !ok {
   133  		t.Errorf("user with email history at the rate limit but expired "+
   134  			"was not found in the valid emails list: %v", fr.valid)
   135  	}
   136  
   137  	if len(fr.valid) != 4 {
   138  		t.Errorf("valid emails list length want 4, got %v: %v",
   139  			len(fr.valid), fr.valid)
   140  	}
   141  
   142  	// Verify warning emails. The only user that hit the rate limit
   143  	// this invocation and thus should be in the warning emails list
   144  	// is the nearLimit user.
   145  	_, ok = warning[emailNearLimit]
   146  	switch {
   147  	case !ok:
   148  		t.Errorf("user that hit the rate limit was not found in the "+
   149  			"warning emails list: %v", fr.warning)
   150  
   151  	case len(fr.warning) != 1:
   152  		t.Errorf("warning emails list length want 1, got %v: %v",
   153  			len(fr.warning), fr.warning)
   154  	}
   155  
   156  	eh, ok := fr.histories[userNoHistory]
   157  	switch {
   158  	case !ok:
   159  		t.Errorf("user with no email history was not found in the " +
   160  			"histories list")
   161  
   162  	case len(eh.Timestamps) != 1:
   163  		t.Errorf("histories length for user with no email history: "+
   164  			"want 1, got %v", len(eh.Timestamps))
   165  
   166  	case eh.LimitWarningSent:
   167  		t.Errorf("limit warning sent for user with no email history: " +
   168  			"want false, got true")
   169  	}
   170  
   171  	// Verify returned email history for underLimit user
   172  	eh, ok = fr.histories[userUnderLimit]
   173  	switch {
   174  	case !ok:
   175  		t.Errorf("user with email history under the rate limit was " +
   176  			"not found in the histories list")
   177  
   178  	case len(eh.Timestamps) != 2:
   179  		t.Errorf("histories length for user under the rate limit: "+
   180  			"want 2, got %v", len(eh.Timestamps))
   181  
   182  	case eh.LimitWarningSent:
   183  		t.Errorf("limit warning sent for user with email history " +
   184  			"under the rate limit: want false, got true")
   185  	}
   186  
   187  	// Verify returned email history for nearLimit user
   188  	eh, ok = fr.histories[userNearLimit]
   189  	switch {
   190  	case !ok:
   191  		t.Errorf("user with email history under the rate limit " +
   192  			"by one was not found in the histories list")
   193  
   194  	case len(eh.Timestamps) != 3:
   195  		t.Errorf("histories length for user under the rate limit "+
   196  			"by one: want 3, got %v", len(eh.Timestamps))
   197  
   198  	case !eh.LimitWarningSent:
   199  		t.Errorf("limit warning sent for user with email history " +
   200  			"under the rate limit by one: want true, got false")
   201  	}
   202  
   203  	// Verify returned email history for atLimitExpired user
   204  	eh, ok = fr.histories[userAtLimitExpired]
   205  	switch {
   206  	case !ok:
   207  		t.Errorf("user with email history at the rate limit but " +
   208  			"expired was not found in the histories list")
   209  
   210  	case len(eh.Timestamps) != 1:
   211  		t.Errorf("histories length for user under the rate limit: "+
   212  			"want 1, got %v", len(eh.Timestamps))
   213  
   214  	case eh.LimitWarningSent:
   215  		t.Errorf("limit warning sent for user with email history " +
   216  			"at the rate limit but expired: want false, got true")
   217  	}
   218  
   219  	// Verify the filtered histories does not contain unexpected
   220  	// histories.
   221  	if len(fr.histories) != 4 {
   222  		t.Errorf("filtered histories length: want 4, got %v",
   223  			len(fr.histories))
   224  	}
   225  }
   226  
   227  func TestFilterTimestamps(t *testing.T) {
   228  	// Timestamps that are expired based on the default rate limit period,
   229  	// and that must not be contained on the output if passed as input.
   230  	minus24h := time.Now().Add(-(24 * time.Hour)).Unix()
   231  	minus26h := time.Now().Add(-(26 * time.Hour)).Unix()
   232  	minus28h := time.Now().Add(-(28 * time.Hour)).Unix()
   233  	minus36h := time.Now().Add(-(36 * time.Hour)).Unix()
   234  	minus42h := time.Now().Add(-(42 * time.Hour)).Unix()
   235  
   236  	// Timestamps that are still valid and within the default rate limit period,
   237  	// and that must be contained on the output if passed as input.
   238  	minus12h := time.Now().Add(-(12 * time.Hour)).Unix()
   239  	minus14h := time.Now().Add(-(14 * time.Hour)).Unix()
   240  	minus16h := time.Now().Add(-(16 * time.Hour)).Unix()
   241  
   242  	// Setup test cases
   243  	var tests = []struct {
   244  		name    string
   245  		in      []int64
   246  		wantOut []int64
   247  	}{
   248  		{
   249  			"remove all timestamps",
   250  			[]int64{minus24h, minus36h, minus42h},
   251  			[]int64{},
   252  		},
   253  		{
   254  			"remove stale timestamps",
   255  			[]int64{minus12h, minus26h, minus28h},
   256  			[]int64{minus12h},
   257  		},
   258  		{
   259  			"no timestamps to remove",
   260  			[]int64{minus12h, minus14h, minus16h},
   261  			[]int64{minus12h, minus14h, minus16h},
   262  		},
   263  	}
   264  
   265  	for _, v := range tests {
   266  		t.Run(v.name, func(t *testing.T) {
   267  			out := filterTimestamps(v.in, defaultRateLimitPeriod)
   268  
   269  			// Verify if the function output matches the expected output.
   270  			diff := cmp.Diff(out, v.wantOut)
   271  			if diff != "" {
   272  				t.Errorf("got/want diff: \n%v", diff)
   273  			}
   274  		})
   275  	}
   276  }
   277  
   278  // newTestClient returns a new client that is setup for testing. The caller can
   279  // optionally provide a list of email histories to seed the testMailerDB with
   280  // on intialization.
   281  func newTestClient(rateLimit int, rateLimitPeriod time.Duration, histories map[uuid.UUID]user.EmailHistory) *client {
   282  	return &client{
   283  		smtp:            nil,
   284  		mailName:        "test",
   285  		mailAddress:     "test@email.com",
   286  		mailerDB:        user.NewTestMailerDB(histories),
   287  		disabled:        false,
   288  		rateLimit:       rateLimit,
   289  		rateLimitPeriod: rateLimitPeriod,
   290  	}
   291  }