//go:build integration package repository import ( "errors" "testing" "time" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) type GatewayCacheSuite struct { IntegrationRedisSuite cache service.GatewayCache } func (s *GatewayCacheSuite) SetupTest() { s.IntegrationRedisSuite.SetupTest() s.cache = NewGatewayCache(s.rdb) } func (s *GatewayCacheSuite) TestGetSessionAccountID_Missing() { _, err := s.cache.GetSessionAccountID(s.ctx, 1, "nonexistent") require.True(s.T(), errors.Is(err, redis.Nil), "expected redis.Nil for missing session") } func (s *GatewayCacheSuite) TestSetAndGetSessionAccountID() { sessionID := "s1" accountID := int64(99) groupID := int64(1) sessionTTL := 1 * time.Minute require.NoError(s.T(), s.cache.SetSessionAccountID(s.ctx, groupID, sessionID, accountID, sessionTTL), "SetSessionAccountID") sid, err := s.cache.GetSessionAccountID(s.ctx, groupID, sessionID) require.NoError(s.T(), err, "GetSessionAccountID") require.Equal(s.T(), accountID, sid, "session id mismatch") } func (s *GatewayCacheSuite) TestSessionAccountID_TTL() { sessionID := "s2" accountID := int64(100) groupID := int64(1) sessionTTL := 1 * time.Minute require.NoError(s.T(), s.cache.SetSessionAccountID(s.ctx, groupID, sessionID, accountID, sessionTTL), "SetSessionAccountID") sessionKey := buildSessionKey(groupID, sessionID) ttl, err := s.rdb.TTL(s.ctx, sessionKey).Result() require.NoError(s.T(), err, "TTL sessionKey after Set") s.AssertTTLWithin(ttl, 1*time.Second, sessionTTL) } func (s *GatewayCacheSuite) TestRefreshSessionTTL() { sessionID := "s3" accountID := int64(101) groupID := int64(1) initialTTL := 1 * time.Minute refreshTTL := 3 * time.Minute require.NoError(s.T(), s.cache.SetSessionAccountID(s.ctx, groupID, sessionID, accountID, initialTTL), "SetSessionAccountID") require.NoError(s.T(), s.cache.RefreshSessionTTL(s.ctx, groupID, sessionID, refreshTTL), "RefreshSessionTTL") sessionKey := buildSessionKey(groupID, sessionID) ttl, err := s.rdb.TTL(s.ctx, sessionKey).Result() require.NoError(s.T(), err, "TTL after Refresh") s.AssertTTLWithin(ttl, 1*time.Second, refreshTTL) } func (s *GatewayCacheSuite) TestRefreshSessionTTL_MissingKey() { // RefreshSessionTTL on a missing key should not error (no-op) err := s.cache.RefreshSessionTTL(s.ctx, 1, "missing-session", 1*time.Minute) require.NoError(s.T(), err, "RefreshSessionTTL on missing key should not error") } func (s *GatewayCacheSuite) TestDeleteSessionAccountID() { sessionID := "openai:s4" accountID := int64(102) groupID := int64(1) sessionTTL := 1 * time.Minute require.NoError(s.T(), s.cache.SetSessionAccountID(s.ctx, groupID, sessionID, accountID, sessionTTL), "SetSessionAccountID") require.NoError(s.T(), s.cache.DeleteSessionAccountID(s.ctx, groupID, sessionID), "DeleteSessionAccountID") _, err := s.cache.GetSessionAccountID(s.ctx, groupID, sessionID) require.True(s.T(), errors.Is(err, redis.Nil), "expected redis.Nil after delete") } func (s *GatewayCacheSuite) TestGetSessionAccountID_CorruptedValue() { sessionID := "corrupted" groupID := int64(1) sessionKey := buildSessionKey(groupID, sessionID) // Set a non-integer value require.NoError(s.T(), s.rdb.Set(s.ctx, sessionKey, "not-a-number", 1*time.Minute).Err(), "Set invalid value") _, err := s.cache.GetSessionAccountID(s.ctx, groupID, sessionID) require.Error(s.T(), err, "expected error for corrupted value") require.False(s.T(), errors.Is(err, redis.Nil), "expected parsing error, not redis.Nil") } func (s *GatewayCacheSuite) TestGetCascadeID_Missing() { id, err := s.cache.GetCascadeID(s.ctx, "nonexistent-cascade-key") require.NoError(s.T(), err, "missing cascade key should return nil error") require.Equal(s.T(), "", id, "missing cascade key should return empty string") } func (s *GatewayCacheSuite) TestSetAndGetCascadeID() { key := "ab12cd34" cascadeID := "cascade-uuid-xyz" ttl := 30 * time.Minute require.NoError(s.T(), s.cache.SetCascadeID(s.ctx, key, cascadeID, ttl), "SetCascadeID") got, err := s.cache.GetCascadeID(s.ctx, key) require.NoError(s.T(), err, "GetCascadeID") require.Equal(s.T(), cascadeID, got, "cascade id round-trip mismatch") } func (s *GatewayCacheSuite) TestCascadeID_TTL() { key := "ttl-key" ttl := 30 * time.Minute require.NoError(s.T(), s.cache.SetCascadeID(s.ctx, key, "cid", ttl), "SetCascadeID") redisKey := buildCascadeKey(key) got, err := s.rdb.TTL(s.ctx, redisKey).Result() require.NoError(s.T(), err, "TTL after SetCascadeID") s.AssertTTLWithin(got, 1*time.Second, ttl) } func (s *GatewayCacheSuite) TestDeleteCascadeID() { key := "del-key" require.NoError(s.T(), s.cache.SetCascadeID(s.ctx, key, "cid", 1*time.Minute), "SetCascadeID") require.NoError(s.T(), s.cache.DeleteCascadeID(s.ctx, key), "DeleteCascadeID") got, err := s.cache.GetCascadeID(s.ctx, key) require.NoError(s.T(), err, "GetCascadeID after delete should not error") require.Equal(s.T(), "", got, "deleted cascade key should return empty") } // 验证 cascade key 与 sticky session key 命名空间隔离(前缀不同)。 func (s *GatewayCacheSuite) TestCascadeID_NamespaceIsolation() { commonID := "shared-id" require.NoError(s.T(), s.cache.SetCascadeID(s.ctx, commonID, "cascade-value", 1*time.Minute), "SetCascadeID") require.NoError(s.T(), s.cache.SetSessionAccountID(s.ctx, 1, commonID, 42, 1*time.Minute), "SetSessionAccountID") gotCascade, err := s.cache.GetCascadeID(s.ctx, commonID) require.NoError(s.T(), err, "GetCascadeID") require.Equal(s.T(), "cascade-value", gotCascade, "cascade namespace must not be polluted by sticky session") gotAccount, err := s.cache.GetSessionAccountID(s.ctx, 1, commonID) require.NoError(s.T(), err, "GetSessionAccountID") require.Equal(s.T(), int64(42), gotAccount, "sticky session must not be polluted by cascade write") } func TestGatewayCacheSuite(t *testing.T) { suite.Run(t, new(GatewayCacheSuite)) }