- miniredis v2.40.0 as a test-only dependency (approved at the Task 4 check) - memory and Redis cache suites, Flush limited to <prefix>cache: - one parity table through surf.MemoryStore, miniredis and real Redis - Install, FromApp and site_cache validation; Postgres after-commit purge - gate: cooler names in the go and docker stages
134 lines
4.0 KiB
Go
134 lines
4.0 KiB
Go
package cooler_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/cooler"
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/testcontainers/testcontainers-go"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
)
|
|
|
|
// realRedis starts redis:7-alpine for the test and returns a client from
|
|
// OpenRedis. It skips under -short.
|
|
func realRedis(t *testing.T) *redis.Client {
|
|
t.Helper()
|
|
if testing.Short() {
|
|
t.Skip("requires testcontainers redis")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
t.Cleanup(cancel)
|
|
ctr, err := testcontainers.Run(ctx, "redis:7-alpine",
|
|
testcontainers.WithExposedPorts("6379/tcp"),
|
|
testcontainers.WithWaitStrategy(wait.ForListeningPort("6379/tcp")),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("redis: testcontainers: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = testcontainers.TerminateContainer(ctr) })
|
|
host, err := ctr.Host(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
port, err := ctr.MappedPort(ctx, "6379/tcp")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client, err := cooler.OpenRedis(ctx, fmt.Sprintf("redis://%s:%s/0", host, port.Port()))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = client.Close() })
|
|
if err := client.Ping(ctx).Err(); err != nil {
|
|
t.Fatalf("redis ping: %v", err)
|
|
}
|
|
return client
|
|
}
|
|
|
|
func TestRateLimitParityDocker(t *testing.T) {
|
|
client := realRedis(t)
|
|
store := cooler.NewRateLimitStore(client, "acme:", nil)
|
|
runParity(t, parityStore{name: "redis", store: store, advance: sleepFor})
|
|
|
|
if got := countAdmitted(cooler.NewRateLimitStore(client, "burst:", nil), 50, 10); got != 10 {
|
|
t.Fatalf("real Redis admitted %d of 50, want 10", got)
|
|
}
|
|
ctx := t.Context()
|
|
if v, err := client.Get(ctx, "burst:rl:burst").Result(); err != nil || v != "10" {
|
|
t.Fatalf("burst counter = %q %v, want 10", v, err)
|
|
}
|
|
if ttl, err := client.PTTL(ctx, "burst:rl:burst").Result(); err != nil || ttl <= 0 || ttl > time.Minute {
|
|
t.Fatalf("burst window TTL = %s %v", ttl, err)
|
|
}
|
|
}
|
|
|
|
func TestRedisCacheDocker(t *testing.T) {
|
|
client := realRedis(t)
|
|
ctx := t.Context()
|
|
c := cooler.NewRedisCache(client, "acme:")
|
|
|
|
if err := c.Set(ctx, "greeting", []byte("hello"), 150*time.Millisecond); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v, ok, err := c.Get(ctx, "greeting"); err != nil || !ok || string(v) != "hello" {
|
|
t.Fatalf("Get = %q %v %v", v, ok, err)
|
|
}
|
|
if ttl, _ := client.PTTL(ctx, "acme:cache:greeting").Result(); ttl <= 0 || ttl > 150*time.Millisecond {
|
|
t.Fatalf("PTTL = %s, want (0, 150ms]", ttl)
|
|
}
|
|
time.Sleep(250 * time.Millisecond)
|
|
if ok, _ := c.Has(ctx, "greeting"); ok {
|
|
t.Fatal("the key outlived its TTL")
|
|
}
|
|
|
|
if err := c.Set(ctx, "views", []byte("1"), time.Minute); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n, err := c.Increment(ctx, "views", 2); err != nil || n != 3 {
|
|
t.Fatalf("Increment = %d %v", n, err)
|
|
}
|
|
if ttl, _ := client.PTTL(ctx, "acme:cache:views").Result(); ttl <= 50*time.Second {
|
|
t.Fatalf("Increment dropped the TTL: %s", ttl)
|
|
}
|
|
calls := 0
|
|
for range 2 {
|
|
v, err := c.Remember(ctx, "remembered", time.Minute, func(context.Context) ([]byte, error) { calls++; return []byte("computed"), nil })
|
|
if err != nil || string(v) != "computed" {
|
|
t.Fatalf("Remember = %q %v", v, err)
|
|
}
|
|
}
|
|
if calls != 1 {
|
|
t.Fatalf("Remember called the callback %d times", calls)
|
|
}
|
|
|
|
// Flush walks SCAN in batches: more keys than one batch, and the keys
|
|
// outside <prefix>cache: survive.
|
|
pipe := client.Pipeline()
|
|
for i := range 1200 {
|
|
pipe.Set(ctx, "acme:cache:bulk"+strconv.Itoa(i), "v", 0)
|
|
}
|
|
survivors := []string{"acme:rl:login|192.0.2.1", "other:cache:greeting", "acme:cachex"}
|
|
for _, k := range survivors {
|
|
pipe.Set(ctx, k, "keep", 0)
|
|
}
|
|
if _, err := pipe.Exec(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c.Flush(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
left, err := client.Keys(ctx, "acme:cache:*").Result()
|
|
if err != nil || len(left) != 0 {
|
|
t.Fatalf("Flush left %d cache keys (%v)", len(left), err)
|
|
}
|
|
for _, k := range survivors {
|
|
if n, err := client.Exists(ctx, k).Result(); err != nil || n != 1 {
|
|
t.Fatalf("Flush removed %q", k)
|
|
}
|
|
}
|
|
}
|