Enhance assistant model and query handling
- Added permission fields (YaoCreatedBy, YaoUpdatedBy, YaoTeamID, YaoTenantID) to the AssistantModel for better tracking of ownership and access control. - Introduced a custom QueryFilter in the AssistantFilter struct to allow for flexible permission-based filtering of assistants. - Updated the GetAssistants method to apply the custom query filter, improving the retrieval logic based on user permissions. - Enhanced test coverage for the new QueryFilter functionality, ensuring accurate filtering of assistants based on various criteria.
This commit is contained in:
parent
c61688d8be
commit
bed36b754f
11 changed files with 1736 additions and 27 deletions
|
|
@ -339,6 +339,20 @@ func ToAssistantModel(v interface{}) (*AssistantModel, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Permission fields
|
||||
if createdBy, ok := data["__yao_created_by"].(string); ok {
|
||||
model.YaoCreatedBy = createdBy
|
||||
}
|
||||
if updatedBy, ok := data["__yao_updated_by"].(string); ok {
|
||||
model.YaoUpdatedBy = updatedBy
|
||||
}
|
||||
if teamID, ok := data["__yao_team_id"].(string); ok {
|
||||
model.YaoTeamID = teamID
|
||||
}
|
||||
if tenantID, ok := data["__yao_tenant_id"].(string); ok {
|
||||
model.YaoTenantID = tenantID
|
||||
}
|
||||
|
||||
return model, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
84
agent/store/types/fields.go
Normal file
84
agent/store/types/fields.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package types
|
||||
|
||||
import "github.com/yaoapp/kun/log"
|
||||
|
||||
// AssistantAllowedFields defines the whitelist of fields that can be selected for assistants
|
||||
var AssistantAllowedFields = map[string]bool{
|
||||
"id": true,
|
||||
"assistant_id": true,
|
||||
"type": true,
|
||||
"name": true,
|
||||
"avatar": true,
|
||||
"connector": true,
|
||||
"description": true,
|
||||
"path": true,
|
||||
"sort": true,
|
||||
"built_in": true,
|
||||
"placeholder": true,
|
||||
"options": true,
|
||||
"prompts": true,
|
||||
"workflow": true,
|
||||
"kb": true,
|
||||
"mcp": true,
|
||||
"tools": true,
|
||||
"tags": true,
|
||||
"readonly": true,
|
||||
"public": true,
|
||||
"share": true,
|
||||
"locales": true,
|
||||
"automated": true,
|
||||
"mentionable": true,
|
||||
"created_at": true,
|
||||
"updated_at": true,
|
||||
"__yao_created_by": true,
|
||||
"__yao_updated_by": true,
|
||||
"__yao_team_id": true,
|
||||
"__yao_tenant_id": true,
|
||||
}
|
||||
|
||||
// AssistantDefaultFields defines the default fields to select for assistants when no specific fields are requested
|
||||
var AssistantDefaultFields = []string{
|
||||
"assistant_id",
|
||||
"type",
|
||||
"name",
|
||||
"avatar",
|
||||
"connector",
|
||||
"description",
|
||||
"sort",
|
||||
"built_in",
|
||||
"readonly",
|
||||
"public",
|
||||
"share",
|
||||
"automated",
|
||||
"mentionable",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
}
|
||||
|
||||
// ValidateAssistantFields validates and filters assistant select fields against the whitelist
|
||||
// Returns the filtered fields. If input is empty, returns empty slice (meaning no restriction).
|
||||
// If all fields are invalid, returns default fields as fallback.
|
||||
func ValidateAssistantFields(fields []string) []string {
|
||||
// If no fields specified, return empty slice (no restriction)
|
||||
if len(fields) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Filter out any fields not in the whitelist
|
||||
sanitized := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if AssistantAllowedFields[field] {
|
||||
sanitized = append(sanitized, field)
|
||||
} else {
|
||||
log.Warn("Ignoring invalid assistant select field: %s", field)
|
||||
}
|
||||
}
|
||||
|
||||
// If all fields were filtered out, return default fields as fallback
|
||||
if len(sanitized) == 0 {
|
||||
log.Warn("All assistant select fields were invalid, using default fields")
|
||||
return AssistantDefaultFields
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
180
agent/store/types/fields_test.go
Normal file
180
agent/store/types/fields_test.go
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateAssistantFields(t *testing.T) {
|
||||
t.Run("EmptyInput_ReturnsEmptySlice", func(t *testing.T) {
|
||||
result := ValidateAssistantFields([]string{})
|
||||
if len(result) != 0 {
|
||||
t.Errorf("Expected empty slice, got %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NilInput_ReturnsEmptySlice", func(t *testing.T) {
|
||||
result := ValidateAssistantFields(nil)
|
||||
if len(result) != 0 {
|
||||
t.Errorf("Expected empty slice, got %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidFields_ReturnsFiltered", func(t *testing.T) {
|
||||
input := []string{"assistant_id", "name", "type"}
|
||||
result := ValidateAssistantFields(input)
|
||||
expected := []string{"assistant_id", "name", "type"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MixedValidInvalidFields_ReturnsOnlyValid", func(t *testing.T) {
|
||||
input := []string{"assistant_id", "invalid_field", "name", "malicious_column"}
|
||||
result := ValidateAssistantFields(input)
|
||||
expected := []string{"assistant_id", "name"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AllInvalidFields_ReturnsDefaultFields", func(t *testing.T) {
|
||||
input := []string{"invalid1", "invalid2", "malicious"}
|
||||
result := ValidateAssistantFields(input)
|
||||
if !reflect.DeepEqual(result, AssistantDefaultFields) {
|
||||
t.Errorf("Expected default fields when all invalid, got %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PermissionFields_AreAllowed", func(t *testing.T) {
|
||||
input := []string{"__yao_created_by", "__yao_team_id", "assistant_id"}
|
||||
result := ValidateAssistantFields(input)
|
||||
expected := []string{"__yao_created_by", "__yao_team_id", "assistant_id"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AllAllowedFields_AreInWhitelist", func(t *testing.T) {
|
||||
// Verify all default fields are in the allowed list
|
||||
for _, field := range AssistantDefaultFields {
|
||||
if !AssistantAllowedFields[field] {
|
||||
t.Errorf("Default field %s is not in allowed fields", field)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SQLInjectionAttempt_IsFiltered", func(t *testing.T) {
|
||||
input := []string{"assistant_id", "name; DROP TABLE assistants;--", "type"}
|
||||
result := ValidateAssistantFields(input)
|
||||
expected := []string{"assistant_id", "type"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected SQL injection attempt to be filtered, got %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DuplicateFields_AreKept", func(t *testing.T) {
|
||||
input := []string{"assistant_id", "name", "assistant_id", "name"}
|
||||
result := ValidateAssistantFields(input)
|
||||
// Duplicates should be kept as-is (validation doesn't deduplicate)
|
||||
expected := []string{"assistant_id", "name", "assistant_id", "name"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAssistantAllowedFields(t *testing.T) {
|
||||
t.Run("ContainsBasicFields", func(t *testing.T) {
|
||||
requiredFields := []string{
|
||||
"assistant_id",
|
||||
"name",
|
||||
"type",
|
||||
"connector",
|
||||
"description",
|
||||
}
|
||||
for _, field := range requiredFields {
|
||||
if !AssistantAllowedFields[field] {
|
||||
t.Errorf("Required field %s is missing from allowed fields", field)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ContainsPermissionFields", func(t *testing.T) {
|
||||
permissionFields := []string{
|
||||
"__yao_created_by",
|
||||
"__yao_updated_by",
|
||||
"__yao_team_id",
|
||||
"__yao_tenant_id",
|
||||
}
|
||||
for _, field := range permissionFields {
|
||||
if !AssistantAllowedFields[field] {
|
||||
t.Errorf("Permission field %s is missing from allowed fields", field)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ContainsComplexFields", func(t *testing.T) {
|
||||
complexFields := []string{
|
||||
"options",
|
||||
"prompts",
|
||||
"workflow",
|
||||
"kb",
|
||||
"mcp",
|
||||
"tools",
|
||||
"placeholder",
|
||||
"locales",
|
||||
}
|
||||
for _, field := range complexFields {
|
||||
if !AssistantAllowedFields[field] {
|
||||
t.Errorf("Complex field %s is missing from allowed fields", field)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAssistantDefaultFields(t *testing.T) {
|
||||
t.Run("ContainsEssentialFields", func(t *testing.T) {
|
||||
essentialFields := []string{
|
||||
"assistant_id",
|
||||
"name",
|
||||
"type",
|
||||
}
|
||||
|
||||
defaultFieldsMap := make(map[string]bool)
|
||||
for _, field := range AssistantDefaultFields {
|
||||
defaultFieldsMap[field] = true
|
||||
}
|
||||
|
||||
for _, field := range essentialFields {
|
||||
if !defaultFieldsMap[field] {
|
||||
t.Errorf("Essential field %s is missing from default fields", field)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DoesNotContainSensitiveFields", func(t *testing.T) {
|
||||
// Default fields should not include complex/large fields by default
|
||||
sensitiveFields := []string{
|
||||
"options",
|
||||
"prompts",
|
||||
"workflow",
|
||||
"kb",
|
||||
"mcp",
|
||||
"tools",
|
||||
"placeholder",
|
||||
"locales",
|
||||
}
|
||||
|
||||
defaultFieldsMap := make(map[string]bool)
|
||||
for _, field := range AssistantDefaultFields {
|
||||
defaultFieldsMap[field] = true
|
||||
}
|
||||
|
||||
for _, field := range sensitiveFields {
|
||||
if defaultFieldsMap[field] {
|
||||
t.Errorf("Large/complex field %s should not be in default fields", field)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package types
|
||||
|
||||
import "github.com/yaoapp/yao/agent/i18n"
|
||||
import (
|
||||
"github.com/yaoapp/xun/dbal/query"
|
||||
"github.com/yaoapp/yao/agent/i18n"
|
||||
)
|
||||
|
||||
// Setting represents the conversation configuration structure
|
||||
// Used to configure basic conversation parameters including connector, user field, table name, etc.
|
||||
|
|
@ -48,18 +51,19 @@ type ChatGroupResponse struct {
|
|||
// AssistantFilter represents the assistant filter structure
|
||||
// Used for filtering and pagination when retrieving assistant lists
|
||||
type AssistantFilter struct {
|
||||
Tags []string `json:"tags,omitempty"` // Filter by tags
|
||||
Type string `json:"type,omitempty"` // Filter by type
|
||||
Keywords string `json:"keywords,omitempty"` // Search in name and description
|
||||
Connector string `json:"connector,omitempty"` // Filter by connector
|
||||
AssistantID string `json:"assistant_id,omitempty"` // Filter by assistant ID
|
||||
AssistantIDs []string `json:"assistant_ids,omitempty"` // Filter by assistant IDs
|
||||
Mentionable *bool `json:"mentionable,omitempty"` // Filter by mentionable status
|
||||
Automated *bool `json:"automated,omitempty"` // Filter by automation status
|
||||
BuiltIn *bool `json:"built_in,omitempty"` // Filter by built-in status
|
||||
Page int `json:"page,omitempty"` // Page number, starting from 1
|
||||
PageSize int `json:"pagesize,omitempty"` // Items per page
|
||||
Select []string `json:"select,omitempty"` // Fields to return, returns all fields if empty
|
||||
Tags []string `json:"tags,omitempty"` // Filter by tags
|
||||
Type string `json:"type,omitempty"` // Filter by type
|
||||
Keywords string `json:"keywords,omitempty"` // Search in name and description
|
||||
Connector string `json:"connector,omitempty"` // Filter by connector
|
||||
AssistantID string `json:"assistant_id,omitempty"` // Filter by assistant ID
|
||||
AssistantIDs []string `json:"assistant_ids,omitempty"` // Filter by assistant IDs
|
||||
Mentionable *bool `json:"mentionable,omitempty"` // Filter by mentionable status
|
||||
Automated *bool `json:"automated,omitempty"` // Filter by automation status
|
||||
BuiltIn *bool `json:"built_in,omitempty"` // Filter by built-in status
|
||||
Page int `json:"page,omitempty"` // Page number, starting from 1
|
||||
PageSize int `json:"pagesize,omitempty"` // Items per page
|
||||
Select []string `json:"select,omitempty"` // Fields to return, returns all fields if empty
|
||||
QueryFilter func(query.Query) `json:"-"` // Custom query function for permission filtering (not serialized)
|
||||
}
|
||||
|
||||
// AssistantList represents the paginated assistant list response structure
|
||||
|
|
|
|||
|
|
@ -258,6 +258,11 @@ func (conv *Xun) GetAssistants(filter types.AssistantFilter, locale ...string) (
|
|||
qb.Where("built_in", *filter.BuiltIn)
|
||||
}
|
||||
|
||||
// Apply custom query filter function (for permission filtering)
|
||||
if filter.QueryFilter != nil {
|
||||
qb.Where(filter.QueryFilter)
|
||||
}
|
||||
|
||||
// Set defaults for pagination
|
||||
if filter.PageSize <= 0 {
|
||||
filter.PageSize = 20
|
||||
|
|
@ -284,14 +289,17 @@ func (conv *Xun) GetAssistants(filter types.AssistantFilter, locale ...string) (
|
|||
prevPage = 0
|
||||
}
|
||||
|
||||
// Apply select fields if provided
|
||||
// Apply select fields with security validation (only if fields are explicitly specified)
|
||||
if len(filter.Select) > 0 {
|
||||
selectFields := make([]interface{}, len(filter.Select))
|
||||
for i, field := range filter.Select {
|
||||
// ValidateAssistantFields will validate fields against whitelist
|
||||
sanitized := types.ValidateAssistantFields(filter.Select)
|
||||
selectFields := make([]interface{}, len(sanitized))
|
||||
for i, field := range sanitized {
|
||||
selectFields[i] = field
|
||||
}
|
||||
qb.Select(selectFields...)
|
||||
}
|
||||
// If no select fields specified, query will return all fields (SELECT *)
|
||||
|
||||
// Get paginated results
|
||||
rows, err := qb.OrderBy("sort", "asc").
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yaoapp/xun/dbal/query"
|
||||
"github.com/yaoapp/yao/agent/i18n"
|
||||
"github.com/yaoapp/yao/agent/store/types"
|
||||
"github.com/yaoapp/yao/config"
|
||||
|
|
@ -1338,6 +1339,251 @@ func TestGetAssistantsWithLocale(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// TestGetAssistantsWithQueryFilter tests using QueryFilter for permission filtering
|
||||
func TestGetAssistantsWithQueryFilter(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
store, err := NewXun(types.Setting{
|
||||
Connector: "default",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
// Create test assistants with different permission settings
|
||||
assistants := []types.AssistantModel{
|
||||
{
|
||||
Name: "Public Assistant",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Description: "Public assistant visible to all",
|
||||
Tags: []string{"query-filter-test"},
|
||||
Public: true,
|
||||
Share: "private",
|
||||
YaoCreatedBy: "user-1",
|
||||
YaoTeamID: "team-1",
|
||||
},
|
||||
{
|
||||
Name: "Team Shared Assistant",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Description: "Team shared assistant",
|
||||
Tags: []string{"query-filter-test"},
|
||||
Public: false,
|
||||
Share: "team",
|
||||
YaoCreatedBy: "user-2",
|
||||
YaoTeamID: "team-1",
|
||||
},
|
||||
{
|
||||
Name: "Private Assistant Owner",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Description: "Private assistant owned by user-1",
|
||||
Tags: []string{"query-filter-test"},
|
||||
Public: false,
|
||||
Share: "private",
|
||||
YaoCreatedBy: "user-1",
|
||||
YaoTeamID: "team-1",
|
||||
},
|
||||
{
|
||||
Name: "Private Assistant Other",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Description: "Private assistant owned by user-3",
|
||||
Tags: []string{"query-filter-test"},
|
||||
Public: false,
|
||||
Share: "private",
|
||||
YaoCreatedBy: "user-3",
|
||||
YaoTeamID: "team-2",
|
||||
},
|
||||
}
|
||||
|
||||
createdIDs := []string{}
|
||||
for _, asst := range assistants {
|
||||
id, err := store.SaveAssistant(&asst)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create assistant: %v", err)
|
||||
}
|
||||
createdIDs = append(createdIDs, id)
|
||||
}
|
||||
|
||||
t.Run("FilterByPublic", func(t *testing.T) {
|
||||
// QueryFilter: only public assistants
|
||||
response, err := store.GetAssistants(types.AssistantFilter{
|
||||
Tags: []string{"query-filter-test"},
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
QueryFilter: func(qb query.Query) {
|
||||
qb.Where("public", true)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get public assistants: %v", err)
|
||||
}
|
||||
|
||||
if len(response.Data) != 1 {
|
||||
t.Errorf("Expected 1 public assistant, got %d", len(response.Data))
|
||||
}
|
||||
|
||||
if len(response.Data) > 0 && response.Data[0].Name != "Public Assistant" {
|
||||
t.Errorf("Expected 'Public Assistant', got '%s'", response.Data[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilterByTeamAndShare", func(t *testing.T) {
|
||||
// QueryFilter: team-1 assistants that are shared with team
|
||||
response, err := store.GetAssistants(types.AssistantFilter{
|
||||
Tags: []string{"query-filter-test"},
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
QueryFilter: func(qb query.Query) {
|
||||
qb.Where("__yao_team_id", "team-1").
|
||||
Where("share", "team")
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get team shared assistants: %v", err)
|
||||
}
|
||||
|
||||
if len(response.Data) != 1 {
|
||||
t.Errorf("Expected 1 team shared assistant, got %d", len(response.Data))
|
||||
}
|
||||
|
||||
if len(response.Data) > 0 && response.Data[0].Name != "Team Shared Assistant" {
|
||||
t.Errorf("Expected 'Team Shared Assistant', got '%s'", response.Data[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilterByOwner", func(t *testing.T) {
|
||||
// QueryFilter: assistants created by user-1
|
||||
response, err := store.GetAssistants(types.AssistantFilter{
|
||||
Tags: []string{"query-filter-test"},
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
QueryFilter: func(qb query.Query) {
|
||||
qb.Where("__yao_created_by", "user-1")
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user-1 assistants: %v", err)
|
||||
}
|
||||
|
||||
if len(response.Data) != 2 {
|
||||
t.Errorf("Expected 2 assistants for user-1, got %d", len(response.Data))
|
||||
}
|
||||
|
||||
for _, asst := range response.Data {
|
||||
if asst.YaoCreatedBy != "user-1" {
|
||||
t.Errorf("Expected creator 'user-1', got '%s'", asst.YaoCreatedBy)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ComplexQueryFilter", func(t *testing.T) {
|
||||
// Complex QueryFilter: (public = true) OR (team_id = team-1 AND (created_by = user-1 OR share = team))
|
||||
response, err := store.GetAssistants(types.AssistantFilter{
|
||||
Tags: []string{"query-filter-test"},
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
QueryFilter: func(qb query.Query) {
|
||||
qb.Where(func(qb query.Query) {
|
||||
// Public assistants
|
||||
qb.Where("public", true)
|
||||
}).OrWhere(func(qb query.Query) {
|
||||
// Team assistants where user is creator or shared with team
|
||||
qb.Where("__yao_team_id", "team-1").Where(func(qb query.Query) {
|
||||
qb.Where("__yao_created_by", "user-1").
|
||||
OrWhere("share", "team")
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get filtered assistants: %v", err)
|
||||
}
|
||||
|
||||
// Should find: Public Assistant, Team Shared Assistant, Private Assistant Owner
|
||||
if len(response.Data) != 3 {
|
||||
t.Errorf("Expected 3 assistants, got %d", len(response.Data))
|
||||
}
|
||||
|
||||
// Verify we got the right assistants
|
||||
names := make(map[string]bool)
|
||||
for _, asst := range response.Data {
|
||||
names[asst.Name] = true
|
||||
}
|
||||
|
||||
expectedNames := []string{"Public Assistant", "Team Shared Assistant", "Private Assistant Owner"}
|
||||
for _, name := range expectedNames {
|
||||
if !names[name] {
|
||||
t.Errorf("Expected to find '%s' in results", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Should NOT find Private Assistant Other
|
||||
if names["Private Assistant Other"] {
|
||||
t.Error("Should not find 'Private Assistant Other' in results")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("QueryFilterWithNullCheck", func(t *testing.T) {
|
||||
// QueryFilter: assistants where team_id is null
|
||||
response, err := store.GetAssistants(types.AssistantFilter{
|
||||
Tags: []string{"query-filter-test"},
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
QueryFilter: func(qb query.Query) {
|
||||
qb.WhereNull("__yao_team_id")
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get assistants with null team_id: %v", err)
|
||||
}
|
||||
|
||||
// All test assistants have team_id, so should find 0
|
||||
if len(response.Data) != 0 {
|
||||
t.Errorf("Expected 0 assistants with null team_id, got %d", len(response.Data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("QueryFilterCombinedWithOtherFilters", func(t *testing.T) {
|
||||
// Combine QueryFilter with other filters
|
||||
response, err := store.GetAssistants(types.AssistantFilter{
|
||||
Tags: []string{"query-filter-test"},
|
||||
Connector: "openai",
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
QueryFilter: func(qb query.Query) {
|
||||
qb.Where("public", true)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get combined filtered assistants: %v", err)
|
||||
}
|
||||
|
||||
// Should only find public openai assistants
|
||||
if len(response.Data) != 1 {
|
||||
t.Errorf("Expected 1 assistant, got %d", len(response.Data))
|
||||
}
|
||||
|
||||
if len(response.Data) > 0 {
|
||||
if response.Data[0].Connector != "openai" {
|
||||
t.Errorf("Expected connector 'openai', got '%s'", response.Data[0].Connector)
|
||||
}
|
||||
if !response.Data[0].Public {
|
||||
t.Error("Expected public assistant")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
for _, id := range createdIDs {
|
||||
_ = store.DeleteAssistant(id)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssistantCompleteWorkflow tests a complete workflow
|
||||
func TestAssistantCompleteWorkflow(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
// Get the Agent instance
|
||||
n := agent.GetAgent()
|
||||
|
||||
// Create agents group with OAuth guard
|
||||
agents := group.Group("/agents")
|
||||
agents.Use(oauth.Guard)
|
||||
// Create assistants group with OAuth guard
|
||||
assistants := group.Group("/assistants")
|
||||
assistants.Use(oauth.Guard)
|
||||
|
||||
// Agent CRUD - Standard REST endpoints
|
||||
agents.GET("/", n.HandleAssistantList) // GET /agents - List agents
|
||||
agents.POST("/", n.HandleAssistantSave) // POST /agents - Create/Update agent
|
||||
agents.GET("/tags", n.HandleAssistantTags) // GET /agents/tags - Get all agent tags
|
||||
agents.GET("/:id", n.HandleAssistantDetail) // GET /agents/:id - Get agent details
|
||||
agents.DELETE("/:id", n.HandleAssistantDelete) // DELETE /agents/:id - Delete agent
|
||||
// Assistant CRUD - Standard REST endpoints
|
||||
assistants.GET("/", ListAssistants) // GET /assistants - List assistants
|
||||
assistants.POST("/", n.HandleAssistantSave) // POST /assistants - Create/Update assistant
|
||||
assistants.GET("/tags", n.HandleAssistantTags) // GET /assistants/tags - Get all assistant tags
|
||||
assistants.GET("/:id", n.HandleAssistantDetail) // GET /assistants/:id - Get assistant details
|
||||
assistants.DELETE("/:id", n.HandleAssistantDelete) // DELETE /assistants/:id - Delete assistant
|
||||
|
||||
// Agent Actions
|
||||
agents.POST("/:id/call", n.HandleAssistantCall) // POST /agents/:id/call - Execute agent API
|
||||
// Assistant Actions
|
||||
assistants.POST("/:id/call", n.HandleAssistantCall) // POST /assistants/:id/call - Execute assistant API
|
||||
}
|
||||
|
|
|
|||
161
openapi/agent/assistant.go
Normal file
161
openapi/agent/assistant.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/yao/agent"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
||||
// ListAssistants lists assistants with pagination and filtering
|
||||
func ListAssistants(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
|
||||
}
|
||||
|
||||
// Parse pagination parameters
|
||||
page := 1
|
||||
if pageStr := c.Query("page"); pageStr != "" {
|
||||
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
||||
page = p
|
||||
}
|
||||
}
|
||||
|
||||
pagesize := 20
|
||||
if pagesizeStr := c.Query("pagesize"); pagesizeStr != "" {
|
||||
if ps, err := strconv.Atoi(pagesizeStr); err == nil && ps > 0 && ps <= 100 {
|
||||
pagesize = ps
|
||||
}
|
||||
}
|
||||
|
||||
// Validate pagination
|
||||
if err := ValidatePagination(page, pagesize); err != nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse select parameter
|
||||
var selectFields []string
|
||||
if selectParam := strings.TrimSpace(c.Query("select")); selectParam != "" {
|
||||
requestedFields := strings.Split(selectParam, ",")
|
||||
for _, field := range requestedFields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field != "" && availableAssistantFields[field] {
|
||||
selectFields = append(selectFields, field)
|
||||
}
|
||||
}
|
||||
// If no valid fields found, use default
|
||||
if len(selectFields) == 0 {
|
||||
selectFields = defaultAssistantFields
|
||||
}
|
||||
} else {
|
||||
selectFields = defaultAssistantFields
|
||||
}
|
||||
|
||||
// Parse filter parameters
|
||||
keywords := strings.TrimSpace(c.Query("keywords"))
|
||||
typeParam := strings.TrimSpace(c.Query("type"))
|
||||
if typeParam == "" {
|
||||
typeParam = "assistant" // Default type
|
||||
}
|
||||
connector := strings.TrimSpace(c.Query("connector"))
|
||||
assistantID := strings.TrimSpace(c.Query("assistant_id"))
|
||||
|
||||
// Parse assistant IDs (multiple)
|
||||
var assistantIDs []string
|
||||
if assistantIDsParam := c.Query("assistant_ids"); assistantIDsParam != "" {
|
||||
assistantIDs = strings.Split(assistantIDsParam, ",")
|
||||
// Trim spaces
|
||||
for i, id := range assistantIDs {
|
||||
assistantIDs[i] = strings.TrimSpace(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
var tags []string
|
||||
if tagsParam := c.Query("tags"); tagsParam != "" {
|
||||
tags = strings.Split(tagsParam, ",")
|
||||
// Trim spaces
|
||||
for i, tag := range tags {
|
||||
tags[i] = strings.TrimSpace(tag)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse boolean filters
|
||||
var builtIn, mentionable, automated *bool
|
||||
if builtInParam := c.Query("built_in"); builtInParam != "" {
|
||||
builtIn = parseBoolValue(builtInParam)
|
||||
}
|
||||
if mentionableParam := c.Query("mentionable"); mentionableParam != "" {
|
||||
mentionable = parseBoolValue(mentionableParam)
|
||||
}
|
||||
if automatedParam := c.Query("automated"); automatedParam != "" {
|
||||
automated = parseBoolValue(automatedParam)
|
||||
}
|
||||
|
||||
// Note: public and share filters are not yet supported in AssistantFilter
|
||||
// They would need to be added to the store layer for proper filtering
|
||||
|
||||
// Parse locale
|
||||
locale := "en-us" // Default locale
|
||||
if loc := c.Query("locale"); loc != "" {
|
||||
locale = strings.ToLower(strings.TrimSpace(loc))
|
||||
}
|
||||
|
||||
// Build filter using the existing AssistantFilter structure
|
||||
filter := BuildAssistantFilter(AssistantFilterParams{
|
||||
Page: page,
|
||||
PageSize: pagesize,
|
||||
Keywords: keywords,
|
||||
Type: typeParam,
|
||||
Connector: connector,
|
||||
AssistantID: assistantID,
|
||||
AssistantIDs: assistantIDs,
|
||||
Tags: tags,
|
||||
SelectFields: selectFields,
|
||||
BuiltIn: builtIn,
|
||||
Mentionable: mentionable,
|
||||
Automated: automated,
|
||||
})
|
||||
|
||||
// Apply permission-based filtering (Scope filtering)
|
||||
filter.QueryFilter = AuthQueryFilter(c, authInfo)
|
||||
|
||||
// Use the existing GetAssistants method from agent.Store
|
||||
result, err := agentInstance.Store.GetAssistants(filter, locale)
|
||||
if err != nil {
|
||||
log.Error("Failed to list assistants: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to list assistants: " + err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Filter sensitive fields for built-in assistants
|
||||
// For built-in assistants, clear code-level fields (prompts, workflow, tools, kb, mcp, options)
|
||||
FilterBuiltInFields(result.Data)
|
||||
|
||||
// Return the result with standard response format
|
||||
response.RespondWithSuccess(c, response.StatusOK, result)
|
||||
}
|
||||
146
openapi/agent/filter.go
Normal file
146
openapi/agent/filter.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/model"
|
||||
"github.com/yaoapp/xun/dbal/query"
|
||||
agenttypes "github.com/yaoapp/yao/agent/store/types"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
)
|
||||
|
||||
// AuthFilter applies permission-based filtering to query wheres for assistants
|
||||
// This function builds where clauses based on the user's authorization constraints
|
||||
// It supports TeamOnly and OwnerOnly constraints for data access control
|
||||
//
|
||||
// Parameters:
|
||||
// - c: gin.Context containing authorization information
|
||||
// - authInfo: authorized information extracted from the context
|
||||
//
|
||||
// Returns:
|
||||
// - []model.QueryWhere: array of where clauses to apply to the query
|
||||
func AuthFilter(c *gin.Context, authInfo *types.AuthorizedInfo) []model.QueryWhere {
|
||||
if authInfo == nil {
|
||||
return []model.QueryWhere{}
|
||||
}
|
||||
|
||||
var wheres []model.QueryWhere
|
||||
scope := authInfo.AccessScope()
|
||||
|
||||
// Team only - User can access:
|
||||
// 1. Public records (public = true)
|
||||
// 2. Records in their team where:
|
||||
// - They created the record (__yao_created_by matches)
|
||||
// - OR the record is shared with team (share = "team")
|
||||
if authInfo.Constraints.TeamOnly && authorized.IsTeamMember(c) {
|
||||
wheres = append(wheres, model.QueryWhere{
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "public", Value: true, Method: "orwhere"},
|
||||
{Wheres: []model.QueryWhere{
|
||||
{Column: "__yao_team_id", Value: scope.TeamID},
|
||||
{Wheres: []model.QueryWhere{
|
||||
{Column: "__yao_created_by", Value: scope.CreatedBy},
|
||||
{Column: "share", Value: "team", Method: "orwhere"},
|
||||
}},
|
||||
}, Method: "orwhere"},
|
||||
},
|
||||
})
|
||||
return wheres
|
||||
}
|
||||
|
||||
// Owner only - User can access:
|
||||
// 1. Public records (public = true)
|
||||
// 2. Records they created where:
|
||||
// - __yao_team_id is null (not team records)
|
||||
// - __yao_created_by matches their user ID
|
||||
if authInfo.Constraints.OwnerOnly && authInfo.UserID != "" {
|
||||
wheres = append(wheres, model.QueryWhere{
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "public", Value: true, Method: "orwhere"},
|
||||
{Wheres: []model.QueryWhere{
|
||||
{Column: "__yao_team_id", OP: "null"},
|
||||
{Column: "__yao_created_by", Value: scope.CreatedBy},
|
||||
}, Method: "orwhere"},
|
||||
},
|
||||
})
|
||||
return wheres
|
||||
}
|
||||
|
||||
return wheres
|
||||
}
|
||||
|
||||
// AuthQueryFilter returns a Query function for easy permission filtering
|
||||
// This is a convenience function that can be directly used with query.Where()
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// if filter := AuthQueryFilter(c, authInfo); filter != nil {
|
||||
// qb.Where(filter)
|
||||
// }
|
||||
func AuthQueryFilter(c *gin.Context, authInfo *types.AuthorizedInfo) func(query.Query) {
|
||||
if authInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
scope := authInfo.AccessScope()
|
||||
|
||||
// Team only - User can access:
|
||||
// 1. Public records (public = true)
|
||||
// 2. Records in their team where:
|
||||
// - They created the record (__yao_created_by matches)
|
||||
// - OR the record is shared with team (share = "team")
|
||||
if authInfo.Constraints.TeamOnly && authorized.IsTeamMember(c) {
|
||||
return func(qb query.Query) {
|
||||
qb.Where(func(qb query.Query) {
|
||||
// Public records
|
||||
qb.Where("public", true)
|
||||
}).OrWhere(func(qb query.Query) {
|
||||
// Team records where user is creator or share is team
|
||||
qb.Where("__yao_team_id", scope.TeamID).Where(func(qb query.Query) {
|
||||
qb.Where("__yao_created_by", scope.CreatedBy).
|
||||
OrWhere("share", "team")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Owner only - User can access:
|
||||
// 1. Public records (public = true)
|
||||
// 2. Records they created where:
|
||||
// - __yao_team_id is null (not team records)
|
||||
// - __yao_created_by matches their user ID
|
||||
if authInfo.Constraints.OwnerOnly && authInfo.UserID != "" {
|
||||
return func(qb query.Query) {
|
||||
qb.Where(func(qb query.Query) {
|
||||
// Public records
|
||||
qb.Where("public", true)
|
||||
}).OrWhere(func(qb query.Query) {
|
||||
// Owner records (team_id is null and created by user)
|
||||
qb.WhereNull("__yao_team_id").
|
||||
Where("__yao_created_by", scope.CreatedBy)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterBuiltInFields filters sensitive fields for built-in assistants
|
||||
// For built-in assistants, code-level fields (prompts, workflow, tools, kb, mcp, options) should be cleared
|
||||
func FilterBuiltInFields(assistants []*agenttypes.AssistantModel) {
|
||||
if assistants == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, assistant := range assistants {
|
||||
if assistant != nil && assistant.BuiltIn {
|
||||
// Clear code-level sensitive fields for built-in assistants
|
||||
assistant.Prompts = nil
|
||||
assistant.Workflow = nil
|
||||
assistant.Tools = nil
|
||||
assistant.KB = nil
|
||||
assistant.MCP = nil
|
||||
assistant.Options = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
113
openapi/agent/types.go
Normal file
113
openapi/agent/types.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
agenttypes "github.com/yaoapp/yao/agent/store/types"
|
||||
)
|
||||
|
||||
// Assistant field definitions
|
||||
var (
|
||||
// availableAssistantFields defines all available fields for security filtering
|
||||
availableAssistantFields = map[string]bool{
|
||||
"id": true, "assistant_id": true, "type": true, "name": true, "avatar": true,
|
||||
"connector": true, "description": true, "path": true, "sort": true,
|
||||
"built_in": true, "placeholder": true, "options": true, "prompts": true,
|
||||
"workflow": true, "kb": true, "mcp": true, "tools": true, "tags": true,
|
||||
"readonly": true, "public": true, "share": true, "locales": true,
|
||||
"automated": true, "mentionable": true,
|
||||
"created_at": true, "updated_at": true, "deleted_at": true,
|
||||
"__yao_created_by": true, "__yao_updated_by": true, "__yao_team_id": true,
|
||||
}
|
||||
|
||||
// defaultAssistantFields defines the default compact field list
|
||||
defaultAssistantFields = []string{
|
||||
"assistant_id", "type", "name", "avatar", "connector", "description",
|
||||
"sort", "built_in", "tags", "readonly", "public", "share",
|
||||
"automated", "mentionable", "created_at", "updated_at",
|
||||
}
|
||||
)
|
||||
|
||||
// parseBoolValue parses various string formats into a boolean pointer
|
||||
// Supports: 1, 0, "1", "0", "true", "false", etc.
|
||||
func parseBoolValue(value string) *bool {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
switch value {
|
||||
case "1", "true", "yes", "on":
|
||||
v := true
|
||||
return &v
|
||||
case "0", "false", "no", "off":
|
||||
v := false
|
||||
return &v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssistantFilterParams represents the parameters for building an AssistantFilter
|
||||
type AssistantFilterParams struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Keywords string
|
||||
Type string
|
||||
Connector string
|
||||
AssistantID string
|
||||
AssistantIDs []string
|
||||
Tags []string
|
||||
SelectFields []string
|
||||
BuiltIn *bool
|
||||
Mentionable *bool
|
||||
Automated *bool
|
||||
Public *bool
|
||||
Share string
|
||||
}
|
||||
|
||||
// BuildAssistantFilter builds an AssistantFilter from parameters
|
||||
func BuildAssistantFilter(params AssistantFilterParams) agenttypes.AssistantFilter {
|
||||
filter := agenttypes.AssistantFilter{
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
Keywords: params.Keywords,
|
||||
Tags: params.Tags,
|
||||
Type: params.Type,
|
||||
Connector: params.Connector,
|
||||
AssistantID: params.AssistantID,
|
||||
AssistantIDs: params.AssistantIDs,
|
||||
Select: params.SelectFields,
|
||||
BuiltIn: params.BuiltIn,
|
||||
Mentionable: params.Mentionable,
|
||||
Automated: params.Automated,
|
||||
}
|
||||
|
||||
// Set default type if not specified
|
||||
if filter.Type == "" {
|
||||
filter.Type = "assistant"
|
||||
}
|
||||
|
||||
// Set default pagination
|
||||
if filter.Page <= 0 {
|
||||
filter.Page = 1
|
||||
}
|
||||
if filter.PageSize <= 0 {
|
||||
filter.PageSize = 20
|
||||
}
|
||||
if filter.PageSize > 100 {
|
||||
filter.PageSize = 100
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
// ValidatePagination validates pagination parameters
|
||||
func ValidatePagination(page, pagesize int) error {
|
||||
if page < 0 {
|
||||
return fmt.Errorf("page must be positive")
|
||||
}
|
||||
if pagesize < 0 {
|
||||
return fmt.Errorf("pagesize must be positive")
|
||||
}
|
||||
if pagesize > 100 {
|
||||
return fmt.Errorf("pagesize cannot exceed 100")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
753
openapi/tests/agent/assistant_test.go
Normal file
753
openapi/tests/agent/assistant_test.go
Normal file
|
|
@ -0,0 +1,753 @@
|
|||
package openapi_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
"github.com/yaoapp/yao/openapi/tests/testutils"
|
||||
)
|
||||
|
||||
// TestListAssistants tests the assistants listing endpoint
|
||||
func TestListAssistants(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 List 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("ListAssistantsSuccess", func(t *testing.T) {
|
||||
// Test listing all assistants
|
||||
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()
|
||||
|
||||
// Expect successful response
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully retrieve assistants")
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Response should have pagination structure
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d assistants", len(data))
|
||||
} else {
|
||||
t.Logf("Successfully retrieved assistants response (data field type: %T)", response["data"])
|
||||
}
|
||||
|
||||
// Check pagination fields
|
||||
assert.Contains(t, response, "page")
|
||||
assert.Contains(t, response, "pagesize")
|
||||
assert.Contains(t, response, "total")
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithPagination", func(t *testing.T) {
|
||||
// Test with pagination parameters
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?page=1&pagesize=10", 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 pagination values
|
||||
page, hasPage := response["page"].(float64)
|
||||
pagesize, hasPagesize := response["pagesize"].(float64)
|
||||
|
||||
if hasPage && hasPagesize {
|
||||
assert.Equal(t, float64(1), page, "Page should be 1")
|
||||
assert.Equal(t, float64(10), pagesize, "Pagesize should be 10")
|
||||
t.Logf("Pagination working correctly: page=%d, pagesize=%d", int(page), int(pagesize))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithKeywords", func(t *testing.T) {
|
||||
// Test with keywords filter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?keywords=test", 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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d assistants with keywords filter", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithType", func(t *testing.T) {
|
||||
// Test with type filter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?type=assistant", 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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d assistants with type filter", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithTags", func(t *testing.T) {
|
||||
// Test with tags filter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?tags=productivity,ai", 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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d assistants with tags filter", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithBuiltInFilter", func(t *testing.T) {
|
||||
// Test with built_in filter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?built_in=true", 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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d built-in assistants", len(data))
|
||||
|
||||
// Verify that built-in assistants have sensitive fields filtered
|
||||
for _, item := range data {
|
||||
assistant, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
builtIn, hasBuiltIn := assistant["built_in"].(bool)
|
||||
if hasBuiltIn && builtIn {
|
||||
// Check that code-level fields are null or absent
|
||||
prompts := assistant["prompts"]
|
||||
workflow := assistant["workflow"]
|
||||
tools := assistant["tools"]
|
||||
kb := assistant["kb"]
|
||||
mcp := assistant["mcp"]
|
||||
options := assistant["options"]
|
||||
|
||||
// These should be nil or absent for built-in assistants
|
||||
if prompts != nil {
|
||||
t.Logf("Warning: Built-in assistant has non-nil prompts field: %v", prompts)
|
||||
}
|
||||
if workflow != nil {
|
||||
t.Logf("Warning: Built-in assistant has non-nil workflow field: %v", workflow)
|
||||
}
|
||||
if tools != nil {
|
||||
t.Logf("Warning: Built-in assistant has non-nil tools field: %v", tools)
|
||||
}
|
||||
if kb != nil {
|
||||
t.Logf("Warning: Built-in assistant has non-nil kb field: %v", kb)
|
||||
}
|
||||
if mcp != nil {
|
||||
t.Logf("Warning: Built-in assistant has non-nil mcp field: %v", mcp)
|
||||
}
|
||||
if options != nil {
|
||||
t.Logf("Warning: Built-in assistant has non-nil options field: %v", options)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithMentionableFilter", func(t *testing.T) {
|
||||
// Test with mentionable filter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?mentionable=true", 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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d mentionable assistants", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithAutomatedFilter", func(t *testing.T) {
|
||||
// Test with automated filter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?automated=false", 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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d non-automated assistants", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithSelectFields", func(t *testing.T) {
|
||||
// Test with select parameter to limit returned fields
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?select=assistant_id,name,avatar,type", 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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData && len(data) > 0 {
|
||||
// Check first assistant to verify field selection worked
|
||||
assistant, ok := data[0].(map[string]interface{})
|
||||
if ok {
|
||||
t.Logf("Assistant fields returned: %+v", assistant)
|
||||
// Note: The actual fields returned depend on the implementation
|
||||
// This test verifies the select parameter is accepted without error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithInvalidSelectFields", func(t *testing.T) {
|
||||
// Test with invalid select fields (should be filtered by whitelist)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?select=invalid_field,malicious_sql", 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 still return 200, but with default/filtered fields
|
||||
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 handled invalid select fields by using whitelist")
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithMultipleFilters", func(t *testing.T) {
|
||||
// Test with multiple filter parameters combined
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?type=assistant&built_in=false&mentionable=true&page=1&pagesize=5", 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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d assistants with multiple filters", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithConnector", func(t *testing.T) {
|
||||
// Test with connector filter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?connector=openai", 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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d assistants with connector filter", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithAssistantID", func(t *testing.T) {
|
||||
// Test with specific assistant_id filter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?assistant_id=test_assistant", 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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d assistants with assistant_id filter", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithAssistantIDs", func(t *testing.T) {
|
||||
// Test with multiple assistant_ids filter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?assistant_ids=assistant1,assistant2,assistant3", 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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("Successfully retrieved %d assistants with assistant_ids filter", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithInvalidPagination", func(t *testing.T) {
|
||||
// Test with invalid pagination parameters (should use defaults)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?page=-1&pagesize=1000", 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 error for invalid pagination
|
||||
if resp.StatusCode == http.StatusBadRequest {
|
||||
var errorResponse map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&errorResponse)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, errorResponse, "error")
|
||||
t.Logf("Correctly rejected invalid pagination parameters")
|
||||
} else {
|
||||
// Or apply default/corrected values
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
t.Logf("Applied default/corrected pagination values")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAssistantEndpointsUnauthorized tests that endpoints return 401 when not authenticated
|
||||
func TestAssistantEndpointsUnauthorized(t *testing.T) {
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
endpoints := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{"GET", "/agent/assistants"},
|
||||
{"GET", "/agent/assistants?page=1&pagesize=10"},
|
||||
{"GET", "/agent/assistants?keywords=test"},
|
||||
{"GET", "/agent/assistants?type=assistant"},
|
||||
{"GET", "/agent/assistants?built_in=true"},
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
t.Run(fmt.Sprintf("Unauthorized_%s_%s", endpoint.method, endpoint.path), func(t *testing.T) {
|
||||
req, err := http.NewRequest(endpoint.method, serverURL+baseURL+endpoint.path, 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)
|
||||
|
||||
t.Logf("Correctly rejected unauthorized request to %s %s", endpoint.method, endpoint.path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssistantPermissionFiltering tests that permission-based filtering works correctly
|
||||
func TestAssistantPermissionFiltering(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 two different test users with tokens
|
||||
client := testutils.RegisterTestClient(t, "Agent 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")
|
||||
|
||||
// User 2 token (different user)
|
||||
token2 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
t.Run("User1CanSeeOwnAssistants", func(t *testing.T) {
|
||||
// User 1 should see their own assistants
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants", 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)
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("User 1 can see %d assistants", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("User2SeesFilteredResults", func(t *testing.T) {
|
||||
// User 2 should see different assistants (permission filtering applied)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+token2.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)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
t.Logf("User 2 can see %d assistants (permission filtering applied)", len(data))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAssistantResponseStructure tests that the response structure is correct
|
||||
func TestAssistantResponseStructure(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 Response Structure 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) {
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?page=1&pagesize=5", 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 matches OpenAPI standard
|
||||
assert.Contains(t, response, "data", "Response should have 'data' field")
|
||||
assert.Contains(t, response, "page", "Response should have 'page' field")
|
||||
assert.Contains(t, response, "pagesize", "Response should have 'pagesize' field")
|
||||
assert.Contains(t, response, "total", "Response should have 'total' field")
|
||||
|
||||
// Verify data is an array
|
||||
data, ok := response["data"].([]interface{})
|
||||
assert.True(t, ok, "Data field should be an array")
|
||||
t.Logf("Response structure is correct with %d assistants", len(data))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAssistantLocaleSupport tests that locale parameter works correctly
|
||||
func TestAssistantLocaleSupport(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 Locale 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")
|
||||
|
||||
locales := []string{"en-us", "zh-cn", "ja-jp", "de-de", "fr-fr"}
|
||||
|
||||
for _, locale := range locales {
|
||||
t.Run(fmt.Sprintf("LocaleSupport_%s", locale), func(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?locale="+locale, 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 assistants with locale: %s", locale)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssistantEdgeCases tests edge cases and boundary conditions
|
||||
func TestAssistantEdgeCases(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 Edge Cases 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("EmptyKeywordsParameter", func(t *testing.T) {
|
||||
// Test with empty keywords parameter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?keywords=", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
t.Logf("Handled empty keywords parameter correctly")
|
||||
})
|
||||
|
||||
t.Run("EmptyTagsParameter", func(t *testing.T) {
|
||||
// Test with empty tags parameter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?tags=", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
t.Logf("Handled empty tags parameter correctly")
|
||||
})
|
||||
|
||||
t.Run("VeryLongKeywords", func(t *testing.T) {
|
||||
// Test with very long keywords string
|
||||
longKeywords := string(make([]byte, 1000))
|
||||
for i := range longKeywords {
|
||||
longKeywords = longKeywords[:i] + "test"
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?keywords="+longKeywords[:500], nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should handle gracefully (either return results or error)
|
||||
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest)
|
||||
t.Logf("Handled very long keywords parameter (status: %d)", resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("SpecialCharactersInKeywords", func(t *testing.T) {
|
||||
// Test with special characters in keywords
|
||||
specialKeywords := "test&special=chars<>\"';--"
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?keywords="+specialKeywords, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
t.Logf("Handled special characters in keywords correctly")
|
||||
})
|
||||
|
||||
t.Run("MaxPageSize", func(t *testing.T) {
|
||||
// Test with maximum page size (should be capped at 100)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=100", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
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)
|
||||
|
||||
pagesize, ok := response["pagesize"].(float64)
|
||||
if ok {
|
||||
assert.LessOrEqual(t, int(pagesize), 100, "Pagesize should be capped at 100")
|
||||
t.Logf("Correctly capped pagesize at %d", int(pagesize))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkListAssistants benchmarks the list assistants endpoint
|
||||
func BenchmarkListAssistants(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 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")
|
||||
|
||||
// Reset timer after setup
|
||||
b.ResetTimer()
|
||||
|
||||
// Run benchmark
|
||||
for i := 0; i < b.N; i++ {
|
||||
req, _ := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?page=1&pagesize=20", 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