package cache import ( "context" "fmt" "os" "testing" "time" ) // TestDisabledFailsOpen verifies a Cache without a Redis backend never panics, // always misses, and silently drops writes. func TestDisabledFailsOpen(t *testing.T) { c := New("not-a-valid-url") // parse error => disabled if c.Enabled() { t.Fatal("expected cache to be disabled for invalid url") } c.SetJSON(context.Background(), "k", map[string]int{"a": 1}, time.Minute) var dst map[string]int if c.GetJSON(context.Background(), "k", &dst) { t.Fatalf("disabled cache must always miss, got %+v", dst) } } func testCache(t *testing.T) *Cache { t.Helper() url := os.Getenv("OPENGOODS_REDIS_URL") if url == "" { url = "redis://localhost:6379/0" } c := New(url) if !c.Enabled() { t.Skip("redis not configured") } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if err := c.rdb.Ping(ctx).Err(); err != nil { t.Skipf("redis not reachable: %v", err) } return c } // TestRoundTrip stores then reads a value back. func TestRoundTrip(t *testing.T) { c := testCache(t) ctx := context.Background() suffix := fmt.Sprintf("test:rt:%d", time.Now().UnixNano()) c.SetJSON(ctx, suffix, map[string]any{"name": "foo", "n": float64(3)}, time.Minute) got := map[string]any{} if !c.GetJSON(ctx, suffix, &got) { t.Fatal("expected cache hit after set") } if got["name"] != "foo" || got["n"] != float64(3) { t.Fatalf("unexpected payload: %+v", got) } } // TestEpochInvalidation verifies that bumping the epoch counter logically drops // every previously cached entry. func TestEpochInvalidation(t *testing.T) { c := testCache(t) ctx := context.Background() suffix := fmt.Sprintf("test:epoch:%d", time.Now().UnixNano()) c.SetJSON(ctx, suffix, map[string]int{"v": 1}, time.Minute) var dst map[string]int if !c.GetJSON(ctx, suffix, &dst) { t.Fatal("expected hit before epoch bump") } // Simulate an ingestion write bumping the global epoch. if err := c.rdb.Incr(ctx, epochKey).Err(); err != nil { t.Fatalf("incr epoch: %v", err) } // Force the process to re-read the epoch rather than use its cached value. c.mu.Lock() c.epochOK = false c.mu.Unlock() if c.GetJSON(ctx, suffix, &dst) { t.Fatal("entry should be invisible after epoch bump") } }