Implement GetAssistant endpoint with permission verification
- Added a new GetAssistant function to retrieve assistant details by ID, incorporating permission checks to ensure authorized access. - Updated the assistant route to use the new GetAssistant handler, enhancing security by verifying user permissions before returning assistant data. - Introduced a FilterBuiltInAssistant function to clear sensitive fields for built-in assistants, ensuring compliance with data protection standards. - Enhanced test coverage for the GetAssistant functionality, including tests for successful retrieval, permission filtering, and error handling scenarios.
This commit is contained in:
parent
42b5e17d78
commit
0f17db74ca
4 changed files with 745 additions and 11 deletions
|
|
@ -20,7 +20,7 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
group.GET("/assistants", ListAssistants) // GET /assistants - List assistants
|
group.GET("/assistants", ListAssistants) // GET /assistants - List assistants
|
||||||
group.POST("/assistants", n.HandleAssistantSave) // POST /assistants - Create/Update assistant
|
group.POST("/assistants", n.HandleAssistantSave) // POST /assistants - Create/Update assistant
|
||||||
group.GET("/assistants/tags", ListAssistantTags) // GET /assistants/tags - Get all assistant tags with permission filtering
|
group.GET("/assistants/tags", ListAssistantTags) // GET /assistants/tags - Get all assistant tags with permission filtering
|
||||||
group.GET("/assistants/:id", n.HandleAssistantDetail) // GET /assistants/:id - Get assistant details
|
group.GET("/assistants/:id", GetAssistant) // GET /assistants/:id - Get assistant details with permission verification
|
||||||
group.DELETE("/assistants/:id", n.HandleAssistantDelete) // DELETE /assistants/:id - Delete assistant
|
group.DELETE("/assistants/:id", n.HandleAssistantDelete) // DELETE /assistants/:id - Delete assistant
|
||||||
|
|
||||||
// Assistant Actions
|
// Assistant Actions
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/yao/agent"
|
"github.com/yaoapp/yao/agent"
|
||||||
|
agenttypes "github.com/yaoapp/yao/agent/store/types"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
"github.com/yaoapp/yao/openapi/response"
|
"github.com/yaoapp/yao/openapi/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -160,6 +162,87 @@ func ListAssistants(c *gin.Context) {
|
||||||
response.RespondWithSuccess(c, response.StatusOK, result)
|
response.RespondWithSuccess(c, response.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAssistant retrieves a single assistant by ID with permission verification
|
||||||
|
func GetAssistant(c *gin.Context) {
|
||||||
|
|
||||||
|
// Get authorized information
|
||||||
|
authInfo := authorized.GetInfo(c)
|
||||||
|
|
||||||
|
// Get Agent instance from global variable
|
||||||
|
agentInstance := agent.GetAgent()
|
||||||
|
if agentInstance == nil || agentInstance.Store == nil {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Agent store not initialized",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get assistant ID from URL parameter
|
||||||
|
assistantID := c.Param("id")
|
||||||
|
if assistantID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "assistant_id is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse locale (optional - if not provided, returns raw data without i18n translation)
|
||||||
|
// This is useful for form editing scenarios where you need the original values
|
||||||
|
var assistant *agenttypes.AssistantModel
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if loc := c.Query("locale"); loc != "" {
|
||||||
|
// If locale is specified, get assistant with translation
|
||||||
|
locale := strings.ToLower(strings.TrimSpace(loc))
|
||||||
|
assistant, err = agentInstance.Store.GetAssistant(assistantID, locale)
|
||||||
|
} else {
|
||||||
|
// If no locale specified, get raw data without translation
|
||||||
|
assistant, err = agentInstance.Store.GetAssistant(assistantID)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get assistant %s: %v", assistantID, err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Assistant not found: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
hasPermission, err := checkAssistantPermission(authInfo, assistant)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to check permission for assistant %s: %v", assistantID, err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to check permission: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasPermission {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrAccessDenied.Code,
|
||||||
|
ErrorDescription: "Forbidden: No permission to access this assistant",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter sensitive fields for built-in assistants
|
||||||
|
FilterBuiltInAssistant(assistant)
|
||||||
|
|
||||||
|
// Return the result with standard response format
|
||||||
|
response.RespondWithSuccess(c, response.StatusOK, map[string]interface{}{
|
||||||
|
"data": assistant,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ListAssistantTags lists assistant tags with permission-based filtering
|
// ListAssistantTags lists assistant tags with permission-based filtering
|
||||||
func ListAssistantTags(c *gin.Context) {
|
func ListAssistantTags(c *gin.Context) {
|
||||||
|
|
||||||
|
|
@ -233,3 +316,53 @@ func ListAssistantTags(c *gin.Context) {
|
||||||
"data": tags,
|
"data": tags,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkAssistantPermission checks if the user has permission to access the assistant
|
||||||
|
// Similar logic to checkFilePermission in openapi/file/file.go
|
||||||
|
func checkAssistantPermission(authInfo *types.AuthorizedInfo, assistant *agenttypes.AssistantModel) (bool, error) {
|
||||||
|
// No auth info, allow access
|
||||||
|
if authInfo == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// No constraints, allow access
|
||||||
|
if !authInfo.Constraints.TeamOnly && !authInfo.Constraints.OwnerOnly {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If assistant is public, allow access
|
||||||
|
if assistant.Public {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combined Team and Owner permission validation
|
||||||
|
if authInfo.Constraints.TeamOnly && authInfo.Constraints.OwnerOnly {
|
||||||
|
if assistant.YaoCreatedBy == authInfo.UserID && assistant.YaoTeamID == authInfo.TeamID {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Team only permission validation
|
||||||
|
if authInfo.Constraints.TeamOnly {
|
||||||
|
// Check if assistant belongs to the same team
|
||||||
|
if assistant.YaoTeamID == authInfo.TeamID {
|
||||||
|
// Allow if user created it or if it's shared with team
|
||||||
|
if assistant.YaoCreatedBy == authInfo.UserID || assistant.Share == "team" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner only permission validation
|
||||||
|
if authInfo.Constraints.OwnerOnly {
|
||||||
|
// Check if user created the assistant and team_id is empty (not a team resource)
|
||||||
|
if assistant.YaoCreatedBy == authInfo.UserID && assistant.YaoTeamID == "" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ func AuthQueryFilter(c *gin.Context, authInfo *types.AuthorizedInfo) func(query.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterBuiltInFields filters sensitive fields for built-in assistants
|
// FilterBuiltInFields filters sensitive fields for built-in assistants in a list
|
||||||
// For built-in assistants, code-level fields (prompts, workflow, tools, kb, mcp, options) should be cleared
|
// For built-in assistants, code-level fields (prompts, workflow, tools, kb, mcp, options) should be cleared
|
||||||
func FilterBuiltInFields(assistants []*agenttypes.AssistantModel) {
|
func FilterBuiltInFields(assistants []*agenttypes.AssistantModel) {
|
||||||
if assistants == nil {
|
if assistants == nil {
|
||||||
|
|
@ -133,7 +133,19 @@ func FilterBuiltInFields(assistants []*agenttypes.AssistantModel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, assistant := range assistants {
|
for _, assistant := range assistants {
|
||||||
if assistant != nil && assistant.BuiltIn {
|
FilterBuiltInAssistant(assistant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterBuiltInAssistant filters sensitive fields for a single built-in assistant
|
||||||
|
// For built-in assistants, code-level fields (prompts, workflow, tools, kb, mcp, options) should be cleared
|
||||||
|
// This function can be used for both single assistant and list of assistants
|
||||||
|
func FilterBuiltInAssistant(assistant *agenttypes.AssistantModel) {
|
||||||
|
if assistant == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if assistant.BuiltIn {
|
||||||
// Clear code-level sensitive fields for built-in assistants
|
// Clear code-level sensitive fields for built-in assistants
|
||||||
assistant.Prompts = nil
|
assistant.Prompts = nil
|
||||||
assistant.Workflow = nil
|
assistant.Workflow = nil
|
||||||
|
|
@ -142,5 +154,4 @@ func FilterBuiltInFields(assistants []*agenttypes.AssistantModel) {
|
||||||
assistant.MCP = nil
|
assistant.MCP = nil
|
||||||
assistant.Options = nil
|
assistant.Options = nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -920,6 +920,533 @@ func TestListAssistantTags(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGetAssistant tests the get assistant detail endpoint
|
||||||
|
func TestGetAssistant(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 test client and get token
|
||||||
|
client := testutils.RegisterTestClient(t, "Agent Detail Test Client", []string{"https://localhost/callback"})
|
||||||
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||||
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||||
|
|
||||||
|
t.Run("GetAssistantSuccess", func(t *testing.T) {
|
||||||
|
// First, get a list of assistants to find a valid assistant_id
|
||||||
|
listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
listResp, err := http.DefaultClient.Do(listReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, listResp)
|
||||||
|
defer listResp.Body.Close()
|
||||||
|
|
||||||
|
var listResponse map[string]interface{}
|
||||||
|
err = json.NewDecoder(listResp.Body).Decode(&listResponse)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
data, hasData := listResponse["data"].([]interface{})
|
||||||
|
if !hasData || len(data) == 0 {
|
||||||
|
t.Skip("No assistants available for testing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first assistant's ID
|
||||||
|
assistant, ok := data[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Skip("Invalid assistant data format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantID, ok := assistant["assistant_id"].(string)
|
||||||
|
if !ok || assistantID == "" {
|
||||||
|
t.Skip("Assistant ID not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Testing with assistant ID: %s", assistantID)
|
||||||
|
|
||||||
|
// Now test getting the specific assistant
|
||||||
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Expect successful response
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully retrieve assistant")
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Response should have data field with assistant details
|
||||||
|
detailData, hasDetailData := response["data"].(map[string]interface{})
|
||||||
|
assert.True(t, hasDetailData, "Response should have 'data' field")
|
||||||
|
assert.NotNil(t, detailData, "Assistant data should not be nil")
|
||||||
|
|
||||||
|
// Verify assistant_id matches
|
||||||
|
returnedID, hasID := detailData["assistant_id"].(string)
|
||||||
|
assert.True(t, hasID, "Assistant should have assistant_id field")
|
||||||
|
assert.Equal(t, assistantID, returnedID, "Returned assistant_id should match requested ID")
|
||||||
|
|
||||||
|
t.Logf("Successfully retrieved assistant: %s", returnedID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetAssistantWithoutLocale", func(t *testing.T) {
|
||||||
|
// Get a valid assistant ID first
|
||||||
|
listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
listResp, err := http.DefaultClient.Do(listReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer listResp.Body.Close()
|
||||||
|
|
||||||
|
var listResponse map[string]interface{}
|
||||||
|
err = json.NewDecoder(listResp.Body).Decode(&listResponse)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
data, hasData := listResponse["data"].([]interface{})
|
||||||
|
if !hasData || len(data) == 0 {
|
||||||
|
t.Skip("No assistants available for testing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistant, ok := data[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Skip("Invalid assistant data format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantID, ok := assistant["assistant_id"].(string)
|
||||||
|
if !ok || assistantID == "" {
|
||||||
|
t.Skip("Assistant ID not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test without locale parameter - should return raw data (useful for form editing)
|
||||||
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, hasDetailData := response["data"].(map[string]interface{})
|
||||||
|
assert.True(t, hasDetailData, "Response should have 'data' field")
|
||||||
|
|
||||||
|
t.Logf("Successfully retrieved assistant without locale (raw data for editing)")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetAssistantWithLocale", func(t *testing.T) {
|
||||||
|
// Get a valid assistant ID first
|
||||||
|
listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
listResp, err := http.DefaultClient.Do(listReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer listResp.Body.Close()
|
||||||
|
|
||||||
|
var listResponse map[string]interface{}
|
||||||
|
err = json.NewDecoder(listResp.Body).Decode(&listResponse)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
data, hasData := listResponse["data"].([]interface{})
|
||||||
|
if !hasData || len(data) == 0 {
|
||||||
|
t.Skip("No assistants available for testing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistant, ok := data[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Skip("Invalid assistant data format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantID, ok := assistant["assistant_id"].(string)
|
||||||
|
if !ok || assistantID == "" {
|
||||||
|
t.Skip("Assistant ID not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with locale parameter - should return translated data
|
||||||
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID+"?locale=zh-cn", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
t.Logf("Successfully retrieved assistant with zh-cn locale (translated)")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetAssistantBuiltInFieldsFiltered", func(t *testing.T) {
|
||||||
|
// Get a built-in assistant
|
||||||
|
listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?built_in=true&pagesize=1", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
listResp, err := http.DefaultClient.Do(listReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer listResp.Body.Close()
|
||||||
|
|
||||||
|
var listResponse map[string]interface{}
|
||||||
|
err = json.NewDecoder(listResp.Body).Decode(&listResponse)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
data, hasData := listResponse["data"].([]interface{})
|
||||||
|
if !hasData || len(data) == 0 {
|
||||||
|
t.Skip("No built-in assistants available for testing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistant, ok := data[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Skip("Invalid assistant data format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantID, ok := assistant["assistant_id"].(string)
|
||||||
|
if !ok || assistantID == "" {
|
||||||
|
t.Skip("Assistant ID not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the built-in assistant detail
|
||||||
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
detailData, hasDetailData := response["data"].(map[string]interface{})
|
||||||
|
assert.True(t, hasDetailData, "Response should have 'data' field")
|
||||||
|
|
||||||
|
// Verify built-in flag
|
||||||
|
builtIn, hasBuiltIn := detailData["built_in"].(bool)
|
||||||
|
if hasBuiltIn && builtIn {
|
||||||
|
// Check that sensitive fields are filtered
|
||||||
|
prompts := detailData["prompts"]
|
||||||
|
workflow := detailData["workflow"]
|
||||||
|
tools := detailData["tools"]
|
||||||
|
kb := detailData["kb"]
|
||||||
|
mcp := detailData["mcp"]
|
||||||
|
options := detailData["options"]
|
||||||
|
|
||||||
|
// These should be nil or absent for built-in assistants
|
||||||
|
assert.Nil(t, prompts, "Built-in assistant should not expose prompts")
|
||||||
|
assert.Nil(t, workflow, "Built-in assistant should not expose workflow")
|
||||||
|
assert.Nil(t, tools, "Built-in assistant should not expose tools")
|
||||||
|
assert.Nil(t, kb, "Built-in assistant should not expose kb")
|
||||||
|
assert.Nil(t, mcp, "Built-in assistant should not expose mcp")
|
||||||
|
assert.Nil(t, options, "Built-in assistant should not expose options")
|
||||||
|
|
||||||
|
t.Logf("Built-in assistant sensitive fields correctly filtered")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetAssistantNotFound", func(t *testing.T) {
|
||||||
|
// Test with non-existent assistant ID
|
||||||
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/non-existent-id-12345", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Should return 404 Not Found
|
||||||
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode, "Should return 404 for non-existent assistant")
|
||||||
|
|
||||||
|
var errorResponse map[string]interface{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&errorResponse)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, errorResponse, "error", "Error response should have 'error' field")
|
||||||
|
t.Logf("Correctly returned 404 for non-existent assistant")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetAssistantUnauthorized", func(t *testing.T) {
|
||||||
|
// Test without authentication
|
||||||
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/some-id", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// No Authorization header
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "Should return 401 without authentication")
|
||||||
|
t.Logf("Correctly rejected unauthorized request")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetAssistantEmptyID", func(t *testing.T) {
|
||||||
|
// Test with empty assistant ID (should be caught by router)
|
||||||
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// This should either redirect to list endpoint or return 404
|
||||||
|
// Depends on router configuration
|
||||||
|
assert.True(t, resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusMovedPermanently || resp.StatusCode == http.StatusOK)
|
||||||
|
t.Logf("Handled empty assistant ID (status: %d)", resp.StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetAssistantPermissionFiltering tests that permission-based filtering works for single assistant retrieval
|
||||||
|
func TestGetAssistantPermissionFiltering(t *testing.T) {
|
||||||
|
serverURL := testutils.Prepare(t)
|
||||||
|
defer testutils.Clean()
|
||||||
|
|
||||||
|
baseURL := ""
|
||||||
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||||
|
baseURL = openapi.Server.Config.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test client
|
||||||
|
client := testutils.RegisterTestClient(t, "Agent Detail Permission Test Client", []string{"https://localhost/callback"})
|
||||||
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||||
|
|
||||||
|
// User 1 token
|
||||||
|
token1 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||||
|
|
||||||
|
t.Run("UserCanAccessPublicAssistant", func(t *testing.T) {
|
||||||
|
// Get a public assistant (if available)
|
||||||
|
listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=10", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
||||||
|
|
||||||
|
listResp, err := http.DefaultClient.Do(listReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer listResp.Body.Close()
|
||||||
|
|
||||||
|
var listResponse map[string]interface{}
|
||||||
|
err = json.NewDecoder(listResp.Body).Decode(&listResponse)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
data, hasData := listResponse["data"].([]interface{})
|
||||||
|
if !hasData || len(data) == 0 {
|
||||||
|
t.Skip("No assistants available for testing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a public assistant
|
||||||
|
var publicAssistantID string
|
||||||
|
for _, item := range data {
|
||||||
|
assistant, ok := item.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isPublic, hasPublic := assistant["public"].(bool)
|
||||||
|
assistantID, hasID := assistant["assistant_id"].(string)
|
||||||
|
|
||||||
|
if hasPublic && isPublic && hasID && assistantID != "" {
|
||||||
|
publicAssistantID = assistantID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if publicAssistantID == "" {
|
||||||
|
t.Skip("No public assistants available for testing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test accessing public assistant
|
||||||
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+publicAssistantID, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "User should be able to access public assistant")
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
t.Logf("User successfully accessed public assistant: %s", publicAssistantID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UserCanAccessOwnAssistant", func(t *testing.T) {
|
||||||
|
// This test assumes user has their own assistants
|
||||||
|
// In a real scenario, you would create a test assistant first
|
||||||
|
listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
||||||
|
|
||||||
|
listResp, err := http.DefaultClient.Do(listReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer listResp.Body.Close()
|
||||||
|
|
||||||
|
var listResponse map[string]interface{}
|
||||||
|
err = json.NewDecoder(listResp.Body).Decode(&listResponse)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
data, hasData := listResponse["data"].([]interface{})
|
||||||
|
if !hasData || len(data) == 0 {
|
||||||
|
t.Skip("No assistants available for testing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistant, ok := data[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Skip("Invalid assistant data format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantID, ok := assistant["assistant_id"].(string)
|
||||||
|
if !ok || assistantID == "" {
|
||||||
|
t.Skip("Assistant ID not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test accessing own assistant
|
||||||
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Should be able to access (either own assistant or permission allows)
|
||||||
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden)
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
t.Logf("User successfully accessed assistant: %s", assistantID)
|
||||||
|
} else {
|
||||||
|
t.Logf("User does not have permission to access assistant: %s", assistantID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetAssistantResponseStructure tests that the response structure matches expectations
|
||||||
|
func TestGetAssistantResponseStructure(t *testing.T) {
|
||||||
|
serverURL := testutils.Prepare(t)
|
||||||
|
defer testutils.Clean()
|
||||||
|
|
||||||
|
baseURL := ""
|
||||||
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||||
|
baseURL = openapi.Server.Config.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
client := testutils.RegisterTestClient(t, "Agent Detail Response Test Client", []string{"https://localhost/callback"})
|
||||||
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||||
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||||
|
|
||||||
|
t.Run("ResponseHasCorrectStructure", func(t *testing.T) {
|
||||||
|
// Get a valid assistant ID first
|
||||||
|
listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
listResp, err := http.DefaultClient.Do(listReq)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer listResp.Body.Close()
|
||||||
|
|
||||||
|
var listResponse map[string]interface{}
|
||||||
|
err = json.NewDecoder(listResp.Body).Decode(&listResponse)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
data, hasData := listResponse["data"].([]interface{})
|
||||||
|
if !hasData || len(data) == 0 {
|
||||||
|
t.Skip("No assistants available for testing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistant, ok := data[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Skip("Invalid assistant data format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantID, ok := assistant["assistant_id"].(string)
|
||||||
|
if !ok || assistantID == "" {
|
||||||
|
t.Skip("Assistant ID not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get assistant detail
|
||||||
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify response structure
|
||||||
|
assert.Contains(t, response, "data", "Response should have 'data' field")
|
||||||
|
|
||||||
|
// Verify data is an object (not an array)
|
||||||
|
detailData, ok := response["data"].(map[string]interface{})
|
||||||
|
assert.True(t, ok, "Data field should be an object")
|
||||||
|
assert.NotNil(t, detailData, "Data should not be nil")
|
||||||
|
|
||||||
|
// Verify essential assistant fields
|
||||||
|
assert.Contains(t, detailData, "assistant_id", "Assistant should have assistant_id")
|
||||||
|
assert.Contains(t, detailData, "name", "Assistant should have name")
|
||||||
|
assert.Contains(t, detailData, "type", "Assistant should have type")
|
||||||
|
|
||||||
|
t.Logf("Response structure is correct for assistant: %s", assistantID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// BenchmarkListAssistants benchmarks the list assistants endpoint
|
// BenchmarkListAssistants benchmarks the list assistants endpoint
|
||||||
func BenchmarkListAssistants(b *testing.B) {
|
func BenchmarkListAssistants(b *testing.B) {
|
||||||
// Convert testing.B to testing.T for Prepare/Clean
|
// Convert testing.B to testing.T for Prepare/Clean
|
||||||
|
|
@ -951,3 +1478,66 @@ func BenchmarkListAssistants(b *testing.B) {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BenchmarkGetAssistant benchmarks the get assistant detail endpoint
|
||||||
|
func BenchmarkGetAssistant(b *testing.B) {
|
||||||
|
// Convert testing.B to testing.T for Prepare/Clean
|
||||||
|
t := &testing.T{}
|
||||||
|
serverURL := testutils.Prepare(t)
|
||||||
|
defer testutils.Clean()
|
||||||
|
|
||||||
|
baseURL := ""
|
||||||
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||||
|
baseURL = openapi.Server.Config.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
client := testutils.RegisterTestClient(t, "Agent Detail Benchmark Client", []string{"https://localhost/callback"})
|
||||||
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
||||||
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||||
|
|
||||||
|
// Get a valid assistant ID for benchmarking
|
||||||
|
listReq, _ := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil)
|
||||||
|
listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
listResp, err := http.DefaultClient.Do(listReq)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to get assistant list: %v", err)
|
||||||
|
}
|
||||||
|
defer listResp.Body.Close()
|
||||||
|
|
||||||
|
var listResponse map[string]interface{}
|
||||||
|
json.NewDecoder(listResp.Body).Decode(&listResponse)
|
||||||
|
|
||||||
|
data, hasData := listResponse["data"].([]interface{})
|
||||||
|
if !hasData || len(data) == 0 {
|
||||||
|
b.Skip("No assistants available for benchmarking")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistant, ok := data[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
b.Skip("Invalid assistant data format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantID, ok := assistant["assistant_id"].(string)
|
||||||
|
if !ok || assistantID == "" {
|
||||||
|
b.Skip("Assistant ID not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset timer after setup
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
// Run benchmark
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
req, _ := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Request failed: %v", err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue