package validator import ( "context" "testing" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/zap" ) // MockRedisClient is a mock implementation of RedisClient interface type MockRedisClient struct { mock.Mock } func (m *MockRedisClient) Ping(ctx context.Context) *redis.StatusCmd { args := m.Called(ctx) return args.Get(0).(*redis.StatusCmd) } func (m *MockRedisClient) Get(ctx context.Context, key string) *redis.StringCmd { args := m.Called(ctx, key) return args.Get(0).(*redis.StringCmd) } // TestTokenValidator_Validate tests the token validation functionality func TestTokenValidator_Validate(t *testing.T) { tests := []struct { name string token string setupMock func(*MockRedisClient) wantUser string wantErr bool errType error }{ { name: "valid token", token: "valid-token-123", setupMock: func(m *MockRedisClient) { // Mock Ping success pingCmd := redis.NewStatusCmd(context.Background()) pingCmd.SetVal("PONG") m.On("Ping", mock.Anything).Return(pingCmd) // Mock Get success getCmd := redis.NewStringCmd(context.Background()) getCmd.SetVal("user-789") m.On("Get", mock.Anything, constants.RedisAuthTokenKey("valid-token-123")).Return(getCmd) }, wantUser: "user-789", wantErr: false, }, { name: "expired or invalid token (redis.Nil)", token: "expired-token", setupMock: func(m *MockRedisClient) { // Mock Ping success pingCmd := redis.NewStatusCmd(context.Background()) pingCmd.SetVal("PONG") m.On("Ping", mock.Anything).Return(pingCmd) // Mock Get returns redis.Nil (key not found) getCmd := redis.NewStringCmd(context.Background()) getCmd.SetErr(redis.Nil) m.On("Get", mock.Anything, constants.RedisAuthTokenKey("expired-token")).Return(getCmd) }, wantUser: "", wantErr: true, errType: errors.ErrInvalidToken, }, { name: "Redis unavailable (fail closed)", token: "any-token", setupMock: func(m *MockRedisClient) { // Mock Ping failure pingCmd := redis.NewStatusCmd(context.Background()) pingCmd.SetErr(context.DeadlineExceeded) m.On("Ping", mock.Anything).Return(pingCmd) }, wantUser: "", wantErr: true, errType: errors.ErrRedisUnavailable, }, { name: "context timeout in Redis operations", token: "timeout-token", setupMock: func(m *MockRedisClient) { // Mock Ping success pingCmd := redis.NewStatusCmd(context.Background()) pingCmd.SetVal("PONG") m.On("Ping", mock.Anything).Return(pingCmd) // Mock Get with context timeout error getCmd := redis.NewStringCmd(context.Background()) getCmd.SetErr(context.DeadlineExceeded) m.On("Get", mock.Anything, constants.RedisAuthTokenKey("timeout-token")).Return(getCmd) }, wantUser: "", wantErr: true, }, { name: "empty token", token: "", setupMock: func(m *MockRedisClient) { // Mock Ping success pingCmd := redis.NewStatusCmd(context.Background()) pingCmd.SetVal("PONG") m.On("Ping", mock.Anything).Return(pingCmd) // Mock Get returns redis.Nil for empty token getCmd := redis.NewStringCmd(context.Background()) getCmd.SetErr(redis.Nil) m.On("Get", mock.Anything, constants.RedisAuthTokenKey("")).Return(getCmd) }, wantUser: "", wantErr: true, errType: errors.ErrInvalidToken, }, { name: "Redis returns empty user ID", token: "invalid-user-token", setupMock: func(m *MockRedisClient) { // Mock Ping success pingCmd := redis.NewStatusCmd(context.Background()) pingCmd.SetVal("PONG") m.On("Ping", mock.Anything).Return(pingCmd) // Mock Get returns empty string getCmd := redis.NewStringCmd(context.Background()) getCmd.SetVal("") m.On("Get", mock.Anything, constants.RedisAuthTokenKey("invalid-user-token")).Return(getCmd) }, wantUser: "", wantErr: true, errType: errors.ErrInvalidToken, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create mock Redis client mockRedis := new(MockRedisClient) if tt.setupMock != nil { tt.setupMock(mockRedis) } // Create validator with mock validator := NewTokenValidator(mockRedis, zap.NewNop()) // Call Validate userID, err := validator.Validate(tt.token) // Assert results if tt.wantErr { assert.Error(t, err, "Expected error for test case: %s", tt.name) if tt.errType != nil { assert.ErrorIs(t, err, tt.errType, "Expected specific error type for test case: %s", tt.name) } } else { assert.NoError(t, err, "Expected no error for test case: %s", tt.name) } assert.Equal(t, tt.wantUser, userID, "User ID mismatch for test case: %s", tt.name) // Assert all expectations were met mockRedis.AssertExpectations(t) }) } } // TestTokenValidator_IsAvailable tests the Redis availability check func TestTokenValidator_IsAvailable(t *testing.T) { tests := []struct { name string setupMock func(*MockRedisClient) want bool }{ { name: "Redis is available", setupMock: func(m *MockRedisClient) { pingCmd := redis.NewStatusCmd(context.Background()) pingCmd.SetVal("PONG") m.On("Ping", mock.Anything).Return(pingCmd) }, want: true, }, { name: "Redis is unavailable", setupMock: func(m *MockRedisClient) { pingCmd := redis.NewStatusCmd(context.Background()) pingCmd.SetErr(context.DeadlineExceeded) m.On("Ping", mock.Anything).Return(pingCmd) }, want: false, }, { name: "Redis connection refused", setupMock: func(m *MockRedisClient) { pingCmd := redis.NewStatusCmd(context.Background()) pingCmd.SetErr(assert.AnError) m.On("Ping", mock.Anything).Return(pingCmd) }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create mock Redis client mockRedis := new(MockRedisClient) if tt.setupMock != nil { tt.setupMock(mockRedis) } // Create validator with mock validator := NewTokenValidator(mockRedis, zap.NewNop()) // Call IsAvailable available := validator.IsAvailable() // Assert result assert.Equal(t, tt.want, available, "Availability mismatch for test case: %s", tt.name) // Assert all expectations were met mockRedis.AssertExpectations(t) }) } } // TestTokenValidator_ValidateWithRealTimeout tests with actual context timeout func TestTokenValidator_ValidateWithRealTimeout(t *testing.T) { // This test verifies that the validator uses a 50ms timeout internally // We test this by simulating a timeout error from Redis mockRedis := new(MockRedisClient) // Mock Ping success pingCmd := redis.NewStatusCmd(context.Background()) pingCmd.SetVal("PONG") mockRedis.On("Ping", mock.Anything).Return(pingCmd) // Mock Get with timeout error getCmd := redis.NewStringCmd(context.Background()) getCmd.SetErr(context.DeadlineExceeded) mockRedis.On("Get", mock.Anything, mock.Anything).Return(getCmd) // Create validator with mock validator := NewTokenValidator(mockRedis, zap.NewNop()) // Call Validate (should return timeout error) userID, err := validator.Validate("timeout-token") // Should get timeout error assert.Error(t, err) assert.Equal(t, "", userID) assert.ErrorIs(t, err, context.DeadlineExceeded) mockRedis.AssertExpectations(t) }