Files
summercms/modules/cooler/docker_test.go
Jakub Zych 78ba385793 test(19.3-05): cooler caches, rate-limit parity, Install and after-commit purge
- 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
2026-10-15 19:49:34 +02:00

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)
}
}
}