Refactor test utilities and enhance access token handling
- Introduced ObtainAccessTokenWithRootPermission function to streamline the creation of test users with root permissions, ensuring consistent access token generation for tests. - Updated various test cases to utilize the new function, improving clarity and reducing redundancy in access token acquisition. - Enhanced team configuration retrieval to expose public settings while hiding sensitive information, improving security in API responses.
This commit is contained in:
parent
f31b3f5882
commit
ddf8fd7a32
16 changed files with 766 additions and 158 deletions
2
Makefile
2
Makefile
|
|
@ -10,7 +10,7 @@ NOW := $(shell date +"%FT%T%z")
|
|||
OS := $(shell uname)
|
||||
|
||||
# ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
TESTFOLDER := $(shell $(GO) list ./... | grep -vE 'examples|tests|openai|aigc|neo|twilio|share*')
|
||||
TESTFOLDER := $(shell $(GO) list ./... | grep -vE 'examples|openai|aigc|neo|twilio|share*' | awk '!/\/tests\// || /openapi\/tests/')
|
||||
TESTTAGS ?= ""
|
||||
|
||||
# TESTWIDGETS := $(shell $(GO) list ./widgets/...)
|
||||
|
|
|
|||
1
openapi/agent/agnet.go
Normal file
1
openapi/agent/agnet.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package agent
|
||||
1
openapi/llm/llm.go
Normal file
1
openapi/llm/llm.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package llm
|
||||
1
openapi/mcp/mcp.go
Normal file
1
openapi/mcp/mcp.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package mcp
|
||||
|
|
@ -46,11 +46,11 @@ func TestCreateCollection(t *testing.T) {
|
|||
"created_by": "test_user",
|
||||
},
|
||||
"config": map[string]interface{}{
|
||||
"embedding_provider": "__yao.openai", // Required: embedding provider ID
|
||||
"embedding_option": "text-embedding-3-small", // Required: embedding option value
|
||||
"locale": "en", // Optional: locale for provider reading
|
||||
"index_type": "hnsw", // Required: valid index type
|
||||
"distance": "cosine", // Required: distance metric
|
||||
"embedding_provider_id": "__yao.openai", // Required: embedding provider ID
|
||||
"embedding_option_id": "text-embedding-3-small", // Required: embedding option value
|
||||
"locale": "en", // Optional: locale for provider reading
|
||||
"index_type": "hnsw", // Required: valid index type
|
||||
"distance": "cosine", // Required: distance metric
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -171,11 +171,11 @@ func TestRemoveCollection(t *testing.T) {
|
|||
"category": "test_remove",
|
||||
},
|
||||
"config": map[string]interface{}{
|
||||
"embedding_provider": "__yao.openai", // Required: embedding provider ID
|
||||
"embedding_option": "text-embedding-3-small", // Required: embedding option value
|
||||
"locale": "en", // Optional: locale for provider reading
|
||||
"index_type": "hnsw", // Required: valid index type
|
||||
"distance": "cosine", // Required: distance metric
|
||||
"embedding_provider_id": "__yao.openai", // Required: embedding provider ID
|
||||
"embedding_option_id": "text-embedding-3-small", // Required: embedding option value
|
||||
"locale": "en", // Optional: locale for provider reading
|
||||
"index_type": "hnsw", // Required: valid index type
|
||||
"distance": "cosine", // Required: distance metric
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -349,11 +349,17 @@ func TestGetCollections(t *testing.T) {
|
|||
// Expect successful response
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully retrieve collections when KB is initialized")
|
||||
|
||||
var collections []interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&collections)
|
||||
var response map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Logf("Successfully retrieved %d collections", len(collections))
|
||||
// Response should have pagination structure
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d collections", len(data))
|
||||
} else {
|
||||
t.Logf("Successfully retrieved collections response (data field type: %T)", response["data"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetCollectionsWithFilter", func(t *testing.T) {
|
||||
|
|
@ -380,11 +386,17 @@ func TestGetCollections(t *testing.T) {
|
|||
// Expect successful response
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully retrieve collections when KB is initialized")
|
||||
|
||||
var collections []interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&collections)
|
||||
var response map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Logf("Successfully retrieved %d filtered collections", len(collections))
|
||||
// Response should have pagination structure
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d filtered collections", len(data))
|
||||
} else {
|
||||
t.Logf("Successfully retrieved filtered collections response (data field type: %T)", response["data"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetCollectionsWithMultipleFilters", func(t *testing.T) {
|
||||
|
|
@ -411,11 +423,17 @@ func TestGetCollections(t *testing.T) {
|
|||
// Expect successful response
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully retrieve collections when KB is initialized")
|
||||
|
||||
var collections []interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&collections)
|
||||
var response map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Logf("Successfully retrieved %d collections with multiple filters", len(collections))
|
||||
// Response should have pagination structure
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d collections with multiple filters", len(data))
|
||||
} else {
|
||||
t.Logf("Successfully retrieved collections with multiple filters response (data field type: %T)", response["data"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -504,11 +522,11 @@ func TestCollectionIntegration(t *testing.T) {
|
|||
"purpose": "full_lifecycle_test",
|
||||
},
|
||||
"config": map[string]interface{}{
|
||||
"embedding_provider": "__yao.openai", // Required: embedding provider ID
|
||||
"embedding_option": "text-embedding-3-small", // Required: embedding option value
|
||||
"locale": "en", // Optional: locale for provider reading
|
||||
"index_type": "hnsw", // Required: valid index type
|
||||
"distance": "cosine", // Required: distance metric
|
||||
"embedding_provider_id": "__yao.openai", // Required: embedding provider ID
|
||||
"embedding_option_id": "text-embedding-3-small", // Required: embedding option value
|
||||
"locale": "en", // Optional: locale for provider reading
|
||||
"index_type": "hnsw", // Required: valid index type
|
||||
"distance": "cosine", // Required: distance metric
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -624,6 +625,130 @@ type TokenInfo struct {
|
|||
UserID string
|
||||
}
|
||||
|
||||
// ObtainAccessTokenWithRootPermission creates a complete test user with root permissions and obtains an access token.
|
||||
// This simulates a real user login flow:
|
||||
// 1. Creates a role with root permissions if it doesn't exist
|
||||
// 2. Creates a real user in the database with this role
|
||||
// 3. Issues a token with system:root scope for full permissions
|
||||
func ObtainAccessTokenWithRootPermission(t *testing.T, serverURL, clientID, clientSecret, redirectURI, scope string) *TokenInfo {
|
||||
testMutex.RLock()
|
||||
server := openapi.Server
|
||||
testMutex.RUnlock()
|
||||
|
||||
if server == nil || server.OAuth == nil {
|
||||
t.Fatal("OpenAPI server not initialized. Call Prepare(t) first.")
|
||||
}
|
||||
|
||||
oauthService := oauth.OAuth
|
||||
if oauthService == nil {
|
||||
t.Fatal("Global OAuth service not initialized")
|
||||
}
|
||||
|
||||
userProvider, err := oauthService.GetUserProvider()
|
||||
if err != nil || userProvider == nil {
|
||||
t.Fatal("UserProvider not available")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Step 1: Ensure system:root role exists in database (delete and recreate if exists)
|
||||
// Note: This is the default client role used by ACL
|
||||
roleID := "system:root"
|
||||
t.Logf("Setting up role %s in database", roleID)
|
||||
|
||||
// Check if role already exists and delete it for clean state
|
||||
_, err = userProvider.GetRole(ctx, roleID)
|
||||
if err == nil {
|
||||
// Role exists, delete it first
|
||||
t.Logf("Role %s already exists, deleting for clean state", roleID)
|
||||
err = userProvider.DeleteRole(ctx, roleID)
|
||||
if err != nil {
|
||||
t.Logf("Warning: Failed to delete existing role: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear role cache to ensure fresh data
|
||||
if oauthService.GetCache() != nil {
|
||||
cache := oauthService.GetCache()
|
||||
// Clear all role-related cache with the correct prefix
|
||||
cache.Del("acl:role:scopes:" + roleID)
|
||||
cache.Del("acl:role:scopes:restricted:" + roleID)
|
||||
t.Logf("Cleared ACL role cache for %s", roleID)
|
||||
}
|
||||
|
||||
// Create the role with system:root permissions
|
||||
// Pass permissions as []string directly
|
||||
roleData := map[string]interface{}{
|
||||
"role_id": roleID,
|
||||
"name": "System Root",
|
||||
"description": "System root role with full system access",
|
||||
"permissions": []string{"system:root"},
|
||||
"is_active": true,
|
||||
}
|
||||
|
||||
_, err = userProvider.CreateRole(ctx, roleData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test role: %v", err)
|
||||
}
|
||||
t.Logf("Successfully created role %s with system:root permissions", roleID)
|
||||
|
||||
// Step 2: Create a real test user in the database
|
||||
testUserID := fmt.Sprintf("test_user_root_%d", time.Now().UnixNano())
|
||||
userData := map[string]interface{}{
|
||||
"user_id": testUserID,
|
||||
"status": "active",
|
||||
"role_id": roleID, // Assign test_root role (which has system:root scope)
|
||||
}
|
||||
|
||||
_, err = userProvider.CreateUser(ctx, userData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test user in database: %v", err)
|
||||
}
|
||||
t.Logf("Created test user %s with role %s in database", testUserID, roleID)
|
||||
|
||||
// Step 3: Create subject (fingerprint) for OAuth
|
||||
subject, err := oauthService.Subject(clientID, testUserID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user subject: %v", err)
|
||||
}
|
||||
t.Logf("Created subject mapping: clientID=%s, userID=%s, subject=%s", clientID, testUserID, subject)
|
||||
|
||||
// Step 4: Create access token with system:root scope for full permissions
|
||||
fullScope := scope
|
||||
if scope != "" && !strings.Contains(scope, "system:root") {
|
||||
fullScope = scope + " system:root"
|
||||
} else if scope == "" {
|
||||
fullScope = "system:root"
|
||||
}
|
||||
|
||||
extraClaims := map[string]interface{}{
|
||||
"user_id": testUserID,
|
||||
}
|
||||
|
||||
accessToken, err := oauthService.MakeAccessToken(clientID, fullScope, subject, 3600, extraClaims)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create access token: %v", err)
|
||||
}
|
||||
|
||||
refreshToken, err := oauthService.MakeRefreshToken(clientID, fullScope, subject, 7200, extraClaims)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create refresh token: %v", err)
|
||||
}
|
||||
|
||||
tokenInfo := &TokenInfo{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
Scope: fullScope,
|
||||
ClientID: clientID,
|
||||
UserID: testUserID,
|
||||
}
|
||||
|
||||
t.Logf("Issued token for user %s with scope: %s", testUserID, fullScope)
|
||||
return tokenInfo
|
||||
}
|
||||
|
||||
// ObtainAccessToken obtains an access token for testing OAuth endpoints that require authentication.
|
||||
func ObtainAccessToken(t *testing.T, serverURL, clientID, clientSecret, redirectURI, scope string) *TokenInfo {
|
||||
testMutex.RLock()
|
||||
|
|
@ -644,37 +769,36 @@ func ObtainAccessToken(t *testing.T, serverURL, clientID, clientSecret, redirect
|
|||
t.Fatal("Global OAuth service not initialized")
|
||||
}
|
||||
|
||||
accessToken, err := oauthService.MakeAccessToken(clientID, scope, subject, 3600)
|
||||
// Step 3: Add system:root to scope for full permissions in tests
|
||||
fullScope := scope
|
||||
if scope != "" && !strings.Contains(scope, "system:root") {
|
||||
fullScope = scope + " system:root"
|
||||
} else if scope == "" {
|
||||
fullScope = "system:root"
|
||||
}
|
||||
|
||||
// Step 4: Create access token with system:root scope
|
||||
accessToken, err := oauthService.MakeAccessToken(clientID, fullScope, subject, 3600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create access token: %v", err)
|
||||
}
|
||||
|
||||
refreshToken, err := oauthService.MakeRefreshToken(clientID, scope, subject, 7200)
|
||||
refreshToken, err := oauthService.MakeRefreshToken(clientID, fullScope, subject, 7200)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create refresh token: %v", err)
|
||||
}
|
||||
|
||||
// Create a synthetic token response
|
||||
token := &types.Token{
|
||||
tokenInfo := &TokenInfo{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
tokenInfo := &TokenInfo{
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
TokenType: token.TokenType,
|
||||
ExpiresIn: token.ExpiresIn,
|
||||
Scope: token.Scope,
|
||||
Scope: fullScope,
|
||||
ClientID: clientID,
|
||||
UserID: testUserID, // Include the test user ID
|
||||
UserID: testUserID,
|
||||
}
|
||||
|
||||
t.Logf("Obtained access token: %s (type: %s, expires_in: %d, user_id: %s)",
|
||||
tokenInfo.AccessToken, tokenInfo.TokenType, tokenInfo.ExpiresIn, tokenInfo.UserID)
|
||||
t.Logf("Obtained access token with scope: %s (user_id: %s)", fullScope, testUserID)
|
||||
return tokenInfo
|
||||
}
|
||||
|
||||
|
|
@ -767,6 +891,41 @@ func ObtainTokenForUser(t *testing.T, clientID, clientSecret, userID, scope stri
|
|||
t.Fatal("Global OAuth service not initialized")
|
||||
}
|
||||
|
||||
// Get user provider to assign root role
|
||||
userProvider, err := oauthService.GetUserProvider()
|
||||
if err != nil || userProvider == nil {
|
||||
t.Fatal("UserProvider not available")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Ensure system:root role exists (create if needed)
|
||||
roleID := "system:root"
|
||||
_, err = userProvider.GetRole(ctx, roleID)
|
||||
if err != nil {
|
||||
// Role doesn't exist, create it
|
||||
t.Logf("Creating system:root role for user %s", userID)
|
||||
roleData := map[string]interface{}{
|
||||
"role_id": roleID,
|
||||
"name": "System Root",
|
||||
"description": "System root role with full system access",
|
||||
"permissions": []string{"system:root"},
|
||||
"is_active": true,
|
||||
}
|
||||
_, err = userProvider.CreateRole(ctx, roleData)
|
||||
if err != nil {
|
||||
t.Logf("Warning: Failed to create system:root role: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign system:root role to the user
|
||||
err = userProvider.SetUserRole(ctx, userID, roleID)
|
||||
if err != nil {
|
||||
t.Logf("Warning: Failed to assign system:root role to user: %v", err)
|
||||
} else {
|
||||
t.Logf("Assigned system:root role to user %s", userID)
|
||||
}
|
||||
|
||||
// Create subject (fingerprint) for this user
|
||||
// This sets up the fingerprint mapping: clientID:subject -> userID
|
||||
subject, err := oauthService.Subject(clientID, userID)
|
||||
|
|
@ -776,14 +935,22 @@ func ObtainTokenForUser(t *testing.T, clientID, clientSecret, userID, scope stri
|
|||
|
||||
t.Logf("Created fingerprint mapping: clientID=%s, userID=%s, subject=%s", clientID, userID, subject)
|
||||
|
||||
// Create access token
|
||||
accessToken, err := oauthService.MakeAccessToken(clientID, scope, subject, 3600)
|
||||
// Add system:root to scope for full permissions
|
||||
fullScope := scope
|
||||
if scope != "" && !strings.Contains(scope, "system:root") {
|
||||
fullScope = scope + " system:root"
|
||||
} else if scope == "" {
|
||||
fullScope = "system:root"
|
||||
}
|
||||
|
||||
// Create access token with system:root scope
|
||||
accessToken, err := oauthService.MakeAccessToken(clientID, fullScope, subject, 3600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create access token: %v", err)
|
||||
}
|
||||
|
||||
// Create refresh token
|
||||
refreshToken, err := oauthService.MakeRefreshToken(clientID, scope, subject, 7200)
|
||||
refreshToken, err := oauthService.MakeRefreshToken(clientID, fullScope, subject, 7200)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create refresh token: %v", err)
|
||||
}
|
||||
|
|
@ -793,12 +960,12 @@ func ObtainTokenForUser(t *testing.T, clientID, clientSecret, userID, scope stri
|
|||
RefreshToken: refreshToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
Scope: scope,
|
||||
Scope: fullScope,
|
||||
ClientID: clientID,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
t.Logf("Issued token for user %s (subject: %s)", userID, subject)
|
||||
t.Logf("Issued token for user %s with scope: %s (subject: %s)", userID, fullScope, subject)
|
||||
return tokenInfo
|
||||
}
|
||||
|
||||
|
|
@ -811,13 +978,47 @@ func createTestUser(t *testing.T, server *openapi.OpenAPI, clientID string) (str
|
|||
// Generate a unique test user ID
|
||||
testUserID := fmt.Sprintf("test_user_%d", time.Now().UnixNano())
|
||||
|
||||
// Access the global OAuth service to set up fingerprint mapping
|
||||
// The OAuth interface doesn't expose Subject method, so we need to access the concrete service
|
||||
// Access the global OAuth service
|
||||
oauthService := oauth.OAuth
|
||||
if oauthService == nil {
|
||||
t.Fatal("Global OAuth service not initialized")
|
||||
}
|
||||
|
||||
// Create user in database with system:root role
|
||||
userProvider, err := oauthService.GetUserProvider()
|
||||
if err == nil && userProvider != nil {
|
||||
ctx := context.Background()
|
||||
roleID := "system:root"
|
||||
|
||||
// Ensure system:root role exists
|
||||
_, err := userProvider.GetRole(ctx, roleID)
|
||||
if err != nil {
|
||||
// Role doesn't exist, create it
|
||||
roleData := map[string]interface{}{
|
||||
"role_id": roleID,
|
||||
"name": "System Root",
|
||||
"description": "System root role with full system access",
|
||||
"permissions": []string{"system:root"},
|
||||
"is_active": true,
|
||||
}
|
||||
_, err = userProvider.CreateRole(ctx, roleData)
|
||||
if err != nil {
|
||||
t.Logf("Warning: Failed to create system:root role: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create user in database with system:root role
|
||||
userData := map[string]interface{}{
|
||||
"user_id": testUserID,
|
||||
"status": "active",
|
||||
"role_id": roleID,
|
||||
}
|
||||
_, err = userProvider.CreateUser(ctx, userData)
|
||||
if err != nil {
|
||||
t.Logf("Warning: Failed to create user in database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create subject (fingerprint) for this user using the concrete OAuth service
|
||||
// This will set up the proper fingerprint mapping: clientID:subject -> userID
|
||||
subject, err := oauthService.Subject(clientID, testUserID)
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ func TestEntryVerifyWithExistingUser(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// Verify response for existing user (login flow)
|
||||
assert.Equal(t, "login", result.Status)
|
||||
assert.Equal(t, user.EntryVerificationStatus("login"), result.Status)
|
||||
assert.True(t, result.UserExists)
|
||||
assert.NotEmpty(t, result.AccessToken)
|
||||
assert.Equal(t, "Bearer", result.TokenType)
|
||||
|
|
@ -132,7 +132,7 @@ func TestEntryVerifyWithNewUser(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// Verify response for new user (register flow)
|
||||
assert.Equal(t, "register", result.Status)
|
||||
assert.Equal(t, user.EntryVerificationStatus("register"), result.Status)
|
||||
assert.False(t, result.UserExists)
|
||||
assert.NotEmpty(t, result.AccessToken)
|
||||
assert.Equal(t, "Bearer", result.TokenType)
|
||||
|
|
@ -266,7 +266,7 @@ func TestEntryVerifyWithMobile(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// Verify response for existing user with mobile
|
||||
assert.Equal(t, "login", result.Status)
|
||||
assert.Equal(t, user.EntryVerificationStatus("login"), result.Status)
|
||||
assert.True(t, result.UserExists)
|
||||
assert.NotEmpty(t, result.AccessToken)
|
||||
|
||||
|
|
@ -305,7 +305,7 @@ func TestEntryVerifyWithMobile(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// Verify response for new mobile user
|
||||
assert.Equal(t, "register", result.Status)
|
||||
assert.Equal(t, user.EntryVerificationStatus("register"), result.Status)
|
||||
assert.False(t, result.UserExists)
|
||||
assert.True(t, result.VerificationSent)
|
||||
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ func TestInvitationCreate(t *testing.T) {
|
|||
client := testutils.RegisterTestClient(t, "Invitation Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||
|
||||
// Get access token
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
// Get access token with root permissions (creates real user in DB with system:root role)
|
||||
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Use UUID to ensure unique identifiers
|
||||
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||
|
|
@ -332,8 +332,8 @@ func TestInvitationList(t *testing.T) {
|
|||
client := testutils.RegisterTestClient(t, "Invitation List Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||
|
||||
// Get access token
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
// Get access token with root permissions
|
||||
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Use UUID to ensure unique identifiers
|
||||
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||
|
|
@ -474,8 +474,8 @@ func TestInvitationGet(t *testing.T) {
|
|||
client := testutils.RegisterTestClient(t, "Invitation Get Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||
|
||||
// Get access token
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
// Get access token with root permissions
|
||||
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Use UUID to ensure unique identifiers
|
||||
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||
|
|
@ -580,8 +580,8 @@ func TestInvitationResend(t *testing.T) {
|
|||
client := testutils.RegisterTestClient(t, "Invitation Resend Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||
|
||||
// Get access token
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
// Get access token with root permissions
|
||||
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Use UUID to ensure unique identifiers
|
||||
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||
|
|
@ -663,8 +663,8 @@ func TestMultipleInvitationCreation(t *testing.T) {
|
|||
client := testutils.RegisterTestClient(t, "Multiple Invitation Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||
|
||||
// Get access token
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
// Get access token with root permissions
|
||||
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Use UUID to ensure unique identifiers
|
||||
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||
|
|
@ -758,8 +758,8 @@ func TestInvitationDelete(t *testing.T) {
|
|||
client := testutils.RegisterTestClient(t, "Invitation Delete Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||
|
||||
// Get access token
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
// Get access token with root permissions
|
||||
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Use UUID to ensure unique identifiers
|
||||
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||
|
|
|
|||
|
|
@ -11,96 +11,6 @@ import (
|
|||
"github.com/yaoapp/yao/openapi/tests/testutils"
|
||||
)
|
||||
|
||||
func TestUserLogin(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register a test client first (needed for user.Load validation)
|
||||
testClient := testutils.RegisterTestClient(t, "User Login Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||
|
||||
// Note: user.Load is automatically called by openapi.Load in testutils.Prepare
|
||||
|
||||
// Test entry endpoint (unified login/register, currently empty implementation)
|
||||
testCases := []struct {
|
||||
name string
|
||||
method string
|
||||
endpoint string
|
||||
body map[string]interface{}
|
||||
expectCode int
|
||||
}{
|
||||
{
|
||||
"post entry without credentials",
|
||||
"POST",
|
||||
"/user/entry",
|
||||
map[string]interface{}{},
|
||||
200, // Currently empty implementation, may change when implemented
|
||||
},
|
||||
{
|
||||
"post entry with credentials",
|
||||
"POST",
|
||||
"/user/entry",
|
||||
map[string]interface{}{
|
||||
"username": "testuser",
|
||||
"password": "testpass",
|
||||
},
|
||||
200, // Currently empty implementation, may change when implemented
|
||||
},
|
||||
{
|
||||
"post entry with email",
|
||||
"POST",
|
||||
"/user/entry",
|
||||
map[string]interface{}{
|
||||
"email": "test@example.com",
|
||||
"password": "testpass",
|
||||
},
|
||||
200, // Currently empty implementation, may change when implemented
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
requestURL := serverURL + baseURL + tc.endpoint
|
||||
|
||||
// Prepare request body
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
if tc.method == "POST" {
|
||||
bodyBytes, _ := json.Marshal(tc.body)
|
||||
req, err = http.NewRequest(tc.method, requestURL, bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req, err = http.NewRequest(tc.method, requestURL, nil)
|
||||
}
|
||||
|
||||
assert.NoError(t, err, "Should create HTTP request")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err, "HTTP request should succeed")
|
||||
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d", tc.expectCode)
|
||||
|
||||
t.Logf("Login test %s: status=%d", tc.name, resp.StatusCode)
|
||||
|
||||
// Note: Since login is currently not implemented (empty function),
|
||||
// we can't test actual login functionality yet.
|
||||
// This test serves as a placeholder for when login is implemented.
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserLoginValidation(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -133,7 +134,7 @@ func TestMemberList(t *testing.T) {
|
|||
if len(data) > 0 {
|
||||
member := data[0].(map[string]interface{})
|
||||
assert.Equal(t, tokenInfo.UserID, member["user_id"], "Owner should be in member list")
|
||||
assert.Equal(t, "owner", member["role_id"], "Creator should have owner role")
|
||||
assert.Equal(t, "owner:free", member["role_id"], "Creator should have owner:free role")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1008,7 +1009,7 @@ func getOwnerMemberID(t *testing.T, serverURL, baseURL, teamID, accessToken stri
|
|||
// Find the owner member and return their user_id
|
||||
for _, item := range data {
|
||||
member := item.(map[string]interface{})
|
||||
if role, ok := member["role_id"].(string); ok && role == "owner" {
|
||||
if role, ok := member["role_id"].(string); ok && strings.HasPrefix(role, "owner") {
|
||||
userID, ok := member["user_id"].(string)
|
||||
if !ok {
|
||||
t.Fatal("Owner member missing user_id")
|
||||
|
|
|
|||
368
openapi/tests/user/team_config_robot_test.go
Normal file
368
openapi/tests/user/team_config_robot_test.go
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
package user_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
"github.com/yaoapp/yao/openapi/tests/testutils"
|
||||
"github.com/yaoapp/yao/openapi/user"
|
||||
)
|
||||
|
||||
// TestTeamConfigRobotLoad tests loading team configuration with robot field
|
||||
func TestTeamConfigRobotLoad(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
_ = serverURL // Server URL not needed for this test
|
||||
|
||||
// Get team config with robot configuration
|
||||
teamConfig := user.GetTeamConfig("en")
|
||||
assert.NotNil(t, teamConfig, "Team config should not be nil")
|
||||
|
||||
if teamConfig != nil && teamConfig.Robot != nil {
|
||||
t.Logf("Robot config loaded successfully")
|
||||
|
||||
// Test robot roles
|
||||
assert.NotNil(t, teamConfig.Robot.Roles, "Robot roles should not be nil")
|
||||
if teamConfig.Robot.Roles != nil {
|
||||
t.Logf("Robot roles: %v", teamConfig.Robot.Roles)
|
||||
assert.Greater(t, len(teamConfig.Robot.Roles), 0, "Robot should have at least one role")
|
||||
}
|
||||
|
||||
// Test robot agents
|
||||
assert.NotNil(t, teamConfig.Robot.Agents, "Robot agents should not be nil")
|
||||
if teamConfig.Robot.Agents != nil {
|
||||
t.Logf("Robot agents - Executor: %s, Planner: %s, Profiler: %s",
|
||||
teamConfig.Robot.Agents.Executor,
|
||||
teamConfig.Robot.Agents.Planner,
|
||||
teamConfig.Robot.Agents.Profiler)
|
||||
assert.NotEmpty(t, teamConfig.Robot.Agents.Executor, "Executor agent should not be empty")
|
||||
assert.NotEmpty(t, teamConfig.Robot.Agents.Planner, "Planner agent should not be empty")
|
||||
assert.NotEmpty(t, teamConfig.Robot.Agents.Profiler, "Profiler agent should not be empty")
|
||||
}
|
||||
|
||||
// Test robot email domains
|
||||
assert.NotNil(t, teamConfig.Robot.EmailDomains, "Robot email domains should not be nil")
|
||||
if teamConfig.Robot.EmailDomains != nil {
|
||||
t.Logf("Robot has %d email domain(s)", len(teamConfig.Robot.EmailDomains))
|
||||
assert.Greater(t, len(teamConfig.Robot.EmailDomains), 0, "Robot should have at least one email domain")
|
||||
|
||||
for i, domain := range teamConfig.Robot.EmailDomains {
|
||||
t.Logf("Email domain %d: %s (%s)", i, domain.Name, domain.Domain)
|
||||
assert.NotEmpty(t, domain.Name, "Email domain name should not be empty")
|
||||
assert.NotEmpty(t, domain.Domain, "Email domain should not be empty")
|
||||
assert.NotEmpty(t, domain.Messenger, "Email messenger should not be empty")
|
||||
assert.Greater(t, domain.PrefixMinLength, 0, "PrefixMinLength should be greater than 0")
|
||||
assert.Greater(t, domain.PrefixMaxLength, domain.PrefixMinLength, "PrefixMaxLength should be greater than PrefixMinLength")
|
||||
|
||||
// Test whitelist
|
||||
assert.NotNil(t, domain.Whitelist, "Whitelist should not be nil")
|
||||
if domain.Whitelist != nil {
|
||||
t.Logf(" Whitelist - Domains: %v, Senders: %v, IPs: %v",
|
||||
domain.Whitelist.Domains,
|
||||
domain.Whitelist.Senders,
|
||||
domain.Whitelist.IPs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test robot defaults
|
||||
assert.NotNil(t, teamConfig.Robot.Defaults, "Robot defaults should not be nil")
|
||||
if teamConfig.Robot.Defaults != nil {
|
||||
t.Logf("Robot defaults - LLM: %s, AutonomousMode: %v, CostLimit: %d",
|
||||
teamConfig.Robot.Defaults.LLM,
|
||||
teamConfig.Robot.Defaults.AutonomousMode,
|
||||
teamConfig.Robot.Defaults.CostLimit)
|
||||
assert.NotEmpty(t, teamConfig.Robot.Defaults.LLM, "Default LLM should not be empty")
|
||||
}
|
||||
} else {
|
||||
t.Log("No robot configuration found in team config")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetTeamConfigPublic tests that GetTeamConfigPublic hides sensitive fields
|
||||
func TestGetTeamConfigPublic(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
_ = serverURL // Server URL not needed for this test
|
||||
|
||||
// Get original config
|
||||
originalConfig := user.GetTeamConfig("en")
|
||||
assert.NotNil(t, originalConfig, "Original config should not be nil")
|
||||
|
||||
// Get public config
|
||||
publicConfig := user.GetTeamConfigPublic("en")
|
||||
assert.NotNil(t, publicConfig, "Public config should not be nil")
|
||||
|
||||
// Test that basic fields are preserved
|
||||
assert.Equal(t, originalConfig.Type, publicConfig.Type, "Type should be preserved")
|
||||
assert.Equal(t, originalConfig.Role, publicConfig.Role, "Role should be preserved")
|
||||
assert.Equal(t, originalConfig.Roles, publicConfig.Roles, "Roles should be preserved")
|
||||
assert.Equal(t, originalConfig.Invite, publicConfig.Invite, "Invite config should be preserved")
|
||||
|
||||
// Test robot configuration
|
||||
if originalConfig.Robot != nil {
|
||||
t.Log("Testing robot config sanitization")
|
||||
|
||||
assert.NotNil(t, publicConfig.Robot, "Public config should have robot config")
|
||||
|
||||
// Test that roles are preserved
|
||||
assert.Equal(t, originalConfig.Robot.Roles, publicConfig.Robot.Roles, "Robot roles should be preserved")
|
||||
|
||||
// Test that agents are hidden (SENSITIVE)
|
||||
assert.Nil(t, publicConfig.Robot.Agents, "Robot agents should be hidden in public config")
|
||||
if originalConfig.Robot.Agents != nil {
|
||||
t.Logf("Original agents (hidden in public): Executor=%s, Planner=%s, Profiler=%s",
|
||||
originalConfig.Robot.Agents.Executor,
|
||||
originalConfig.Robot.Agents.Planner,
|
||||
originalConfig.Robot.Agents.Profiler)
|
||||
}
|
||||
|
||||
// Test that defaults are preserved
|
||||
assert.Equal(t, originalConfig.Robot.Defaults, publicConfig.Robot.Defaults, "Robot defaults should be preserved")
|
||||
|
||||
// Test email domains
|
||||
if originalConfig.Robot.EmailDomains != nil {
|
||||
assert.NotNil(t, publicConfig.Robot.EmailDomains, "Public config should have email domains")
|
||||
assert.Equal(t, len(originalConfig.Robot.EmailDomains), len(publicConfig.Robot.EmailDomains),
|
||||
"Email domains count should match")
|
||||
|
||||
for i := 0; i < len(originalConfig.Robot.EmailDomains); i++ {
|
||||
origDomain := originalConfig.Robot.EmailDomains[i]
|
||||
pubDomain := publicConfig.Robot.EmailDomains[i]
|
||||
|
||||
// Test that basic fields are preserved
|
||||
assert.Equal(t, origDomain.Name, pubDomain.Name, "Domain name should be preserved")
|
||||
assert.Equal(t, origDomain.Domain, pubDomain.Domain, "Domain should be preserved")
|
||||
assert.Equal(t, origDomain.Messenger, pubDomain.Messenger, "Messenger should be preserved")
|
||||
assert.Equal(t, origDomain.PrefixMinLength, pubDomain.PrefixMinLength, "PrefixMinLength should be preserved")
|
||||
assert.Equal(t, origDomain.PrefixMaxLength, pubDomain.PrefixMaxLength, "PrefixMaxLength should be preserved")
|
||||
assert.Equal(t, origDomain.ReservedWords, pubDomain.ReservedWords, "ReservedWords should be preserved")
|
||||
|
||||
// Test that whitelist is hidden (SENSITIVE)
|
||||
assert.Nil(t, pubDomain.Whitelist, "Whitelist should be hidden in public config")
|
||||
if origDomain.Whitelist != nil {
|
||||
t.Logf("Domain %s whitelist (hidden in public): Domains=%v, Senders=%v, IPs=%v",
|
||||
origDomain.Name,
|
||||
origDomain.Whitelist.Domains,
|
||||
origDomain.Whitelist.Senders,
|
||||
origDomain.Whitelist.IPs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetTeamConfigPublicNoMutation tests that GetTeamConfigPublic doesn't mutate original data
|
||||
func TestGetTeamConfigPublicNoMutation(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
_ = serverURL // Server URL not needed for this test
|
||||
|
||||
// Get original config
|
||||
originalConfig := user.GetTeamConfig("en")
|
||||
if originalConfig == nil || originalConfig.Robot == nil {
|
||||
t.Skip("No robot config available for this test")
|
||||
}
|
||||
|
||||
// Store original values for comparison
|
||||
var originalAgentsPresent bool
|
||||
var originalAgents *user.RobotAgents
|
||||
if originalConfig.Robot.Agents != nil {
|
||||
originalAgentsPresent = true
|
||||
originalAgents = &user.RobotAgents{
|
||||
Executor: originalConfig.Robot.Agents.Executor,
|
||||
Planner: originalConfig.Robot.Agents.Planner,
|
||||
Profiler: originalConfig.Robot.Agents.Profiler,
|
||||
}
|
||||
}
|
||||
|
||||
var originalWhitelists []*user.EmailDomainWhitelist
|
||||
if originalConfig.Robot.EmailDomains != nil {
|
||||
for _, domain := range originalConfig.Robot.EmailDomains {
|
||||
if domain.Whitelist != nil {
|
||||
originalWhitelists = append(originalWhitelists, &user.EmailDomainWhitelist{
|
||||
Domains: domain.Whitelist.Domains,
|
||||
Senders: domain.Whitelist.Senders,
|
||||
IPs: domain.Whitelist.IPs,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get public config (should create a copy, not mutate original)
|
||||
publicConfig := user.GetTeamConfigPublic("en")
|
||||
assert.NotNil(t, publicConfig, "Public config should not be nil")
|
||||
|
||||
// Verify original config is unchanged
|
||||
originalConfigAfter := user.GetTeamConfig("en")
|
||||
assert.NotNil(t, originalConfigAfter, "Original config should still exist")
|
||||
|
||||
if originalAgentsPresent {
|
||||
assert.NotNil(t, originalConfigAfter.Robot.Agents, "Original agents should still be present")
|
||||
assert.Equal(t, originalAgents.Executor, originalConfigAfter.Robot.Agents.Executor, "Original executor should be unchanged")
|
||||
assert.Equal(t, originalAgents.Planner, originalConfigAfter.Robot.Agents.Planner, "Original planner should be unchanged")
|
||||
assert.Equal(t, originalAgents.Profiler, originalConfigAfter.Robot.Agents.Profiler, "Original profiler should be unchanged")
|
||||
}
|
||||
|
||||
if len(originalWhitelists) > 0 {
|
||||
for i, domain := range originalConfigAfter.Robot.EmailDomains {
|
||||
if i < len(originalWhitelists) {
|
||||
assert.NotNil(t, domain.Whitelist, "Original whitelist should still be present")
|
||||
assert.Equal(t, originalWhitelists[i].Domains, domain.Whitelist.Domains, "Original whitelist domains should be unchanged")
|
||||
assert.Equal(t, originalWhitelists[i].Senders, domain.Whitelist.Senders, "Original whitelist senders should be unchanged")
|
||||
assert.Equal(t, originalWhitelists[i].IPs, domain.Whitelist.IPs, "Original whitelist IPs should be unchanged")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Log("Original config remains intact after calling GetTeamConfigPublic")
|
||||
}
|
||||
|
||||
// TestTeamConfigAPIPublic tests that the API endpoint returns public config
|
||||
func TestTeamConfigAPIPublic(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register a test client and get access token
|
||||
testClient := testutils.RegisterTestClient(t, "Robot Config Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||
|
||||
// Obtain access token for authentication
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Test API endpoint
|
||||
requestURL := serverURL + baseURL + "/user/teams/config?locale=en"
|
||||
|
||||
// Create request with Authorization header
|
||||
req, err := http.NewRequest("GET", requestURL, nil)
|
||||
assert.NoError(t, err, "Should create HTTP request")
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err, "HTTP request should succeed")
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Expected status code 200")
|
||||
|
||||
// Parse response body
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "Should read response body")
|
||||
|
||||
var teamConfig user.TeamConfig
|
||||
err = json.Unmarshal(body, &teamConfig)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
|
||||
t.Logf("API response has %d roles", len(teamConfig.Roles))
|
||||
|
||||
// Test that robot config is present (if available in config files)
|
||||
if teamConfig.Robot != nil {
|
||||
t.Log("Robot config present in API response")
|
||||
|
||||
// Test that public fields are present
|
||||
if teamConfig.Robot.Roles != nil {
|
||||
t.Logf("Robot roles: %v", teamConfig.Robot.Roles)
|
||||
assert.Greater(t, len(teamConfig.Robot.Roles), 0, "Robot should have at least one role")
|
||||
}
|
||||
|
||||
if teamConfig.Robot.Defaults != nil {
|
||||
t.Logf("Robot defaults - LLM: %s, AutonomousMode: %v, CostLimit: %d",
|
||||
teamConfig.Robot.Defaults.LLM,
|
||||
teamConfig.Robot.Defaults.AutonomousMode,
|
||||
teamConfig.Robot.Defaults.CostLimit)
|
||||
}
|
||||
|
||||
// Test that sensitive fields are hidden
|
||||
assert.Nil(t, teamConfig.Robot.Agents, "Robot agents should be hidden in API response (SENSITIVE)")
|
||||
|
||||
if teamConfig.Robot.EmailDomains != nil {
|
||||
t.Logf("Robot has %d email domain(s)", len(teamConfig.Robot.EmailDomains))
|
||||
for i, domain := range teamConfig.Robot.EmailDomains {
|
||||
t.Logf("Email domain %d: %s (%s)", i, domain.Name, domain.Domain)
|
||||
assert.NotEmpty(t, domain.Name, "Email domain name should be present")
|
||||
assert.NotEmpty(t, domain.Domain, "Email domain should be present")
|
||||
|
||||
// Test that whitelist is hidden
|
||||
assert.Nil(t, domain.Whitelist, "Whitelist should be hidden in API response (SENSITIVE)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Log("No robot configuration in API response")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamConfigAPILocales tests that API returns correct locale-specific robot config
|
||||
func TestTeamConfigAPILocales(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register a test client and get access token
|
||||
testClient := testutils.RegisterTestClient(t, "Robot Config Locale Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||
|
||||
// Obtain access token for authentication
|
||||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
// Test different locales
|
||||
locales := []string{"en", "zh-cn", ""}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
for _, locale := range locales {
|
||||
t.Run("locale_"+locale, func(t *testing.T) {
|
||||
requestURL := serverURL + baseURL + "/user/teams/config"
|
||||
if locale != "" {
|
||||
requestURL += "?locale=" + locale
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", requestURL, nil)
|
||||
assert.NoError(t, err, "Should create HTTP request")
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err, "HTTP request should succeed")
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Expected status code 200")
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "Should read response body")
|
||||
|
||||
var teamConfig user.TeamConfig
|
||||
err = json.Unmarshal(body, &teamConfig)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
|
||||
t.Logf("Locale '%s': %d roles", locale, len(teamConfig.Roles))
|
||||
|
||||
// Verify sensitive fields are hidden
|
||||
if teamConfig.Robot != nil {
|
||||
assert.Nil(t, teamConfig.Robot.Agents, "Agents should be hidden for locale: "+locale)
|
||||
if teamConfig.Robot.EmailDomains != nil {
|
||||
for _, domain := range teamConfig.Robot.EmailDomains {
|
||||
assert.Nil(t, domain.Whitelist, "Whitelist should be hidden for locale: "+locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -330,6 +330,54 @@ func GetTeamConfig(locale string) *TeamConfig {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetTeamConfigPublic returns the public team configuration for a given locale
|
||||
// This method hides sensitive fields (agents, email_domains.whitelist) without destroying the loaded data
|
||||
func GetTeamConfigPublic(locale string) *TeamConfig {
|
||||
configMutex.RLock()
|
||||
defer configMutex.RUnlock()
|
||||
|
||||
// Get the original config
|
||||
originalConfig := GetTeamConfig(locale)
|
||||
if originalConfig == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a deep copy of the config to avoid modifying the original
|
||||
publicConfig := &TeamConfig{
|
||||
Type: originalConfig.Type,
|
||||
Role: originalConfig.Role,
|
||||
Roles: originalConfig.Roles, // Shallow copy is OK for roles (read-only)
|
||||
Invite: originalConfig.Invite, // Shallow copy is OK for invite config (read-only)
|
||||
}
|
||||
|
||||
// Handle robot config - create a copy without sensitive fields
|
||||
if originalConfig.Robot != nil {
|
||||
publicConfig.Robot = &RobotConfig{
|
||||
Roles: originalConfig.Robot.Roles, // Shallow copy of string slice
|
||||
// Agents is intentionally omitted (sensitive)
|
||||
Defaults: originalConfig.Robot.Defaults, // Shallow copy is OK (read-only)
|
||||
}
|
||||
|
||||
// Copy email domains without whitelist
|
||||
if originalConfig.Robot.EmailDomains != nil {
|
||||
publicConfig.Robot.EmailDomains = make([]*RobotEmailDomain, len(originalConfig.Robot.EmailDomains))
|
||||
for i, domain := range originalConfig.Robot.EmailDomains {
|
||||
publicConfig.Robot.EmailDomains[i] = &RobotEmailDomain{
|
||||
Name: domain.Name,
|
||||
Messenger: domain.Messenger,
|
||||
Domain: domain.Domain,
|
||||
PrefixMinLength: domain.PrefixMinLength,
|
||||
PrefixMaxLength: domain.PrefixMaxLength,
|
||||
ReservedWords: domain.ReservedWords,
|
||||
// Whitelist is intentionally omitted (sensitive)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return publicConfig
|
||||
}
|
||||
|
||||
// extractEnvVarName extracts the environment variable name from a string like "$ENV.VAR_NAME"
|
||||
func extractEnvVarName(value string) string {
|
||||
if value == "" {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func GinTeamConfig(c *gin.Context) {
|
|||
locale = strings.TrimSpace(locale)
|
||||
locale = strings.Trim(locale, "?&=")
|
||||
|
||||
config := GetTeamConfig(locale)
|
||||
config := GetTeamConfigPublic(locale)
|
||||
if config == nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
|
|
|
|||
|
|
@ -499,9 +499,50 @@ type CreateInvitationRequest struct {
|
|||
|
||||
// ==== Team Configuration Types ====
|
||||
|
||||
// RobotConfig represents the AI member (robot) configuration
|
||||
type RobotConfig struct {
|
||||
Roles []string `json:"roles,omitempty"` // Available roles for AI members
|
||||
Agents *RobotAgents `json:"agents,omitempty"` // Agent configuration for AI members
|
||||
EmailDomains []*RobotEmailDomain `json:"email_domains,omitempty"` // Email domain configurations
|
||||
Defaults *RobotDefaults `json:"defaults,omitempty"` // Default settings for AI members
|
||||
}
|
||||
|
||||
// RobotAgents represents the agent configuration for AI members
|
||||
type RobotAgents struct {
|
||||
Executor string `json:"executor,omitempty"` // Agent responsible for executing tasks
|
||||
Planner string `json:"planner,omitempty"` // Agent responsible for planning
|
||||
Profiler string `json:"profiler,omitempty"` // Agent responsible for identity
|
||||
}
|
||||
|
||||
// RobotEmailDomain represents an email domain configuration for AI members
|
||||
type RobotEmailDomain struct {
|
||||
Name string `json:"name,omitempty"` // Display name
|
||||
Messenger string `json:"messenger,omitempty"` // Messenger channel
|
||||
Domain string `json:"domain,omitempty"` // Email domain
|
||||
PrefixMinLength int `json:"prefix_min_length,omitempty"` // Minimum prefix length
|
||||
PrefixMaxLength int `json:"prefix_max_length,omitempty"` // Maximum prefix length
|
||||
ReservedWords []string `json:"reserved_words,omitempty"` // Reserved words
|
||||
Whitelist *EmailDomainWhitelist `json:"whitelist,omitempty"` // Whitelist configuration
|
||||
}
|
||||
|
||||
// EmailDomainWhitelist represents the whitelist configuration for email domains
|
||||
type EmailDomainWhitelist struct {
|
||||
Domains []string `json:"domains,omitempty"` // Whitelisted domains
|
||||
Senders []string `json:"senders,omitempty"` // Whitelisted senders
|
||||
IPs []string `json:"ips,omitempty"` // Whitelisted IPs
|
||||
}
|
||||
|
||||
// RobotDefaults represents default settings for AI members
|
||||
type RobotDefaults struct {
|
||||
LLM string `json:"llm,omitempty"` // Default LLM model
|
||||
AutonomousMode bool `json:"autonomous_mode,omitempty"` // Default autonomous mode
|
||||
CostLimit int `json:"cost_limit,omitempty"` // Default daily cost limit
|
||||
}
|
||||
|
||||
// TeamConfig represents the team configuration loaded from DSL files
|
||||
type TeamConfig struct {
|
||||
Roles []*TeamRole `json:"roles,omitempty"`
|
||||
Robot *RobotConfig `json:"robot,omitempty"`
|
||||
Invite *InviteConfig `json:"invite,omitempty"`
|
||||
Type string `json:"type,omitempty"` // Default subscription type for new teams
|
||||
Role string `json:"role,omitempty"` // Default user role for team creator
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ func attachTeam(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
team.Use(oauth.Guard)
|
||||
|
||||
// Team Configuration
|
||||
team.GET("/config", GinTeamConfig) // Get team configuration (requires authentication)
|
||||
team.GET("/config", GinTeamConfig) // Get team configuration (public version, sensitive fields hidden)
|
||||
|
||||
// Team Selection
|
||||
team.POST("/select", GinTeamSelection) // POST /teams/select - Select a team and issue tokens with team_id (requires authentication)
|
||||
|
|
|
|||
|
|
@ -249,6 +249,24 @@ func loadSystemStores(t *testing.T, cfg config.Config) error {
|
|||
source = replaceVars(source, vars)
|
||||
}
|
||||
|
||||
// Parse store config to check if we need to create directories (for badger stores)
|
||||
var storeConfig map[string]interface{}
|
||||
if err := application.Parse(path, []byte(source), &storeConfig); err == nil {
|
||||
// Check if this is a badger store
|
||||
if storeType, ok := storeConfig["type"].(string); ok && storeType == "badger" {
|
||||
// Extract the path from option.path
|
||||
if option, ok := storeConfig["option"].(map[string]interface{}); ok {
|
||||
if storePath, ok := option["path"].(string); ok {
|
||||
// Create directory for badger store
|
||||
if err := os.MkdirAll(storePath, 0755); err != nil {
|
||||
log.Error("failed to create directory for store %s at %s: %s", id, storePath, err.Error())
|
||||
return fmt.Errorf("failed to create directory for store %s: %w", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load store with the processed source
|
||||
_, err = store.LoadSource([]byte(source), id, filepath.Join("__system", path))
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue