- Deleted the OAuth interface and related types that were previously defined in the `interfaces.go` and `types.go` files, streamlining the codebase. - Updated the `oauth.go` file to integrate user and client providers directly, enhancing the service's functionality and reducing complexity. - Refactored the user information retrieval method to utilize the new user provider structure, ensuring compatibility with the updated architecture.
517 lines
13 KiB
Go
517 lines
13 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|