592 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			592 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
| package ratelimits
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"math/rand/v2"
 | |
| 	"net"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/jmhodges/clock"
 | |
| 	"github.com/prometheus/client_golang/prometheus"
 | |
| 
 | |
| 	"github.com/letsencrypt/boulder/config"
 | |
| 	berrors "github.com/letsencrypt/boulder/errors"
 | |
| 	"github.com/letsencrypt/boulder/metrics"
 | |
| 	"github.com/letsencrypt/boulder/test"
 | |
| )
 | |
| 
 | |
| // tenZeroZeroTwo is overridden in 'testdata/working_override.yml' to have
 | |
| // higher burst and count values.
 | |
| const tenZeroZeroTwo = "10.0.0.2"
 | |
| 
 | |
| // newTestLimiter constructs a new limiter.
 | |
| func newTestLimiter(t *testing.T, s source, clk clock.FakeClock) *Limiter {
 | |
| 	l, err := NewLimiter(clk, s, metrics.NoopRegisterer)
 | |
| 	test.AssertNotError(t, err, "should not error")
 | |
| 	return l
 | |
| }
 | |
| 
 | |
| // newTestTransactionBuilder constructs a new *TransactionBuilder with the
 | |
| // following configuration:
 | |
| //   - 'NewRegistrationsPerIPAddress' burst: 20 count: 20 period: 1s
 | |
| //   - 'NewRegistrationsPerIPAddress:10.0.0.2' burst: 40 count: 40 period: 1s
 | |
| func newTestTransactionBuilder(t *testing.T) *TransactionBuilder {
 | |
| 	c, err := NewTransactionBuilder("testdata/working_default.yml", "testdata/working_override.yml")
 | |
| 	test.AssertNotError(t, err, "should not error")
 | |
| 	return c
 | |
| }
 | |
| 
 | |
| func setup(t *testing.T) (context.Context, map[string]*Limiter, *TransactionBuilder, clock.FakeClock, string) {
 | |
| 	testCtx := context.Background()
 | |
| 	clk := clock.NewFake()
 | |
| 
 | |
| 	// Generate a random IP address to avoid collisions during and between test
 | |
| 	// runs.
 | |
| 	randIP := make(net.IP, 4)
 | |
| 	for i := range 4 {
 | |
| 		randIP[i] = byte(rand.IntN(256))
 | |
| 	}
 | |
| 
 | |
| 	// Construct a limiter for each source.
 | |
| 	return testCtx, map[string]*Limiter{
 | |
| 		"inmem": newInmemTestLimiter(t, clk),
 | |
| 		"redis": newRedisTestLimiter(t, clk),
 | |
| 	}, newTestTransactionBuilder(t), clk, randIP.String()
 | |
| }
 | |
| 
 | |
