package client import ( "context" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yaoapp/gou/connector" "github.com/yaoapp/gou/store" "github.com/yaoapp/gou/store/badger" "github.com/yaoapp/gou/store/lru" "github.com/yaoapp/yao/openapi/oauth/types" ) // Store configuration for parameterized tests type StoreConfig struct { Name string GetFunc func(*testing.T) store.Store } // Test helpers func getMongoStore(t *testing.T) store.Store { // Skip test if MongoDB is not available host := os.Getenv("MONGO_TEST_HOST") if host == "" { t.Skip("MongoDB not available - set MONGO_TEST_HOST environment variable") } // Create MongoDB store using connector mongoConnector, err := connector.New("mongo", "oauth_test", []byte(`{ "name": "OAuth Test MongoDB", "type": "mongo", "options": { "db": "oauth_test", "hosts": [{ "host": "`+host+`", "port": "`+os.Getenv("MONGO_TEST_PORT")+`", "user": "`+os.Getenv("MONGO_TEST_USER")+`", "pass": "`+os.Getenv("MONGO_TEST_PASS")+`" }] } }`)) require.NoError(t, err) mongoStore, err := store.New(mongoConnector, nil) require.NoError(t, err) return mongoStore } func getBadgerStore(t *testing.T) store.Store { // Create temporary directory for test database tempDir := t.TempDir() dbPath := filepath.Join(tempDir, "test_oauth_badger") badgerStore, err := badger.New(dbPath) require.NoError(t, err) // Clean up on test completion t.Cleanup(func() { badgerStore.Close() }) return badgerStore } func getLRUCache(t *testing.T) store.Store { cache, err := lru.New(1000) require.NoError(t, err) return cache } // Get all available store configurations func getStoreConfigs() []StoreConfig { return []StoreConfig{ {Name: "MongoDB", GetFunc: getMongoStore}, {Name: "Badger", GetFunc: getBadgerStore}, } } func createTestClient(clientID string) *types.ClientInfo { return &types.ClientInfo{ ClientID: clientID, ClientSecret: "secret-" + clientID, ClientName: "Test Client " + clientID, ClientType: types.ClientTypeConfidential, RedirectURIs: []string{"https://example.com/callback"}, GrantTypes: []string{types.GrantTypeAuthorizationCode}, ResponseTypes: []string{types.ResponseTypeCode}, Scope: "openid profile email", CreatedAt: time.Now(), UpdatedAt: time.Now(), } } func TestNewDefaultClient(t *testing.T) { storeConfigs := getStoreConfigs() for _, config := range storeConfigs { t.Run(config.Name, func(t *testing.T) { t.Run("valid options", func(t *testing.T) { store := config.GetFunc(t) cache := getLRUCache(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test:", Store: store, Cache: cache, }) assert.NoError(t, err) assert.NotNil(t, client) assert.Equal(t, "test:", client.prefix) assert.Equal(t, store, client.store) assert.Equal(t, cache, client.cache) }) t.Run("nil options", func(t *testing.T) { client, err := NewDefaultClient(nil) assert.Error(t, err) assert.Nil(t, client) assert.Equal(t, types.ErrInvalidConfiguration, err) }) t.Run("nil store", func(t *testing.T) { client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test:", Store: nil, }) assert.Error(t, err) assert.Nil(t, client) assert.Equal(t, types.ErrStoreMissing, err) }) t.Run("empty prefix uses default", func(t *testing.T) { store := config.GetFunc(t) client, err := NewDefaultClient(&DefaultClientOptions{ Store: store, }) assert.NoError(t, err) assert.NotNil(t, client) assert.Equal(t, "__yao:", client.prefix) }) t.Run("without cache", func(t *testing.T) { store := config.GetFunc(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test:", Store: store, }) assert.NoError(t, err) assert.NotNil(t, client) assert.Nil(t, client.cache) }) }) } } func TestKeyGeneration(t *testing.T) { storeConfigs := getStoreConfigs() for _, config := range storeConfigs { t.Run(config.Name, func(t *testing.T) { store := config.GetFunc(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test:", Store: store, }) require.NoError(t, err) t.Run("client key", func(t *testing.T) { key := client.clientKey("test-client") expected := "test:oauth:client:test-client" assert.Equal(t, expected, key) }) t.Run("client list key", func(t *testing.T) { key := client.clientListKey() expected := "test:oauth:clients" assert.Equal(t, expected, key) }) }) } } func TestCreateClient(t *testing.T) { storeConfigs := getStoreConfigs() for _, config := range storeConfigs { t.Run(config.Name, func(t *testing.T) { ctx := context.Background() t.Run("create client without cache", func(t *testing.T) { store := config.GetFunc(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test1:", Store: store, }) require.NoError(t, err) // Clean up first client.store.Clear() testClient := createTestClient("test-client-1") created, err := client.CreateClient(ctx, testClient) assert.NoError(t, err) assert.NotNil(t, created) assert.Equal(t, testClient.ClientID, created.ClientID) assert.Equal(t, testClient.ClientSecret, created.ClientSecret) assert.NotZero(t, created.CreatedAt) assert.NotZero(t, created.UpdatedAt) // Verify client is in store retrieved, err := client.GetClientByID(ctx, testClient.ClientID) assert.NoError(t, err) assert.Equal(t, testClient.ClientID, retrieved.ClientID) }) t.Run("create client with cache", func(t *testing.T) { store := config.GetFunc(t) cache := getLRUCache(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test2:", Store: store, Cache: cache, }) require.NoError(t, err) // Clean up first client.store.Clear() client.cache.Clear() testClient := createTestClient("test-client-2") created, err := client.CreateClient(ctx, testClient) assert.NoError(t, err) assert.NotNil(t, created) // Verify client is cached key := client.clientKey(testClient.ClientID) cached, ok := client.cache.Get(key) assert.True(t, ok) assert.NotNil(t, cached) }) t.Run("create client with empty ID", func(t *testing.T) { store := config.GetFunc(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test3:", Store: store, }) require.NoError(t, err) testClient := createTestClient("") created, err := client.CreateClient(ctx, testClient) assert.Error(t, err) assert.Nil(t, created) assert.Contains(t, err.Error(), "Client ID is required") }) }) } } func TestGetClientByID(t *testing.T) { storeConfigs := getStoreConfigs() for _, config := range storeConfigs { t.Run(config.Name, func(t *testing.T) { ctx := context.Background() t.Run("get client without cache", func(t *testing.T) { store := config.GetFunc(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test4:", Store: store, }) require.NoError(t, err) // Clean up first client.store.Clear() testClient := createTestClient("test-client-4") // Create client first _, err = client.CreateClient(ctx, testClient) require.NoError(t, err) // Get client retrieved, err := client.GetClientByID(ctx, testClient.ClientID) assert.NoError(t, err) assert.NotNil(t, retrieved) assert.Equal(t, testClient.ClientID, retrieved.ClientID) assert.Equal(t, testClient.ClientSecret, retrieved.ClientSecret) }) t.Run("get client with cache hit", func(t *testing.T) { store := config.GetFunc(t) cache := getLRUCache(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test5:", Store: store, Cache: cache, }) require.NoError(t, err) // Clean up first client.store.Clear() client.cache.Clear() testClient := createTestClient("test-client-5") // Create client first _, err = client.CreateClient(ctx, testClient) require.NoError(t, err) // Get client (should hit cache) retrieved, err := client.GetClientByID(ctx, testClient.ClientID) assert.NoError(t, err) assert.NotNil(t, retrieved) assert.Equal(t, testClient.ClientID, retrieved.ClientID) }) t.Run("get non-existent client", func(t *testing.T) { store := config.GetFunc(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test6:", Store: store, }) require.NoError(t, err) retrieved, err := client.GetClientByID(ctx, "non-existent") assert.Error(t, err) assert.Nil(t, retrieved) assert.Contains(t, err.Error(), "Client not found") }) }) } } func TestDeleteClient(t *testing.T) { storeConfigs := getStoreConfigs() for _, config := range storeConfigs { t.Run(config.Name, func(t *testing.T) { ctx := context.Background() t.Run("delete client with cache", func(t *testing.T) { store := config.GetFunc(t) cache := getLRUCache(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test7:", Store: store, Cache: cache, }) require.NoError(t, err) // Clean up first client.store.Clear() client.cache.Clear() testClient := createTestClient("test-client-7") // Create client first _, err = client.CreateClient(ctx, testClient) require.NoError(t, err) // Verify client is cached key := client.clientKey(testClient.ClientID) _, ok := client.cache.Get(key) assert.True(t, ok) // Delete client err = client.DeleteClient(ctx, testClient.ClientID) assert.NoError(t, err) // Verify cache is cleared _, ok = client.cache.Get(key) assert.False(t, ok) }) t.Run("delete non-existent client", func(t *testing.T) { store := config.GetFunc(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test8:", Store: store, }) require.NoError(t, err) err = client.DeleteClient(ctx, "non-existent") assert.Error(t, err) assert.Contains(t, err.Error(), "Client not found") }) }) } } func TestValidateClient(t *testing.T) { storeConfigs := getStoreConfigs() for _, config := range storeConfigs { t.Run(config.Name, func(t *testing.T) { store := config.GetFunc(t) client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test9:", Store: store, }) require.NoError(t, err) ctx := context.Background() t.Run("valid client", func(t *testing.T) { testClient := createTestClient("test-client-9") result, err := client.ValidateClient(ctx, testClient) assert.NoError(t, err) assert.True(t, result.Valid) assert.Empty(t, result.Errors) }) t.Run("client without ID", func(t *testing.T) { testClient := createTestClient("") result, err := client.ValidateClient(ctx, testClient) assert.NoError(t, err) assert.False(t, result.Valid) assert.Contains(t, result.Errors, "Client ID is required") }) t.Run("client with invalid type", func(t *testing.T) { testClient := createTestClient("test-client-10") testClient.ClientType = "invalid" result, err := client.ValidateClient(ctx, testClient) assert.NoError(t, err) assert.False(t, result.Valid) assert.Contains(t, result.Errors, "Invalid client type") }) }) } } func TestCacheConsistency(t *testing.T) { storeConfigs := getStoreConfigs() for _, config := range storeConfigs { t.Run(config.Name, func(t *testing.T) { store := config.GetFunc(t) cache := getLRUCache(t) ctx := context.Background() client, err := NewDefaultClient(&DefaultClientOptions{ Prefix: "test10:", Store: store, Cache: cache, }) require.NoError(t, err) // Clean up first client.store.Clear() client.cache.Clear() testClient := createTestClient("test-client-10") // Create client _, err = client.CreateClient(ctx, testClient) require.NoError(t, err) // Verify cache is updated key := client.clientKey(testClient.ClientID) cached, ok := client.cache.Get(key) assert.True(t, ok) assert.NotNil(t, cached) // Update client updateData := &types.ClientInfo{ ClientID: testClient.ClientID, ClientSecret: "updated-secret", ClientName: "Updated Client", ClientType: types.ClientTypeConfidential, RedirectURIs: []string{"https://updated.com/callback"}, GrantTypes: []string{types.GrantTypeAuthorizationCode}, ResponseTypes: []string{types.ResponseTypeCode}, Scope: "openid profile", } _, err = client.UpdateClient(ctx, testClient.ClientID, updateData) require.NoError(t, err) // Verify cache is updated cached, ok = client.cache.Get(key) assert.True(t, ok) cachedClient := cached.(*types.ClientInfo) assert.Equal(t, "Updated Client", cachedClient.ClientName) assert.Equal(t, "updated-secret", cachedClient.ClientSecret) // Delete client err = client.DeleteClient(ctx, testClient.ClientID) require.NoError(t, err) // Verify cache is cleared _, ok = client.cache.Get(key) assert.False(t, ok) }) } }