blob: 3d9a2967167a1d0d4d3d32be50e76b47879b5a30 [file] [log] [blame]
// Copyright 2022 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package quota
import (
"context"
"strconv"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/gomodule/redigo/redis"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/common/testing/ftt"
"go.chromium.org/luci/common/testing/truth/assert"
"go.chromium.org/luci/common/testing/truth/should"
pb "go.chromium.org/luci/server/quotabeta/proto"
"go.chromium.org/luci/server/quotabeta/quotaconfig"
"go.chromium.org/luci/server/redisconn"
)
func TestQuota(t *testing.T) {
t.Parallel()
ftt.Run("getInterface", t, func(t *ftt.Test) {
ctx := context.Background()
t.Run("panic", func(t *ftt.Test) {
shouldPanic := func() {
getInterface(ctx)
}
assert.Loosely(t, shouldPanic, should.PanicLike("quotaconfig.Interface implementation not found"))
})
t.Run("ok", func(t *ftt.Test) {
m, err := quotaconfig.NewMemory(ctx, nil)
assert.Loosely(t, err, should.BeNil)
ctx = Use(ctx, m)
assert.Loosely(t, getInterface(ctx), should.NotBeNil)
})
})
ftt.Run("UpdateQuota", t, func(t *ftt.Test) {
s, err := miniredis.Run()
assert.Loosely(t, err, should.BeNil)
defer s.Close()
ctx := redisconn.UsePool(context.Background(), &redis.Pool{
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", s.Addr())
},
})
m, err := quotaconfig.NewMemory(ctx, []*pb.Policy{
{
Name: "quota",
Resources: 5,
Replenishment: 1,
},
{
Name: "quota/${user}",
Resources: 2,
},
})
assert.Loosely(t, err, should.BeNil)
ctx, tc := testclock.UseTime(Use(ctx, m), testclock.TestRecentTimeLocal)
now := strconv.FormatInt(tc.Now().Unix(), 10)
t.Run("empty database", func(t *ftt.Test) {
t.Run("policy not found", func(t *ftt.Test) {
up := map[string]int64{
"fake": 0,
}
assert.Loosely(t, UpdateQuota(ctx, up, nil), should.ErrLike("not found"))
assert.Loosely(t, s.Keys(), should.BeEmpty)
})
t.Run("user not specified", func(t *ftt.Test) {
up := map[string]int64{
"quota/${user}": 0,
}
assert.Loosely(t, UpdateQuota(ctx, up, nil), should.ErrLike("user unspecified"))
assert.Loosely(t, s.Keys(), should.BeEmpty)
})
t.Run("capped", func(t *ftt.Test) {
up := map[string]int64{
"quota/${user}": 1,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
})
t.Run("zero", func(t *ftt.Test) {
up := map[string]int64{
"quota/${user}": 0,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
})
t.Run("debit one", func(t *ftt.Test) {
up := map[string]int64{
"quota/${user}": -1,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("1"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
})
t.Run("debit all", func(t *ftt.Test) {
up := map[string]int64{
"quota/${user}": -2,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("0"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
})
t.Run("debit excessive", func(t *ftt.Test) {
up := map[string]int64{
"quota/${user}": -3,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.Equal(ErrInsufficientQuota))
assert.Loosely(t, s.Keys(), should.BeEmpty)
})
t.Run("debit multiple", func(t *ftt.Test) {
up := map[string]int64{
"quota/${user}": -1,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.Equal(ErrInsufficientQuota))
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("0"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
})
t.Run("atomicity", func(t *ftt.Test) {
t.Run("policy not found", func(t *ftt.Test) {
up := map[string]int64{
"quota": 0,
"quota/${user}": 0,
"fake": 0,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.ErrLike("not found"))
assert.Loosely(t, s.Keys(), should.BeEmpty)
})
t.Run("user not specified", func(t *ftt.Test) {
up := map[string]int64{
"quota": 0,
"quota/${user}": 0,
}
opts := &Options{}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.ErrLike("user unspecified"))
assert.Loosely(t, s.Keys(), should.BeEmpty)
})
t.Run("debit one", func(t *ftt.Test) {
up := map[string]int64{
"quota": -1,
"quota/${user}": -1,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("4"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("1"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
})
t.Run("debit excessive", func(t *ftt.Test) {
up := map[string]int64{
"quota": -1,
"quota/${user}": -3,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.Equal(ErrInsufficientQuota))
assert.Loosely(t, s.Keys(), should.BeEmpty)
})
})
t.Run("idempotence", func(t *ftt.Test) {
t.Run("deduplication", func(t *ftt.Test) {
up := map[string]int64{
"quota/${user}": -1,
}
opts := &Options{
RequestID: "request-id",
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"deduplicationKeys",
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("deduplicationKeys", "request-id"), should.Equal(strconv.FormatInt(testclock.TestRecentTimeLocal.Add(time.Hour).Unix(), 10)))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("1"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
// Ensure update succeeds without modifying the database again.
ctx, _ := testclock.UseTime(ctx, testclock.TestRecentTimeLocal.Add(30*time.Minute))
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"deduplicationKeys",
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("deduplicationKeys", "request-id"), should.Equal(strconv.FormatInt(testclock.TestRecentTimeLocal.Add(time.Hour).Unix(), 10)))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("1"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
})
t.Run("expired", func(t *ftt.Test) {
up := map[string]int64{
"quota/${user}": -1,
}
opts := &Options{
RequestID: "request-id",
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"deduplicationKeys",
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("deduplicationKeys", "request-id"), should.Equal(strconv.FormatInt(testclock.TestRecentTimeLocal.Add(time.Hour).Unix(), 10)))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("1"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
// Ensure update modifies the database after the deduplication deadline.
ctx, tc := testclock.UseTime(ctx, testclock.TestRecentTimeLocal.Add(2*time.Hour))
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"deduplicationKeys",
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("deduplicationKeys", "request-id"), should.Equal(strconv.FormatInt(tc.Now().Add(time.Hour).Unix(), 10)))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("0"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(strconv.FormatInt(tc.Now().Unix(), 10)))
})
t.Run("error", func(t *ftt.Test) {
up := map[string]int64{
"quota/${user}": -10,
}
opts := &Options{
RequestID: "request-id",
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.ErrLike("insufficient quota"))
assert.Loosely(t, s.Keys(), should.Match([]string{
"deduplicationKeys",
}))
assert.Loosely(t, s.HGet("deduplicationKeys", "request-id"), should.Equal("0"))
up = map[string]int64{
"quota/${user}": -1,
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"deduplicationKeys",
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("deduplicationKeys", "request-id"), should.Equal(strconv.FormatInt(testclock.TestRecentTimeLocal.Add(time.Hour).Unix(), 10)))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("1"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
})
})
})
t.Run("existing database", func(t *ftt.Test) {
conn, err := redisconn.Get(ctx)
assert.Loosely(t, err, should.BeNil)
_, err = conn.Do("HINCRBY", "entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources", 2)
assert.Loosely(t, err, should.BeNil)
_, err = conn.Do("HINCRBY", "entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated", tc.Now().Unix())
assert.Loosely(t, err, should.BeNil)
t.Run("capped", func(t *ftt.Test) {
up := map[string]int64{
"quota": 10,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("5"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("credit one", func(t *ftt.Test) {
up := map[string]int64{
"quota": 1,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("3"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("debit one", func(t *ftt.Test) {
up := map[string]int64{
"quota": -1,
}
opts := &Options{}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("1"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("debit excessive", func(t *ftt.Test) {
up := map[string]int64{
"quota": -3,
}
opts := &Options{}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.Equal(ErrInsufficientQuota))
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("atomicity", func(t *ftt.Test) {
t.Run("policy not found", func(t *ftt.Test) {
up := map[string]int64{
"quota": 0,
"quota/${user}": 0,
"fake": 0,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.ErrLike("not found"))
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("user not specified", func(t *ftt.Test) {
up := map[string]int64{
"quota": 0,
"quota/${user}": 0,
}
opts := &Options{}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.ErrLike("user unspecified"))
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("debit one", func(t *ftt.Test) {
up := map[string]int64{
"quota": -1,
"quota/${user}": -1,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("1"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("1"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
})
t.Run("debit excessive", func(t *ftt.Test) {
up := map[string]int64{
"quota": -1,
"quota/${user}": -3,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.Equal(ErrInsufficientQuota))
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
})
t.Run("replenishment", func(t *ftt.Test) {
ctx, tc := testclock.UseTime(ctx, testclock.TestRecentTimeLocal.Add(time.Second))
now := strconv.FormatInt(tc.Now().Unix(), 10)
t.Run("future update", func(t *ftt.Test) {
ctx, _ := testclock.UseTime(ctx, testclock.TestRecentTimeLocal.Add(-1*time.Second))
up := map[string]int64{
"quota": 1,
}
assert.Loosely(t, UpdateQuota(ctx, up, nil), should.ErrLike("last updated in the future"))
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(strconv.FormatInt(testclock.TestRecentTimeLocal.Unix(), 10)))
})
t.Run("distant past update", func(t *ftt.Test) {
ctx, tc := testclock.UseTime(ctx, testclock.TestRecentTimeLocal.Add(60*time.Second))
now := strconv.FormatInt(tc.Now().Unix(), 10)
up := map[string]int64{
"quota": -1,
}
assert.Loosely(t, UpdateQuota(ctx, up, nil), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("4"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("cap", func(t *ftt.Test) {
up := map[string]int64{
"quota": 10,
}
assert.Loosely(t, UpdateQuota(ctx, up, nil), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("5"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("credit one", func(t *ftt.Test) {
up := map[string]int64{
"quota": 1,
}
assert.Loosely(t, UpdateQuota(ctx, up, nil), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("4"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("zero", func(t *ftt.Test) {
up := map[string]int64{
"quota": 0,
}
assert.Loosely(t, UpdateQuota(ctx, up, nil), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("3"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("debit one", func(t *ftt.Test) {
up := map[string]int64{
"quota": -1,
}
assert.Loosely(t, UpdateQuota(ctx, up, nil), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("debit all", func(t *ftt.Test) {
up := map[string]int64{
"quota": -3,
}
assert.Loosely(t, UpdateQuota(ctx, up, nil), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("0"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
})
t.Run("debit excessive", func(t *ftt.Test) {
up := map[string]int64{
"quota": -4,
}
assert.Loosely(t, UpdateQuota(ctx, up, nil), should.Equal(ErrInsufficientQuota))
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(strconv.FormatInt(testclock.TestRecentTimeLocal.Unix(), 10)))
})
t.Run("atomicity", func(t *ftt.Test) {
t.Run("future update", func(t *ftt.Test) {
ctx, _ := testclock.UseTime(ctx, testclock.TestRecentTimeLocal.Add(-1*time.Second))
up := map[string]int64{
"quota": -1,
"quota/${user}": -1,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.ErrLike("last updated in the future"))
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(strconv.FormatInt(testclock.TestRecentTimeLocal.Unix(), 10)))
})
t.Run("debit one", func(t *ftt.Test) {
up := map[string]int64{
"quota": -1,
"quota/${user}": -1,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.BeNil)
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
"entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(now))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "resources"), should.Equal("1"))
assert.Loosely(t, s.HGet("entry:f20c860d2ea007ea2360c6ebe2d943acc8a531412c18ff3bd47ab1449988aa6d", "updated"), should.Equal(now))
})
t.Run("debit excessive", func(t *ftt.Test) {
up := map[string]int64{
"quota": -1,
"quota/${user}": -3,
}
opts := &Options{
User: "user@example.com",
}
assert.Loosely(t, UpdateQuota(ctx, up, opts), should.Equal(ErrInsufficientQuota))
assert.Loosely(t, s.Keys(), should.Match([]string{
"entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7",
}))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "resources"), should.Equal("2"))
assert.Loosely(t, s.HGet("entry:b878a6801d9a9e68b30ed63430bb5e0bddcd984a37a3ee385abc27ff031c7fe7", "updated"), should.Equal(strconv.FormatInt(testclock.TestRecentTimeLocal.Unix(), 10)))
})
})
})
})
})
}