| func TestLimiter_CheckWithLimitOverrides(t *testing.T) {
 | |
| 	t.Parallel()
 | |
| 	testCtx, limiters, txnBuilder, clk, testIP := setup(t)
 | |
| 	for name, l := range limiters {
 | |
| 		t.Run(name, func(t *testing.T) {
 | |
| 			// Verify our overrideUsageGauge is being set correctly. 0.0 == 0%
 | |
| 			// of the bucket has been consumed.
 | |
| 			test.AssertMetricWithLabelsEquals(t, l.overrideUsageGauge, prometheus.Labels{
 | |
| 				"limit":      NewRegistrationsPerIPAddress.String(),
 | |
| 				"bucket_key": joinWithColon(NewRegistrationsPerIPAddress.EnumString(), tenZeroZeroTwo)}, 0)
 | |
| 
 | |
| 			overriddenBucketKey, err := newIPAddressBucketKey(NewRegistrationsPerIPAddress, net.ParseIP(tenZeroZeroTwo))
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			overriddenLimit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, overriddenBucketKey)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 
 | |
| 			// Attempt to spend all 40 requests, this should succeed.
 | |
| 			overriddenTxn40, err := newTransaction(overriddenLimit, overriddenBucketKey, 40)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err := l.Spend(testCtx, overriddenTxn40)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 
 | |
| 			// Attempting to spend 1 more, this should fail.
 | |
| 			overriddenTxn1, err := newTransaction(overriddenLimit, overriddenBucketKey, 1)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err = l.Spend(testCtx, overriddenTxn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, !d.allowed, "should not be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Second)
 | |
| 
 | |
| 			// Verify our overrideUsageGauge is being set correctly. 1.0 == 100%
 | |
| 			// of the bucket has been consumed.
 | |
| 			test.AssertMetricWithLabelsEquals(t, l.overrideUsageGauge, prometheus.Labels{
 | |
| 				"limit_name": NewRegistrationsPerIPAddress.String(),
 | |
| 				"bucket_key": joinWithColon(NewRegistrationsPerIPAddress.EnumString(), tenZeroZeroTwo)}, 1.0)
 | |
| 
 | |
| 			// Verify our RetryIn is correct. 1 second == 1000 milliseconds and
 | |
| 			// 1000/40 = 25 milliseconds per request.
 | |
| 			test.AssertEquals(t, d.retryIn, time.Millisecond*25)
 | |
| 
 | |
| 			// Wait 50 milliseconds and try again.
 | |
| 			clk.Add(d.retryIn)
 | |
| 
 | |
| 			// We should be allowed to spend 1 more request.
 | |
| 			d, err = l.Spend(testCtx, overriddenTxn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Second)
 | |
| 
 | |
| 			// Wait 1 second for a full bucket reset.
 | |
| 			clk.Add(d.resetIn)
 | |
| 
 | |
| 			// Quickly spend 40 requests in a row.
 | |
| 			for i := range 40 {
 | |
| 				d, err = l.Spend(testCtx, overriddenTxn1)
 | |
| 				test.AssertNotError(t, err, "should not error")
 | |
| 				test.Assert(t, d.allowed, "should be allowed")
 | |
| 				test.AssertEquals(t, d.remaining, int64(39-i))
 | |
| 			}
 | |
| 
 | |
| 			// Attempting to spend 1 more, this should fail.
 | |
| 			d, err = l.Spend(testCtx, overriddenTxn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, !d.allowed, "should not be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Second)
 | |
| 
 | |
| 			// Wait 1 second for a full bucket reset.
 | |
| 			clk.Add(d.resetIn)
 | |
| 
 | |
| 			testIP := net.ParseIP(testIP)
 | |
| 			normalBucketKey, err := newIPAddressBucketKey(NewRegistrationsPerIPAddress, testIP)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			normalLimit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, normalBucketKey)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 
 | |
| 			// Spend the same bucket but in a batch with bucket subject to
 | |
| 			// default limits. This should succeed, but the decision should
 | |
| 			// reflect that of the default bucket.
 | |
| 			defaultTxn1, err := newTransaction(normalLimit, normalBucketKey, 1)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultTxn1})
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(19))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
 | |
| 
 | |
| 			// Refund quota to both buckets. This should succeed, but the
 | |
| 			// decision should reflect that of the default bucket.
 | |
| 			d, err = l.BatchRefund(testCtx, []Transaction{overriddenTxn1, defaultTxn1})
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(20))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Duration(0))
 | |
| 
 | |
| 			// Once more.
 | |
| 			d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultTxn1})
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(19))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
 | |
| 
 | |
| 			// Reset between tests.
 | |
| 			err = l.Reset(testCtx, overriddenBucketKey)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			err = l.Reset(testCtx, normalBucketKey)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 
 | |
| 			// Spend the same bucket but in a batch with a Transaction that is
 | |
| 			// check-only. This should succeed, but the decision should reflect
 | |
| 			// that of the default bucket.
 | |
| 			defaultCheckOnlyTxn1, err := newCheckOnlyTransaction(normalLimit, normalBucketKey, 1)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultCheckOnlyTxn1})
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.AssertEquals(t, d.remaining, int64(19))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
 | |
| 
 | |
| 			// Check the remaining quota of the overridden bucket.
 | |
| 			overriddenCheckOnlyTxn0, err := newCheckOnlyTransaction(overriddenLimit, overriddenBucketKey, 0)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err = l.Check(testCtx, overriddenCheckOnlyTxn0)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.AssertEquals(t, d.remaining, int64(39))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*25)
 | |
| 
 | |
| 			// Check the remaining quota of the default bucket.
 | |
| 			defaultTxn0, err := newTransaction(normalLimit, normalBucketKey, 0)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err = l.Check(testCtx, defaultTxn0)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.AssertEquals(t, d.remaining, int64(20))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Duration(0))
 | |
| 
 | |
| 			// Spend the same bucket but in a batch with a Transaction that is
 | |
| 			// spend-only. This should succeed, but the decision should reflect
 | |
| 			// that of the overridden bucket.
 | |
| 			defaultSpendOnlyTxn1, err := newSpendOnlyTransaction(normalLimit, normalBucketKey, 1)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultSpendOnlyTxn1})
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.AssertEquals(t, d.remaining, int64(38))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
 | |
| 
 | |
| 			// Check the remaining quota of the overridden bucket.
 | |
