package gamelease_test import ( "context" "testing" "time" "galaxy/rtmanager/internal/adapters/redisstate" "galaxy/rtmanager/internal/adapters/redisstate/gamelease" "github.com/alicebob/miniredis/v2" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newLeaseStore(t *testing.T) (*gamelease.Store, *miniredis.Miniredis) { t.Helper() server := miniredis.RunT(t) client := redis.NewClient(&redis.Options{Addr: server.Addr()}) t.Cleanup(func() { _ = client.Close() }) store, err := gamelease.New(gamelease.Config{Client: client}) require.NoError(t, err) return store, server } func TestNewRejectsNilClient(t *testing.T) { _, err := gamelease.New(gamelease.Config{}) require.Error(t, err) } func TestTryAcquireSetsKeyAndTTL(t *testing.T) { store, server := newLeaseStore(t) acquired, err := store.TryAcquire(context.Background(), "game-1", "token-A", time.Minute) require.NoError(t, err) assert.True(t, acquired) key := redisstate.Keyspace{}.GameLease("game-1") assert.True(t, server.Exists(key), "key %q must exist after TryAcquire", key) stored, err := server.Get(key) require.NoError(t, err) assert.Equal(t, "token-A", stored) // TTL must be positive (miniredis returns the remaining duration). ttl := server.TTL(key) assert.Greater(t, ttl, time.Duration(0)) } func TestTryAcquireReturnsFalseWhenAlreadyHeld(t *testing.T) { store, _ := newLeaseStore(t) acquired, err := store.TryAcquire(context.Background(), "game-1", "token-A", time.Minute) require.NoError(t, err) require.True(t, acquired) acquired, err = store.TryAcquire(context.Background(), "game-1", "token-B", time.Minute) require.NoError(t, err) assert.False(t, acquired) } func TestReleaseRemovesKeyForOwnerToken(t *testing.T) { store, server := newLeaseStore(t) _, err := store.TryAcquire(context.Background(), "game-1", "token-A", time.Minute) require.NoError(t, err) require.NoError(t, store.Release(context.Background(), "game-1", "token-A")) key := redisstate.Keyspace{}.GameLease("game-1") assert.False(t, server.Exists(key), "key %q must be deleted after Release", key) } func TestReleaseIsNoOpForForeignToken(t *testing.T) { store, server := newLeaseStore(t) _, err := store.TryAcquire(context.Background(), "game-1", "token-A", time.Minute) require.NoError(t, err) require.NoError(t, store.Release(context.Background(), "game-1", "token-B")) key := redisstate.Keyspace{}.GameLease("game-1") assert.True(t, server.Exists(key), "key %q must still exist when foreign token is released", key) stored, err := server.Get(key) require.NoError(t, err) assert.Equal(t, "token-A", stored) } func TestTryAcquireSucceedsAfterTTLExpiry(t *testing.T) { store, server := newLeaseStore(t) acquired, err := store.TryAcquire(context.Background(), "game-1", "token-A", time.Minute) require.NoError(t, err) require.True(t, acquired) server.FastForward(2 * time.Minute) acquired, err = store.TryAcquire(context.Background(), "game-1", "token-B", time.Minute) require.NoError(t, err) assert.True(t, acquired) } func TestTryAcquireRejectsInvalidArguments(t *testing.T) { store, _ := newLeaseStore(t) _, err := store.TryAcquire(context.Background(), "", "token", time.Minute) require.Error(t, err) _, err = store.TryAcquire(context.Background(), "game-1", "", time.Minute) require.Error(t, err) _, err = store.TryAcquire(context.Background(), "game-1", "token", 0) require.Error(t, err) } func TestReleaseRejectsInvalidArguments(t *testing.T) { store, _ := newLeaseStore(t) require.Error(t, store.Release(context.Background(), "", "token")) require.Error(t, store.Release(context.Background(), "game-1", "")) } func TestKeyspaceGameLeaseIsPrefixedAndEncoded(t *testing.T) { key := redisstate.Keyspace{}.GameLease("game with spaces") assert.NotEmpty(t, key) assert.Contains(t, key, "rtmanager:game_lease:") suffix := key[len("rtmanager:game_lease:"):] // base64url-encoded suffix must not contain the original spaces. assert.NotContains(t, suffix, " ") }