| 			d, err = l.Check(testCtx, overriddenCheckOnlyTxn0)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.AssertEquals(t, d.remaining, int64(38))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
 | |
| 
 | |
| 			// Check the remaining quota of the default bucket.
 | |
| 			d, err = l.Check(testCtx, defaultTxn0)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.AssertEquals(t, d.remaining, int64(19))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
 | |
| 
 | |
| 			// Once more, but in now the spend-only Transaction will attempt to
 | |
| 			// spend 20 requests. The spend-only Transaction should fail, but
 | |
| 			// the decision should reflect that of the overridden bucket.
 | |
| 			defaultSpendOnlyTxn20, err := newSpendOnlyTransaction(normalLimit, normalBucketKey, 20)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultSpendOnlyTxn20})
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.AssertEquals(t, d.remaining, int64(37))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*75)
 | |
| 
 | |
| 			// Check the remaining quota of the overridden bucket.
 | |
| 			d, err = l.Check(testCtx, overriddenCheckOnlyTxn0)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.AssertEquals(t, d.remaining, int64(37))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*75)
 | |
| 
 | |
| 			// Check the remaining quota of the default bucket.
 | |
| 			d, err = l.Check(testCtx, defaultTxn0)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.AssertEquals(t, d.remaining, int64(19))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
 | |
| 
 | |
| 			// Reset between tests.
 | |
| 			err = l.Reset(testCtx, overriddenBucketKey)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestLimiter_InitializationViaCheckAndSpend(t *testing.T) {
 | |
| 	t.Parallel()
 | |
| 	testCtx, limiters, txnBuilder, _, testIP := setup(t)
 | |
| 	for name, l := range limiters {
 | |
| 		t.Run(name, func(t *testing.T) {
 | |
| 			bucketKey, err := newIPAddressBucketKey(NewRegistrationsPerIPAddress, net.ParseIP(testIP))
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			limit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, bucketKey)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 
 | |
| 			// Check on an empty bucket should return the theoretical next state
 | |
| 			// of that bucket if the cost were spent.
 | |
| 			txn1, err := newTransaction(limit, bucketKey, 1)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err := l.Check(testCtx, txn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(19))
 | |
| 			// Verify our ResetIn timing is correct. 1 second == 1000
 | |
| 			// milliseconds and 1000/20 = 50 milliseconds per request.
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 
 | |
| 			// However, that cost should not be spent yet, a 0 cost check should
 | |
| 			// tell us that we actually have 20 remaining.
 | |
| 			txn0, err := newTransaction(limit, bucketKey, 0)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err = l.Check(testCtx, txn0)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(20))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Duration(0))
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 
 | |
| 			// Reset our bucket.
 | |
| 			err = l.Reset(testCtx, bucketKey)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 
 | |
| 			// Similar to above, but we'll use Spend() to actually initialize
 | |
| 			// the bucket. Spend should return the same result as Check.
 | |
| 			d, err = l.Spend(testCtx, txn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(19))
 | |
| 			// Verify our ResetIn timing is correct. 1 second == 1000
 | |
| 			// milliseconds and 1000/20 = 50 milliseconds per request.
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 
 | |
| 			// However, that cost should not be spent yet, a 0 cost check should
 | |
| 			// tell us that we actually have 19 remaining.
 | |
| 			d, err = l.Check(testCtx, txn0)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(19))
 | |
| 			// Verify our ResetIn is correct. 1 second == 1000 milliseconds and
 | |
| 			// 1000/20 = 50 milliseconds per request.
 | |
| 			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
 | |
| 			test.AssertEquals(t, d.retryIn, time.Duration(0))
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestLimiter_DefaultLimits(t *testing.T) {
 | |
| 	t.Parallel()
 | |
| 	testCtx, limiters, txnBuilder, clk, testIP := setup(t)
 | |
| 	for name, l := range limiters {
 | |
| 		t.Run(name, func(t *testing.T) {
 | |
| 			bucketKey, err := newIPAddressBucketKey(NewRegistrationsPerIPAddress, net.ParseIP(testIP))
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			limit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, bucketKey)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 
 | |
| 			// Attempt to spend all 20 requests, this should succeed.
 | |
| 			txn20, err := newTransaction(limit, bucketKey, 20)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err := l.Spend(testCtx, txn20)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Second)
 | |
| 
 | |
| 			// Attempting to spend 1 more, this should fail.
 | |
| 			txn1, err := newTransaction(limit, bucketKey, 1)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err = l.Spend(testCtx, txn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, !d.allowed, "should not be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Second)
 | |
| 
 | |
| 			// Verify our ResetIn is correct. 1 second == 1000 milliseconds and
 | |
| 			// 1000/20 = 50 milliseconds per request.
 | |
| 			test.AssertEquals(t, d.retryIn, time.Millisecond*50)
 | |
| 
 | |
| 			// Wait 50 milliseconds and try again.
 | |
| 			clk.Add(d.retryIn)
 | |
| 
 | |
| 			// We should be allowed to spend 1 more request.
 | |
| 			d, err = l.Spend(testCtx, txn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Second)
 | |
| 
 | |
| 			// Wait 1 second for a full bucket reset.
 | |
| 			clk.Add(d.resetIn)
 | |
| 
 | |
| 			// Quickly spend 20 requests in a row.
 | |
| 			for i := range 20 {
 | |
| 				d, err = l.Spend(testCtx, txn1)
 | |
| 				test.AssertNotError(t, err, "should not error")
 | |
| 				test.Assert(t, d.allowed, "should be allowed")
 | |
| 				test.AssertEquals(t, d.remaining, int64(19-i))
 | |
| 			}
 | |
| 
 | |
| 			// Attempting to spend 1 more, this should fail.
 | |
| 			d, err = l.Spend(testCtx, txn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, !d.allowed, "should not be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Second)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestLimiter_RefundAndReset(t *testing.T) {
 | |
| 	t.Parallel()
 | |
| 	testCtx, limiters, txnBuilder, clk, testIP := setup(t)
 | |
| 	for name, l := range limiters {
 | |
| 		t.Run(name, func(t *testing.T) {
 | |
| 			bucketKey, err := newIPAddressBucketKey(NewRegistrationsPerIPAddress, net.ParseIP(testIP))
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			limit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, bucketKey)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 
 | |
| 			// Attempt to spend all 20 requests, this should succeed.
 | |
| 			txn20, err := newTransaction(limit, bucketKey, 20)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err := l.Spend(testCtx, txn20)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Second)
 | |
| 
 | |
| 			// Refund 10 requests.
 | |
| 			txn10, err := newTransaction(limit, bucketKey, 10)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err = l.Refund(testCtx, txn10)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.AssertEquals(t, d.remaining, int64(10))
 | |
| 
 | |
| 			// Spend 10 requests, this should succeed.
 | |
| 			d, err = l.Spend(testCtx, txn10)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Second)
 | |
| 
 | |
| 			err = l.Reset(testCtx, bucketKey)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 
 | |
| 			// Attempt to spend 20 more requests, this should succeed.
 | |
| 			d, err = l.Spend(testCtx, txn20)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, d.allowed, "should be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(0))
 | |
| 			test.AssertEquals(t, d.resetIn, time.Second)
 | |
| 
 | |
| 			// Reset to full.
 | |
| 			clk.Add(d.resetIn)
 | |
| 
 | |
| 			// Refund 1 requests above our limit, this should fail.
 | |
| 			txn1, err := newTransaction(limit, bucketKey, 1)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			d, err = l.Refund(testCtx, txn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.Assert(t, !d.allowed, "should not be allowed")
 | |
| 			test.AssertEquals(t, d.remaining, int64(20))
 | |
| 
 | |
| 			// Spend so we can refund.
 | |
| 			_, err = l.Spend(testCtx, txn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 
 | |
| 			// Refund a spendOnly Transaction, which should succeed.
 | |
| 			spendOnlyTxn1, err := newSpendOnlyTransaction(limit, bucketKey, 1)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			_, err = l.Refund(testCtx, spendOnlyTxn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 
 | |
| 			// Spend so we can refund.
 | |
| 			expectedDecision, err := l.Spend(testCtx, txn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 
 | |
| 			// Refund a checkOnly Transaction, which shouldn't error but should
 | |
| 			// return the same TAT as the previous spend.
 | |
| 			checkOnlyTxn1, err := newCheckOnlyTransaction(limit, bucketKey, 1)
 | |
| 			test.AssertNotError(t, err, "txn should be valid")
 | |
| 			newDecision, err := l.Refund(testCtx, checkOnlyTxn1)
 | |
| 			test.AssertNotError(t, err, "should not error")
 | |
| 			test.AssertEquals(t, newDecision.newTAT, expectedDecision.newTAT)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRateLimitError(t *testing.T) {
 | |
| 	t.Parallel()
 | |
| 	now := clock.NewFake().Now()
 | |
| 
 | |
| 	testCases := []struct {
 | |
| 		name            string
 | |
| 		decision        *Decision
 | |
| 		expectedErr     string
 | |
| 		expectedErrType berrors.ErrorType
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "Allowed decision",
 | |
| 			decision: &Decision{
 | |
| 				allowed: true,
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "RegistrationsPerIP limit reached",
 | |
| 			decision: &Decision{
 | |
| 				allowed: false,
 | |
| 				retryIn: 5 * time.Second,
 | |
| 				transaction: Transaction{
 | |
| 					limit: limit{
 | |
| 						name:   NewRegistrationsPerIPAddress,
 | |
| 						Burst:  10,
 | |
| 						Period: config.Duration{Duration: time.Hour},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			expectedErr:     "too many new registrations (10) from this IP address in the last 1h0m0s, retry after 1970-01-01 00:00:05 UTC",
 | |
| 			expectedErrType: berrors.RateLimit,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "RegistrationsPerIPv6Range limit reached",
 | |
| 			decision: &Decision{
 | |
| 				allowed: false,
 | |
| 				retryIn: 10 * time.Second,
 | |
| 				transaction: Transaction{
 | |
| 					limit: limit{
 | |
| 						name:   NewRegistrationsPerIPv6Range,
 | |
| 						Burst:  5,
 | |
| 						Period: config.Duration{Duration: time.Hour},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			expectedErr:     "too many new registrations (5) from this /48 block of IPv6 addresses in the last 1h0m0s, retry after 1970-01-01 00:00:10 UTC",
 | |
| 			expectedErrType: berrors.RateLimit,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "FailedAuthorizationsPerDomainPerAccount limit reached",
 | |
| 			decision: &Decision{
 | |
| 				allowed: false,
 | |
| 				retryIn: 15 * time.Second,
 | |
| 				transaction: Transaction{
 | |
| 					limit: limit{
 | |
| 						name:   FailedAuthorizationsPerDomainPerAccount,
 | |
| 						Burst:  7,
 | |
| 						Period: config.Duration{Duration: time.Hour},
 | |
| 					},
 | |
| 					bucketKey: "4:12345:example.com",
 | |
| 				},
 | |
| 			},
 | |
| 			expectedErr:     "too many failed authorizations (7) for \"example.com\" in the last 1h0m0s, retry after 1970-01-01 00:00:15 UTC",
 | |
| 			expectedErrType: berrors.RateLimit,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "CertificatesPerDomain limit reached",
 | |
| 			decision: &Decision{
 | |
| 				allowed: false,
 | |
| 				retryIn: 20 * time.Second,
 | |
| 				transaction: Transaction{
 | |
| 					limit: limit{
 | |
| 						name:   CertificatesPerDomain,
 | |
| 						Burst:  3,
 | |
| 						Period: config.Duration{Duration: time.Hour},
 | |
| 					},
 | |
| 					bucketKey: "5:example.org",
 | |
| 				},
 | |
| 			},
 | |
| 			expectedErr:     "too many certificates (3) already issued for \"example.org\" in the last 1h0m0s, retry after 1970-01-01 00:00:20 UTC",
 | |
| 			expectedErrType: berrors.RateLimit,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "CertificatesPerDomainPerAccount limit reached",
 | |
| 			decision: &Decision{
 | |
| 				allowed: false,
 | |
| 				retryIn: 20 * time.Second,
 | |
| 				transaction: Transaction{
 | |
| 					limit: limit{
 | |
| 						name:   CertificatesPerDomainPerAccount,
 | |
| 						Burst:  3,
 | |
| 						Period: config.Duration{Duration: time.Hour},
 | |
| 					},
 | |
| 					bucketKey: "6:12345678:example.net",
 | |
| 				},
 | |
| 			},
 | |
| 			expectedErr:     "too many certificates (3) already issued for \"example.net\" in the last 1h0m0s, retry after 1970-01-01 00:00:20 UTC",
 | |
| 			expectedErrType: berrors.RateLimit,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Unknown rate limit name",
 | |
| 			decision: &Decision{
 | |
| 				allowed: false,
 | |
| 				retryIn: 30 * time.Second,
 | |
| 				transaction: Transaction{
 | |
| 					limit: limit{
 | |
| 						name: 9999999,
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			expectedErr:     "cannot generate error for unknown rate limit",
 | |
| 			expectedErrType: berrors.InternalServer,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range testCases {
 | |
| 		t.Run(tc.name, func(t *testing.T) {
 | |
| 			t.Parallel()
 | |
| 			err := tc.decision.Result(now)
 | |
| 			if tc.expectedErr == "" {
 | |
| 				test.AssertNotError(t, err, "expected no error")
 | |
| 			} else {
 | |
| 				test.AssertError(t, err, "expected an error")
 | |
| 				test.AssertContains(t, err.Error(), tc.expectedErr)
 | |
| 				test.AssertErrorIs(t, err, tc.expectedErrType)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |