- Introduced a new endpoint `GET /teams/:team_id/members/:member_id/profile` to allow users to retrieve member profile information, including display name, bio, avatar, and email. - Implemented the `GinMemberGetProfile` handler to process profile retrieval requests with appropriate authentication and error handling. - Added business logic in `memberGetProfile` to ensure only authenticated users can access their own profile data. - Expanded test cases in `TestMemberProfileGet` to validate various scenarios for profile retrieval, ensuring comprehensive coverage for authentication and data validation.
2489 lines
78 KiB
Go
2489 lines
78 KiB
Go
package user_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/yaoapp/kun/maps"
|
|
"github.com/yaoapp/yao/openapi"
|
|
"github.com/yaoapp/yao/openapi/tests/testutils"
|
|
)
|
|
|
|
// TestMemberList tests the GET /user/teams/:team_id/members endpoint
|
|
func TestMemberList(t *testing.T) {
|
|
// Initialize test environment
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register a test client for OAuth authentication
|
|
testClient := testutils.RegisterTestClient(t, "Member List Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
|
|
|
// Obtain access token for authenticated requests
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Create a test team first
|
|
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Member List Test Team")
|
|
teamID := getTeamID(createdTeam)
|
|
|
|
// Create some test members and robots for filtering tests
|
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
|
|
|
// Create a robot member for member_type filtering
|
|
robotBody := map[string]interface{}{
|
|
"name": "Test Robot " + testUUID,
|
|
"email": fmt.Sprintf("test-robot-%s@test.com", testUUID),
|
|
"role": "member",
|
|
"prompt": "You are a test robot for filtering",
|
|
}
|
|
robotBodyBytes, _ := json.Marshal(robotBody)
|
|
robotReq, _ := http.NewRequest("POST", serverURL+baseURL+"/user/teams/"+teamID+"/members/robots", bytes.NewBuffer(robotBodyBytes))
|
|
robotReq.Header.Set("Content-Type", "application/json")
|
|
robotReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
client := &http.Client{}
|
|
robotResp, err := client.Do(robotReq)
|
|
if err == nil && robotResp != nil {
|
|
robotResp.Body.Close()
|
|
if robotResp.StatusCode != 201 {
|
|
t.Logf("Warning: Failed to create robot member for testing (status=%d)", robotResp.StatusCode)
|
|
}
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
teamID string
|
|
query string
|
|
headers map[string]string
|
|
expectCode int
|
|
expectMsg string
|
|
validateFn func(*testing.T, map[string]interface{}) // Optional validation function
|
|
}{
|
|
{
|
|
"list members without authentication",
|
|
teamID,
|
|
"",
|
|
map[string]string{},
|
|
401,
|
|
"should require authentication",
|
|
nil,
|
|
},
|
|
{
|
|
"list members with valid token",
|
|
teamID,
|
|
"",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should return team members with default sorting (is_owner desc, status desc, created_at desc)",
|
|
func(t *testing.T, response map[string]interface{}) {
|
|
// Verify default sorting: is_owner desc first, then status desc
|
|
if data, ok := response["data"].([]interface{}); ok && len(data) > 1 {
|
|
foundNonOwner := false
|
|
foundActive := false
|
|
|
|
for _, item := range data {
|
|
member := item.(map[string]interface{})
|
|
|
|
// Check is_owner sorting (owners first)
|
|
isOwner := false
|
|
if ownerVal, ok := member["is_owner"]; ok {
|
|
switch v := ownerVal.(type) {
|
|
case float64:
|
|
isOwner = v == 1
|
|
case int:
|
|
isOwner = v == 1
|
|
case bool:
|
|
isOwner = v
|
|
}
|
|
}
|
|
|
|
if isOwner {
|
|
assert.False(t, foundNonOwner, "Owners should come before non-owners")
|
|
} else {
|
|
foundNonOwner = true
|
|
}
|
|
|
|
// Check status sorting (pending before active) among non-owners
|
|
if !isOwner {
|
|
status := member["status"].(string)
|
|
if status == "pending" {
|
|
assert.False(t, foundActive, "Pending members should come before active members")
|
|
} else if status == "active" {
|
|
foundActive = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"list members with pagination",
|
|
teamID,
|
|
"?page=1&pagesize=10",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should handle pagination parameters",
|
|
func(t *testing.T, response map[string]interface{}) {
|
|
assert.Equal(t, float64(1), response["page"], "Should have correct page number")
|
|
assert.Equal(t, float64(10), response["pagesize"], "Should have correct pagesize")
|
|
},
|
|
},
|
|
{
|
|
"list members with status filter",
|
|
teamID,
|
|
"?status=active",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should filter by status",
|
|
func(t *testing.T, response map[string]interface{}) {
|
|
if data, ok := response["data"].([]interface{}); ok {
|
|
for _, item := range data {
|
|
member := item.(map[string]interface{})
|
|
assert.Equal(t, "active", member["status"], "All members should have active status")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"list members filtered by member_type user",
|
|
teamID,
|
|
"?member_type=user",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should filter by member_type=user",
|
|
func(t *testing.T, response map[string]interface{}) {
|
|
if data, ok := response["data"].([]interface{}); ok {
|
|
for _, item := range data {
|
|
member := item.(map[string]interface{})
|
|
assert.Equal(t, "user", member["member_type"], "All members should be user type")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"list members filtered by member_type robot",
|
|
teamID,
|
|
"?member_type=robot",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should filter by member_type=robot",
|
|
func(t *testing.T, response map[string]interface{}) {
|
|
if data, ok := response["data"].([]interface{}); ok {
|
|
for _, item := range data {
|
|
member := item.(map[string]interface{})
|
|
assert.Equal(t, "robot", member["member_type"], "All members should be robot type")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"list members filtered by role_id",
|
|
teamID,
|
|
"?role_id=owner:free",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should filter by role_id",
|
|
func(t *testing.T, response map[string]interface{}) {
|
|
if data, ok := response["data"].([]interface{}); ok {
|
|
for _, item := range data {
|
|
member := item.(map[string]interface{})
|
|
assert.Equal(t, "owner:free", member["role_id"], "All members should have owner:free role")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"list members with order by created_at asc",
|
|
teamID,
|
|
"?order=created_at+asc",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should sort by is_owner desc, status desc, then created_at ascending",
|
|
func(t *testing.T, response map[string]interface{}) {
|
|
// Verify owner and status sorting priority
|
|
if data, ok := response["data"].([]interface{}); ok && len(data) > 1 {
|
|
foundNonOwner := false
|
|
for _, item := range data {
|
|
member := item.(map[string]interface{})
|
|
|
|
isOwner := false
|
|
if ownerVal, ok := member["is_owner"]; ok {
|
|
switch v := ownerVal.(type) {
|
|
case float64:
|
|
isOwner = v == 1
|
|
case int:
|
|
isOwner = v == 1
|
|
case bool:
|
|
isOwner = v
|
|
}
|
|
}
|
|
|
|
if isOwner {
|
|
assert.False(t, foundNonOwner, "Owners should come before non-owners even with custom sorting")
|
|
} else {
|
|
foundNonOwner = true
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"list members with order by joined_at desc",
|
|
teamID,
|
|
"?order=joined_at+desc",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should sort by is_owner desc, status desc, then joined_at descending",
|
|
nil,
|
|
},
|
|
{
|
|
"list members with order by joined_at (default desc)",
|
|
teamID,
|
|
"?order=joined_at",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should sort by is_owner desc, status desc, then joined_at with default desc direction",
|
|
nil,
|
|
},
|
|
{
|
|
"list members with field selection",
|
|
teamID,
|
|
"?fields=id,user_id,member_type,role_id,status",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should return only selected fields",
|
|
func(t *testing.T, response map[string]interface{}) {
|
|
if data, ok := response["data"].([]interface{}); ok && len(data) > 0 {
|
|
member := data[0].(map[string]interface{})
|
|
// Should have selected fields
|
|
assert.Contains(t, member, "id", "Should have id field")
|
|
assert.Contains(t, member, "user_id", "Should have user_id field")
|
|
assert.Contains(t, member, "member_type", "Should have member_type field")
|
|
assert.Contains(t, member, "role_id", "Should have role_id field")
|
|
assert.Contains(t, member, "status", "Should have status field")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"list members with invalid status value",
|
|
teamID,
|
|
"?status=invalid_status",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should reject invalid status value",
|
|
nil,
|
|
},
|
|
{
|
|
"list members with invalid member_type value",
|
|
teamID,
|
|
"?member_type=invalid_type",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should reject invalid member_type value",
|
|
nil,
|
|
},
|
|
{
|
|
"list members with invalid order field",
|
|
teamID,
|
|
"?order=invalid_field+desc",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should reject invalid order field",
|
|
nil,
|
|
},
|
|
{
|
|
"list members with invalid order direction",
|
|
teamID,
|
|
"?order=created_at+invalid",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should reject invalid order direction",
|
|
nil,
|
|
},
|
|
{
|
|
"list members with combined filters",
|
|
teamID,
|
|
"?status=active&member_type=user&order=created_at+asc&page=1&pagesize=5",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should handle combined filters and sorting",
|
|
func(t *testing.T, response map[string]interface{}) {
|
|
assert.Equal(t, float64(1), response["page"], "Should have correct page number")
|
|
assert.Equal(t, float64(5), response["pagesize"], "Should have correct pagesize")
|
|
if data, ok := response["data"].([]interface{}); ok {
|
|
for _, item := range data {
|
|
member := item.(map[string]interface{})
|
|
assert.Equal(t, "active", member["status"], "All members should have active status")
|
|
assert.Equal(t, "user", member["member_type"], "All members should be user type")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"list members of non-existent team",
|
|
"non-existent-team-id",
|
|
"",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent team",
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members" + tc.query
|
|
req, err := http.NewRequest("GET", requestURL, nil)
|
|
assert.NoError(t, err, "Should create HTTP request")
|
|
|
|
// Add headers
|
|
for key, value := range tc.headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
httpClient := &http.Client{}
|
|
resp, err := httpClient.Do(req)
|
|
assert.NoError(t, err, "HTTP request should succeed")
|
|
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read response body")
|
|
|
|
if resp.StatusCode == 200 {
|
|
// Parse response as pagination result
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(body, &response)
|
|
assert.NoError(t, err, "Should parse JSON response")
|
|
|
|
// Check pagination structure
|
|
assert.Contains(t, response, "data", "Should have data array")
|
|
assert.Contains(t, response, "total", "Should have total count")
|
|
assert.Contains(t, response, "page", "Should have page number")
|
|
assert.Contains(t, response, "pagesize", "Should have pagesize")
|
|
|
|
// Run custom validation if provided
|
|
if tc.validateFn != nil {
|
|
tc.validateFn(t, response)
|
|
}
|
|
}
|
|
|
|
t.Logf("Member list test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMemberGet tests the GET /user/teams/:team_id/members/:member_id endpoint
|
|
func TestMemberGet(t *testing.T) {
|
|
// Initialize test environment
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register a test client for OAuth authentication
|
|
testClient := testutils.RegisterTestClient(t, "Member Get Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
|
|
|
// Obtain access token for authenticated requests
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Create a test team and get owner member ID
|
|
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Member Get Test Team")
|
|
teamID := getTeamID(createdTeam)
|
|
|
|
// Get member list to find the owner member ID
|
|
ownerMemberID := getOwnerMemberID(t, serverURL, baseURL, teamID, tokenInfo.AccessToken)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
teamID string
|
|
memberID string
|
|
headers map[string]string
|
|
expectCode int
|
|
expectMsg string
|
|
}{
|
|
{
|
|
"get member without authentication",
|
|
teamID,
|
|
ownerMemberID,
|
|
map[string]string{},
|
|
401,
|
|
"should require authentication",
|
|
},
|
|
{
|
|
"get existing member",
|
|
teamID,
|
|
ownerMemberID,
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should return member details",
|
|
},
|
|
{
|
|
"get non-existent member",
|
|
teamID,
|
|
"999999",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent member",
|
|
},
|
|
{
|
|
"get member from non-existent team",
|
|
"non-existent-team-id",
|
|
ownerMemberID,
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent team",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members/" + tc.memberID
|
|
req, err := http.NewRequest("GET", requestURL, nil)
|
|
assert.NoError(t, err, "Should create HTTP request")
|
|
|
|
// Add headers
|
|
for key, value := range tc.headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
assert.NoError(t, err, "HTTP request should succeed")
|
|
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read response body")
|
|
|
|
if resp.StatusCode == 200 {
|
|
// Parse response as member object
|
|
var member map[string]interface{}
|
|
err = json.Unmarshal(body, &member)
|
|
assert.NoError(t, err, "Should parse JSON response")
|
|
|
|
// Verify member structure
|
|
assert.Contains(t, member, "id", "Should have member ID")
|
|
assert.Contains(t, member, "team_id", "Should have team_id")
|
|
assert.Contains(t, member, "user_id", "Should have user_id")
|
|
assert.Contains(t, member, "role_id", "Should have role_id")
|
|
assert.Contains(t, member, "status", "Should have status")
|
|
assert.Contains(t, member, "created_at", "Should have created_at")
|
|
assert.Contains(t, member, "updated_at", "Should have updated_at")
|
|
|
|
// Verify values
|
|
assert.Equal(t, teamID, member["team_id"], "Should have correct team_id")
|
|
assert.Equal(t, tokenInfo.UserID, member["user_id"], "Should have correct user_id")
|
|
}
|
|
|
|
t.Logf("Member get test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMemberUpdate tests the PUT /user/teams/:team_id/members/:member_id endpoint
|
|
func TestMemberUpdate(t *testing.T) {
|
|
// Initialize test environment
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register a test client for OAuth authentication
|
|
testClient := testutils.RegisterTestClient(t, "Member Update Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
|
|
|
// Obtain access token for authenticated requests
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
setupFunc func() (string, string) // Returns (teamID, memberID)
|
|
body map[string]interface{}
|
|
headers map[string]string
|
|
expectCode int
|
|
expectMsg string
|
|
}{
|
|
{
|
|
"update member without authentication",
|
|
func() (string, string) {
|
|
team := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Update Test Team 1")
|
|
teamID := getTeamID(team)
|
|
memberID := createTestMember(t, serverURL, baseURL, teamID, tokenInfo.AccessToken, "test-update-user-1")
|
|
return teamID, memberID
|
|
},
|
|
map[string]interface{}{
|
|
"role_id": "admin",
|
|
},
|
|
map[string]string{},
|
|
401,
|
|
"should require authentication",
|
|
},
|
|
{
|
|
"update member role",
|
|
func() (string, string) {
|
|
team := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Update Test Team 2")
|
|
teamID := getTeamID(team)
|
|
memberID := createTestMember(t, serverURL, baseURL, teamID, tokenInfo.AccessToken, "test-update-user-2")
|
|
return teamID, memberID
|
|
},
|
|
map[string]interface{}{
|
|
"role_id": "admin",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update member role",
|
|
},
|
|
{
|
|
"update member status",
|
|
func() (string, string) {
|
|
team := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Update Test Team 3")
|
|
teamID := getTeamID(team)
|
|
memberID := createTestMember(t, serverURL, baseURL, teamID, tokenInfo.AccessToken, "test-update-user-3")
|
|
return teamID, memberID
|
|
},
|
|
map[string]interface{}{
|
|
"status": "inactive",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update member status",
|
|
},
|
|
{
|
|
"update non-existent member",
|
|
func() (string, string) {
|
|
team := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Update Test Team 5")
|
|
teamID := getTeamID(team)
|
|
return teamID, "999999"
|
|
},
|
|
map[string]interface{}{
|
|
"role_id": "admin",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent member",
|
|
},
|
|
{
|
|
"update member in non-existent team",
|
|
func() (string, string) {
|
|
team := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Update Test Team 6")
|
|
teamID := getTeamID(team)
|
|
memberID := createTestMember(t, serverURL, baseURL, teamID, tokenInfo.AccessToken, "test-update-user-5")
|
|
return "non-existent-team-id", memberID
|
|
},
|
|
map[string]interface{}{
|
|
"role_id": "admin",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent team",
|
|
},
|
|
{
|
|
"update member with invalid JSON",
|
|
func() (string, string) {
|
|
team := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Update Test Team 7")
|
|
teamID := getTeamID(team)
|
|
memberID := createTestMember(t, serverURL, baseURL, teamID, tokenInfo.AccessToken, "test-update-user-6")
|
|
return teamID, memberID
|
|
},
|
|
nil, // Will send invalid JSON
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should handle invalid JSON",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
teamID, memberID := tc.setupFunc()
|
|
requestURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
|
|
|
var req *http.Request
|
|
var err error
|
|
|
|
if tc.body == nil {
|
|
// Send invalid JSON for invalid JSON test case
|
|
req, err = http.NewRequest("PUT", requestURL, bytes.NewBufferString("invalid json"))
|
|
} else {
|
|
bodyBytes, _ := json.Marshal(tc.body)
|
|
req, err = http.NewRequest("PUT", requestURL, bytes.NewBuffer(bodyBytes))
|
|
}
|
|
assert.NoError(t, err, "Should create HTTP request")
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Add headers
|
|
for key, value := range tc.headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
assert.NoError(t, err, "HTTP request should succeed")
|
|
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read response body")
|
|
|
|
if resp.StatusCode == 200 {
|
|
// Parse response as success message
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(body, &response)
|
|
assert.NoError(t, err, "Should parse JSON response")
|
|
|
|
assert.Contains(t, response, "message", "Should have success message")
|
|
assert.Equal(t, "Member updated successfully", response["message"], "Should have correct success message")
|
|
}
|
|
|
|
t.Logf("Member update test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMemberDelete tests the DELETE /user/teams/:team_id/members/:member_id endpoint
|
|
func TestMemberDelete(t *testing.T) {
|
|
// Initialize test environment
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register a test client for OAuth authentication
|
|
testClient := testutils.RegisterTestClient(t, "Member Delete Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
|
|
|
// Obtain access token for authenticated requests
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Create teams and members for testing deletion
|
|
createMemberForDeletion := func(name string) (string, string) {
|
|
team := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Delete Test Team "+name)
|
|
teamID := getTeamID(team)
|
|
memberID := createTestMember(t, serverURL, baseURL, teamID, tokenInfo.AccessToken, "test-delete-user-"+name)
|
|
return teamID, memberID
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
setupFunc func() (string, string) // Returns (teamID, memberID)
|
|
headers map[string]string
|
|
expectCode int
|
|
expectMsg string
|
|
}{
|
|
{
|
|
"delete member without authentication",
|
|
func() (string, string) { return createMemberForDeletion("1") },
|
|
map[string]string{},
|
|
401,
|
|
"should require authentication",
|
|
},
|
|
{
|
|
"delete existing member",
|
|
func() (string, string) { return createMemberForDeletion("2") },
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should delete member successfully",
|
|
},
|
|
{
|
|
"delete non-existent member",
|
|
func() (string, string) {
|
|
team := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Delete Test Team 3")
|
|
return getTeamID(team), "999999"
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent member",
|
|
},
|
|
{
|
|
"delete member from non-existent team",
|
|
func() (string, string) {
|
|
_, memberID := createMemberForDeletion("4")
|
|
return "non-existent-team-id", memberID
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent team",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
teamID, memberID := tc.setupFunc()
|
|
requestURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
|
|
|
req, err := http.NewRequest("DELETE", requestURL, nil)
|
|
assert.NoError(t, err, "Should create HTTP request")
|
|
|
|
// Add headers
|
|
for key, value := range tc.headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
assert.NoError(t, err, "HTTP request should succeed")
|
|
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read response body")
|
|
|
|
if resp.StatusCode == 200 {
|
|
// Parse response as success message
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(body, &response)
|
|
assert.NoError(t, err, "Should parse JSON response")
|
|
|
|
assert.Contains(t, response, "message", "Should have success message")
|
|
assert.Equal(t, "Member removed successfully", response["message"], "Should have correct success message")
|
|
}
|
|
|
|
t.Logf("Member delete test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMemberPermissionVerification tests permission verification for member operations
|
|
func TestMemberPermissionVerification(t *testing.T) {
|
|
// Initialize test environment
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test clients for different users
|
|
ownerClient := testutils.RegisterTestClient(t, "Owner Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, ownerClient.ClientID)
|
|
|
|
nonOwnerClient := testutils.RegisterTestClient(t, "Non-Owner Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, nonOwnerClient.ClientID)
|
|
|
|
// Obtain access tokens
|
|
ownerToken := testutils.ObtainAccessToken(t, serverURL, ownerClient.ClientID, ownerClient.ClientSecret, "https://localhost/callback", "openid profile")
|
|
nonOwnerToken := testutils.ObtainAccessToken(t, serverURL, nonOwnerClient.ClientID, nonOwnerClient.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Create a team with owner
|
|
createdTeam := createTestTeam(t, serverURL, baseURL, ownerToken.AccessToken, "Permission Test Team")
|
|
teamID := getTeamID(createdTeam)
|
|
|
|
// Add non-owner as a member
|
|
memberID := createTestMember(t, serverURL, baseURL, teamID, ownerToken.AccessToken, nonOwnerToken.UserID)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
endpoint string
|
|
method string
|
|
token string
|
|
expectCode int
|
|
expectMsg string
|
|
}{
|
|
{
|
|
"owner can list members",
|
|
"/user/teams/" + teamID + "/members",
|
|
"GET",
|
|
ownerToken.AccessToken,
|
|
200,
|
|
"owner should be able to list members",
|
|
},
|
|
{
|
|
"member can list members",
|
|
"/user/teams/" + teamID + "/members",
|
|
"GET",
|
|
nonOwnerToken.AccessToken,
|
|
200,
|
|
"member should be able to list members",
|
|
},
|
|
{
|
|
"owner can get member details",
|
|
"/user/teams/" + teamID + "/members/" + memberID,
|
|
"GET",
|
|
ownerToken.AccessToken,
|
|
200,
|
|
"owner should be able to get member details",
|
|
},
|
|
{
|
|
"member can get member details",
|
|
"/user/teams/" + teamID + "/members/" + memberID,
|
|
"GET",
|
|
nonOwnerToken.AccessToken,
|
|
200,
|
|
"member should be able to get member details",
|
|
},
|
|
{
|
|
"owner can update members",
|
|
"/user/teams/" + teamID + "/members/" + memberID,
|
|
"PUT",
|
|
ownerToken.AccessToken,
|
|
200,
|
|
"owner should be able to update members",
|
|
},
|
|
{
|
|
"member cannot update members",
|
|
"/user/teams/" + teamID + "/members/" + memberID,
|
|
"PUT",
|
|
nonOwnerToken.AccessToken,
|
|
403,
|
|
"member should not be able to update members",
|
|
},
|
|
{
|
|
"owner can delete members",
|
|
"/user/teams/" + teamID + "/members/" + memberID,
|
|
"DELETE",
|
|
ownerToken.AccessToken,
|
|
200,
|
|
"owner should be able to delete members",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
requestURL := serverURL + baseURL + tc.endpoint
|
|
|
|
var req *http.Request
|
|
var err error
|
|
|
|
// Create request body for POST/PUT methods
|
|
if tc.method == "POST" {
|
|
body := map[string]interface{}{
|
|
"user_id": "test-permission-user",
|
|
"role_id": "member",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
req, err = http.NewRequest(tc.method, requestURL, bytes.NewBuffer(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else if tc.method == "PUT" {
|
|
body := map[string]interface{}{
|
|
"role_id": "admin",
|
|
}
|
|
bodyBytes, _ := json.Marshal(body)
|
|
req, err = http.NewRequest(tc.method, requestURL, bytes.NewBuffer(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req, err = http.NewRequest(tc.method, requestURL, nil)
|
|
}
|
|
|
|
assert.NoError(t, err, "Should create HTTP request")
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tc.token)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
assert.NoError(t, err, "HTTP request should succeed")
|
|
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read response body")
|
|
|
|
t.Logf("Permission test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// createTestTeam creates a team for testing and returns the team data
|
|
func createTestTeam(t *testing.T, serverURL, baseURL, accessToken, teamName string) map[string]interface{} {
|
|
createTeamBody := map[string]interface{}{
|
|
"name": teamName,
|
|
"description": "Team created for testing purposes",
|
|
"role_id": "system:root", // Use system:root role which includes all scopes
|
|
}
|
|
|
|
bodyBytes, err := json.Marshal(createTeamBody)
|
|
assert.NoError(t, err, "Should marshal team creation body")
|
|
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/user/teams", bytes.NewBuffer(bodyBytes))
|
|
assert.NoError(t, err, "Should create team creation request")
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
assert.NoError(t, err, "Should send team creation request")
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, 201, resp.StatusCode, "Should create team successfully")
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read team creation response")
|
|
|
|
var team map[string]interface{}
|
|
err = json.Unmarshal(body, &team)
|
|
assert.NoError(t, err, "Should parse team creation response")
|
|
|
|
return team
|
|
}
|
|
|
|
// createTestMember creates a member for testing using provider directly (no API call).
|
|
// This is the recommended approach since direct member creation endpoint was removed.
|
|
// Members should normally be added via invitation flow or robot creation endpoint.
|
|
// Returns the member_id (global unique identifier).
|
|
func createTestMember(t *testing.T, serverURL, baseURL, teamID, accessToken, userID string) string {
|
|
// Get user provider for direct database operations
|
|
provider := testutils.GetUserProvider(t)
|
|
ctx := context.Background()
|
|
|
|
// Create member data using maps.MapStrAny (required by UserProvider interface)
|
|
memberData := maps.MapStrAny{
|
|
"team_id": teamID,
|
|
"user_id": userID,
|
|
"member_type": "user",
|
|
"role_id": "team:member",
|
|
"status": "active",
|
|
}
|
|
|
|
// Create member directly in database
|
|
memberID, err := provider.CreateMember(ctx, memberData)
|
|
assert.NoError(t, err, "Should create member in database")
|
|
assert.NotEmpty(t, memberID, "Member ID should not be empty")
|
|
|
|
t.Logf("Created test member directly in database: user_id=%s, member_id=%s, team_id=%s", userID, memberID, teamID)
|
|
|
|
// Return member_id (global unique identifier used in API)
|
|
return memberID
|
|
}
|
|
|
|
// getOwnerMemberID gets the member_id of the team owner (global unique identifier)
|
|
func getOwnerMemberID(t *testing.T, serverURL, baseURL, teamID, accessToken string) string {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/user/teams/"+teamID+"/members", nil)
|
|
assert.NoError(t, err, "Should create member list request")
|
|
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
assert.NoError(t, err, "Should send member list request")
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, 200, resp.StatusCode, "Should get members successfully")
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read member list response")
|
|
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(body, &response)
|
|
assert.NoError(t, err, "Should parse member list response")
|
|
|
|
data, ok := response["data"].([]interface{})
|
|
assert.True(t, ok, "Should have data array")
|
|
assert.Greater(t, len(data), 0, "Should have at least one member")
|
|
|
|
// Find the owner member and return their member_id
|
|
for _, item := range data {
|
|
member := item.(map[string]interface{})
|
|
if role, ok := member["role_id"].(string); ok && strings.HasPrefix(role, "owner") {
|
|
memberID, ok := member["member_id"].(string)
|
|
if !ok {
|
|
t.Fatal("Owner member missing member_id")
|
|
}
|
|
return memberID
|
|
}
|
|
}
|
|
|
|
t.Fatal("Could not find owner member")
|
|
return ""
|
|
}
|
|
|
|
// TestMemberCreateRobot tests the POST /user/teams/:team_id/members/robots endpoint
|
|
func TestMemberCreateRobot(t *testing.T) {
|
|
// Initialize test environment
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register a test client for OAuth authentication
|
|
testClient := testutils.RegisterTestClient(t, "Robot Member Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
|
|
|
// Obtain access token with root permissions (required for creating robot members)
|
|
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Use UUID to ensure unique team name
|
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
|
|
|
// Create a test team
|
|
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Robot Member Test Team "+testUUID)
|
|
teamID := getTeamID(createdTeam)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
teamID string
|
|
body map[string]interface{}
|
|
headers map[string]string
|
|
expectCode int
|
|
expectMsg string
|
|
}{
|
|
{
|
|
"create robot without authentication",
|
|
teamID,
|
|
map[string]interface{}{
|
|
"name": "Test Robot",
|
|
"email": "robot@test.com",
|
|
"role": "member",
|
|
"prompt": "You are a helpful assistant",
|
|
},
|
|
map[string]string{},
|
|
401,
|
|
"should require authentication",
|
|
},
|
|
{
|
|
"create robot with all fields",
|
|
teamID,
|
|
map[string]interface{}{
|
|
"name": "AI Assistant Full",
|
|
"avatar": fmt.Sprintf("https://example.com/avatars/ai-full-%s.png", testUUID),
|
|
"email": fmt.Sprintf("ai-full-%s@test.com", testUUID),
|
|
"robot_email": fmt.Sprintf("robot-full-%s@robot.test.com", testUUID),
|
|
"authorized_senders": []string{"user1@test.com", "user2@test.com"},
|
|
"email_filter_rules": []string{".*@company\\.com", ".*@partner\\.com"},
|
|
"bio": "A comprehensive AI assistant",
|
|
"role": "member",
|
|
"report_to": tokenInfo.UserID,
|
|
"prompt": "You are a helpful AI assistant with full capabilities",
|
|
"llm": "gpt-4",
|
|
"agents": []string{"data-analyst", "code-reviewer"},
|
|
"mcp_tools": []string{"filesystem", "database"},
|
|
"autonomous_mode": "enabled",
|
|
"cost_limit": 100.50,
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
201,
|
|
"should create robot with all fields successfully",
|
|
},
|
|
{
|
|
"create robot with required fields only",
|
|
teamID,
|
|
map[string]interface{}{
|
|
"name": "AI Assistant Min",
|
|
"robot_email": fmt.Sprintf("ai-min-%s@test.com", testUUID),
|
|
"role": "member",
|
|
"prompt": "You are a basic assistant",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
201,
|
|
"should create robot with required fields only",
|
|
},
|
|
{
|
|
"create robot with autonomous_mode variations",
|
|
teamID,
|
|
map[string]interface{}{
|
|
"name": "AI Assistant Auto",
|
|
"robot_email": fmt.Sprintf("ai-auto-%s@test.com", testUUID),
|
|
"role": "member",
|
|
"prompt": "You are an autonomous assistant",
|
|
"autonomous_mode": "1", // Test numeric string
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
201,
|
|
"should handle autonomous_mode=1",
|
|
},
|
|
{
|
|
"create robot with disabled autonomous_mode",
|
|
teamID,
|
|
map[string]interface{}{
|
|
"name": "AI Assistant Manual",
|
|
"robot_email": fmt.Sprintf("ai-manual-%s@test.com", testUUID),
|
|
"role": "member",
|
|
"prompt": "You are a manual assistant",
|
|
"autonomous_mode": "disabled",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
201,
|
|
"should handle autonomous_mode=disabled",
|
|
},
|
|
{
|
|
"create robot without name",
|
|
teamID,
|
|
map[string]interface{}{
|
|
"robot_email": "no-name@test.com",
|
|
"role": "member",
|
|
"prompt": "You are an assistant",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should require name",
|
|
},
|
|
{
|
|
"create robot without robot_email",
|
|
teamID,
|
|
map[string]interface{}{
|
|
"name": "No Robot Email Robot",
|
|
"role": "member",
|
|
"prompt": "You are an assistant",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should require robot_email",
|
|
},
|
|
{
|
|
"create robot without role",
|
|
teamID,
|
|
map[string]interface{}{
|
|
"name": "No Role Robot",
|
|
"robot_email": "no-role@test.com",
|
|
"prompt": "You are an assistant",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should require role",
|
|
},
|
|
{
|
|
"create robot without prompt",
|
|
teamID,
|
|
map[string]interface{}{
|
|
"name": "No Prompt Robot",
|
|
"robot_email": "no-prompt@test.com",
|
|
"role": "member",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should require prompt",
|
|
},
|
|
{
|
|
"create robot with duplicate robot_email",
|
|
teamID,
|
|
map[string]interface{}{
|
|
"name": "Duplicate Robot Email Robot",
|
|
"email": fmt.Sprintf("duplicate-robot-%s@test.com", testUUID), // Different email
|
|
"robot_email": fmt.Sprintf("robot-full-%s@robot.test.com", testUUID), // Same robot_email as first successful case
|
|
"role": "member",
|
|
"prompt": "You are an assistant",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
409,
|
|
"should reject duplicate robot_email globally",
|
|
},
|
|
{
|
|
"create robot in non-existent team",
|
|
"non-existent-team-id",
|
|
map[string]interface{}{
|
|
"name": "Robot in Void",
|
|
"robot_email": "void@test.com",
|
|
"role": "member",
|
|
"prompt": "You are lost",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent team",
|
|
},
|
|
{
|
|
"create robot with invalid JSON",
|
|
teamID,
|
|
nil, // Will send invalid JSON
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should handle invalid JSON",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members/robots"
|
|
|
|
var req *http.Request
|
|
var err error
|
|
|
|
if tc.body == nil {
|
|
// Send invalid JSON for invalid JSON test case
|
|
req, err = http.NewRequest("POST", requestURL, bytes.NewBufferString("invalid json"))
|
|
} else {
|
|
bodyBytes, _ := json.Marshal(tc.body)
|
|
req, err = http.NewRequest("POST", requestURL, bytes.NewBuffer(bodyBytes))
|
|
}
|
|
assert.NoError(t, err, "Should create HTTP request")
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Add headers
|
|
for key, value := range tc.headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
assert.NoError(t, err, "HTTP request should succeed")
|
|
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read response body")
|
|
|
|
if resp.StatusCode == 201 {
|
|
// Parse response as created member
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(body, &response)
|
|
assert.NoError(t, err, "Should parse JSON response")
|
|
|
|
// Verify response structure
|
|
assert.Contains(t, response, "member_id", "Should have member_id")
|
|
assert.NotEmpty(t, response["member_id"], "Member ID should not be empty")
|
|
|
|
// Verify the member was created with correct type
|
|
memberID := toString(response["member_id"])
|
|
getMemberURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members/" + memberID
|
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
getResp, err := client.Do(getReq)
|
|
if err == nil && getResp != nil {
|
|
defer getResp.Body.Close()
|
|
if getResp.StatusCode == 200 {
|
|
var member map[string]interface{}
|
|
getBody, _ := io.ReadAll(getResp.Body)
|
|
json.Unmarshal(getBody, &member)
|
|
|
|
// Verify robot member fields
|
|
assert.Equal(t, "robot", member["member_type"], "Should be robot member type")
|
|
if tc.body["name"] != nil {
|
|
assert.Equal(t, tc.body["name"], member["display_name"], "Should have correct display_name")
|
|
}
|
|
if tc.body["email"] != nil {
|
|
assert.Equal(t, tc.body["email"], member["email"], "Should have correct email")
|
|
}
|
|
if tc.body["prompt"] != nil {
|
|
assert.Equal(t, tc.body["prompt"], member["system_prompt"], "Should have correct system_prompt")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Logf("Robot member create test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// toString converts interface{} to string for test assertions
|
|
func toString(v interface{}) string {
|
|
switch val := v.(type) {
|
|
case string:
|
|
return val
|
|
case float64:
|
|
return fmt.Sprintf("%.0f", val)
|
|
case int:
|
|
return fmt.Sprintf("%d", val)
|
|
case int64:
|
|
return fmt.Sprintf("%d", val)
|
|
default:
|
|
return fmt.Sprintf("%v", val)
|
|
}
|
|
}
|
|
|
|
// TestMemberCheckRobotEmail tests the GET /user/teams/:team_id/members/check-robot-email endpoint
|
|
func TestMemberCheckRobotEmail(t *testing.T) {
|
|
// Initialize test environment
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register a test client for OAuth authentication
|
|
testClient := testutils.RegisterTestClient(t, "Member Check Robot Email Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
|
|
|
// Obtain access token for authenticated requests
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Use UUID to ensure unique test data
|
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
|
|
|
// Create a test team
|
|
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Robot Email Check Test Team "+testUUID)
|
|
teamID := getTeamID(createdTeam)
|
|
|
|
// Create a robot member with a known robot_email (globally unique)
|
|
existingRobotEmail := fmt.Sprintf("existing-robot-%s@robot.test.com", testUUID)
|
|
robotBody := map[string]interface{}{
|
|
"name": "Existing Robot",
|
|
"email": fmt.Sprintf("display-%s@test.com", testUUID), // Display email (can be non-unique)
|
|
"robot_email": existingRobotEmail, // Globally unique robot email
|
|
"role": "member",
|
|
"prompt": "You are a test robot",
|
|
}
|
|
robotBodyBytes, _ := json.Marshal(robotBody)
|
|
robotReq, _ := http.NewRequest("POST", serverURL+baseURL+"/user/teams/"+teamID+"/members/robots", bytes.NewBuffer(robotBodyBytes))
|
|
robotReq.Header.Set("Content-Type", "application/json")
|
|
robotReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
client := &http.Client{}
|
|
robotResp, err := client.Do(robotReq)
|
|
assert.NoError(t, err)
|
|
if robotResp != nil {
|
|
robotResp.Body.Close()
|
|
assert.Equal(t, 201, robotResp.StatusCode, "Should create robot member successfully")
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
teamID string
|
|
robotEmail string
|
|
headers map[string]string
|
|
expectCode int
|
|
expectExists bool
|
|
expectMsg string
|
|
}{
|
|
{
|
|
"check robot email without authentication",
|
|
teamID,
|
|
existingRobotEmail,
|
|
map[string]string{},
|
|
401,
|
|
false,
|
|
"should require authentication",
|
|
},
|
|
{
|
|
"check existing robot email",
|
|
teamID,
|
|
existingRobotEmail,
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
true,
|
|
"should return exists=true for existing robot email",
|
|
},
|
|
{
|
|
"check non-existing robot email",
|
|
teamID,
|
|
fmt.Sprintf("nonexistent-%s@robot.test.com", testUUID),
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
false,
|
|
"should return exists=false for non-existing robot email",
|
|
},
|
|
{
|
|
"check robot email without robot_email parameter",
|
|
teamID,
|
|
"",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
false,
|
|
"should require robot_email parameter",
|
|
},
|
|
{
|
|
"check robot email in non-existent team",
|
|
"non-existent-team-id",
|
|
"test@robot.example.com",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
false,
|
|
"should return not found for non-existent team",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members/check-robot-email"
|
|
if tc.robotEmail != "" {
|
|
requestURL += "?robot_email=" + tc.robotEmail
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", requestURL, nil)
|
|
assert.NoError(t, err, "Should create HTTP request")
|
|
|
|
// Add headers
|
|
for key, value := range tc.headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
assert.NoError(t, err, "HTTP request should succeed")
|
|
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read response body")
|
|
|
|
if resp.StatusCode == 200 {
|
|
// Parse response
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(body, &response)
|
|
assert.NoError(t, err, "Should parse JSON response")
|
|
|
|
// Verify response structure (global check, no team_id in response)
|
|
assert.Contains(t, response, "exists", "Should have exists field")
|
|
assert.Contains(t, response, "robot_email", "Should have robot_email field")
|
|
|
|
// Verify values
|
|
assert.Equal(t, tc.expectExists, response["exists"], "Should have correct exists value")
|
|
assert.Equal(t, tc.robotEmail, response["robot_email"], "Should have correct robot_email")
|
|
}
|
|
|
|
t.Logf("Member check email test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMemberUpdateRobot tests the PUT /user/teams/:team_id/members/robots/:member_id endpoint
|
|
func TestMemberUpdateRobot(t *testing.T) {
|
|
// Initialize test environment
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register a test client for OAuth authentication
|
|
testClient := testutils.RegisterTestClient(t, "Robot Member Update Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
|
|
|
// Obtain access token with root permissions (required for robot operations)
|
|
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Use UUID to ensure unique test data
|
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
|
|
|
// Create a test team
|
|
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Robot Update Test Team "+testUUID)
|
|
teamID := getTeamID(createdTeam)
|
|
|
|
// Helper function to create a robot member for testing
|
|
createTestRobot := func(suffix string) (string, string) {
|
|
robotEmail := fmt.Sprintf("test-robot-%s-%s@robot.test.com", testUUID, suffix)
|
|
robotBody := map[string]interface{}{
|
|
"name": "Test Robot " + suffix,
|
|
"robot_email": robotEmail,
|
|
"email": fmt.Sprintf("display-%s-%s@test.com", testUUID, suffix),
|
|
"role": "member",
|
|
"prompt": "Original prompt for " + suffix,
|
|
"llm": "gpt-3.5-turbo",
|
|
"autonomous_mode": "disabled",
|
|
"cost_limit": 50.0,
|
|
}
|
|
robotBodyBytes, _ := json.Marshal(robotBody)
|
|
robotReq, _ := http.NewRequest("POST", serverURL+baseURL+"/user/teams/"+teamID+"/members/robots", bytes.NewBuffer(robotBodyBytes))
|
|
robotReq.Header.Set("Content-Type", "application/json")
|
|
robotReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
client := &http.Client{}
|
|
robotResp, err := client.Do(robotReq)
|
|
assert.NoError(t, err)
|
|
if robotResp != nil {
|
|
defer robotResp.Body.Close()
|
|
assert.Equal(t, 201, robotResp.StatusCode, "Should create robot member successfully")
|
|
body, _ := io.ReadAll(robotResp.Body)
|
|
var response map[string]interface{}
|
|
json.Unmarshal(body, &response)
|
|
return toString(response["member_id"]), robotEmail
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
setupFunc func() (string, string) // Returns (memberID, originalRobotEmail)
|
|
body map[string]interface{}
|
|
headers map[string]string
|
|
expectCode int
|
|
expectMsg string
|
|
validateFn func(*testing.T, string) // Optional validation function with memberID
|
|
}{
|
|
{
|
|
"update robot without authentication",
|
|
func() (string, string) { return createTestRobot("1") },
|
|
map[string]interface{}{
|
|
"name": "Updated Name",
|
|
},
|
|
map[string]string{},
|
|
401,
|
|
"should require authentication",
|
|
nil,
|
|
},
|
|
{
|
|
"update robot with all fields",
|
|
func() (string, string) { return createTestRobot("2") },
|
|
map[string]interface{}{
|
|
"name": "Updated Robot Full",
|
|
"avatar": fmt.Sprintf("https://example.com/avatars/full-%s.png", testUUID),
|
|
"email": fmt.Sprintf("updated-display-%s@test.com", testUUID),
|
|
"robot_email": fmt.Sprintf("updated-robot-%s@robot.test.com", testUUID),
|
|
"bio": "Updated comprehensive description",
|
|
"role": "admin",
|
|
"report_to": tokenInfo.UserID,
|
|
"prompt": "Updated system prompt",
|
|
"llm": "gpt-4",
|
|
"agents": []string{"agent1", "agent2"},
|
|
"mcp_tools": []string{"tool1", "tool2"},
|
|
"authorized_senders": []string{"admin@test.com"},
|
|
"email_filter_rules": []string{".*@test\\.com$"},
|
|
"autonomous_mode": "enabled",
|
|
"cost_limit": 100.0,
|
|
"status": "active",
|
|
"robot_status": "working",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update robot with all fields successfully",
|
|
func(t *testing.T, memberID string) {
|
|
// Verify the update
|
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
client := &http.Client{}
|
|
getResp, err := client.Do(getReq)
|
|
assert.NoError(t, err)
|
|
if getResp != nil {
|
|
defer getResp.Body.Close()
|
|
if getResp.StatusCode == 200 {
|
|
var member map[string]interface{}
|
|
body, _ := io.ReadAll(getResp.Body)
|
|
json.Unmarshal(body, &member)
|
|
assert.Equal(t, "Updated Robot Full", member["display_name"])
|
|
assert.Equal(t, fmt.Sprintf("https://example.com/avatars/full-%s.png", testUUID), member["avatar"])
|
|
assert.Equal(t, "Updated system prompt", member["system_prompt"])
|
|
assert.Equal(t, "gpt-4", member["language_model"])
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"update robot with partial fields",
|
|
func() (string, string) { return createTestRobot("3") },
|
|
map[string]interface{}{
|
|
"name": "Partially Updated Robot",
|
|
"prompt": "Partially updated prompt",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update robot with partial fields",
|
|
func(t *testing.T, memberID string) {
|
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
client := &http.Client{}
|
|
getResp, err := client.Do(getReq)
|
|
assert.NoError(t, err)
|
|
if getResp != nil {
|
|
defer getResp.Body.Close()
|
|
if getResp.StatusCode == 200 {
|
|
var member map[string]interface{}
|
|
body, _ := io.ReadAll(getResp.Body)
|
|
json.Unmarshal(body, &member)
|
|
assert.Equal(t, "Partially Updated Robot", member["display_name"])
|
|
assert.Equal(t, "Partially updated prompt", member["system_prompt"])
|
|
// Original fields should remain
|
|
assert.Equal(t, "gpt-3.5-turbo", member["language_model"])
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"update robot_email to new unique email",
|
|
func() (string, string) { return createTestRobot("4") },
|
|
map[string]interface{}{
|
|
"robot_email": fmt.Sprintf("new-unique-%s@robot.test.com", testUUID),
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update robot_email to new unique email",
|
|
func(t *testing.T, memberID string) {
|
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
client := &http.Client{}
|
|
getResp, err := client.Do(getReq)
|
|
assert.NoError(t, err)
|
|
if getResp != nil {
|
|
defer getResp.Body.Close()
|
|
if getResp.StatusCode == 200 {
|
|
var member map[string]interface{}
|
|
body, _ := io.ReadAll(getResp.Body)
|
|
json.Unmarshal(body, &member)
|
|
assert.Equal(t, fmt.Sprintf("new-unique-%s@robot.test.com", testUUID), member["robot_email"])
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"update robot_email to duplicate email",
|
|
func() (string, string) {
|
|
// Create two robots
|
|
memberID1, email1 := createTestRobot("5a")
|
|
_, _ = createTestRobot("5b")
|
|
return memberID1, email1
|
|
},
|
|
map[string]interface{}{
|
|
"robot_email": fmt.Sprintf("test-robot-%s-5b@robot.test.com", testUUID),
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
409,
|
|
"should reject duplicate robot_email",
|
|
nil,
|
|
},
|
|
{
|
|
"update autonomous_mode variations",
|
|
func() (string, string) { return createTestRobot("6") },
|
|
map[string]interface{}{
|
|
"autonomous_mode": "1",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should handle autonomous_mode=1",
|
|
func(t *testing.T, memberID string) {
|
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
client := &http.Client{}
|
|
getResp, err := client.Do(getReq)
|
|
assert.NoError(t, err)
|
|
if getResp != nil {
|
|
defer getResp.Body.Close()
|
|
if getResp.StatusCode == 200 {
|
|
var member map[string]interface{}
|
|
body, _ := io.ReadAll(getResp.Body)
|
|
json.Unmarshal(body, &member)
|
|
// autonomous_mode should be enabled
|
|
autonomousMode := member["autonomous_mode"]
|
|
assert.True(t, autonomousMode == true || autonomousMode == float64(1) || autonomousMode == int64(1))
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"update robot status",
|
|
func() (string, string) { return createTestRobot("7") },
|
|
map[string]interface{}{
|
|
"status": "inactive",
|
|
"robot_status": "error",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update robot status fields",
|
|
func(t *testing.T, memberID string) {
|
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
client := &http.Client{}
|
|
getResp, err := client.Do(getReq)
|
|
assert.NoError(t, err)
|
|
if getResp != nil {
|
|
defer getResp.Body.Close()
|
|
if getResp.StatusCode == 200 {
|
|
var member map[string]interface{}
|
|
body, _ := io.ReadAll(getResp.Body)
|
|
json.Unmarshal(body, &member)
|
|
assert.Equal(t, "inactive", member["status"])
|
|
assert.Equal(t, "error", member["robot_status"])
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"update array fields",
|
|
func() (string, string) { return createTestRobot("8") },
|
|
map[string]interface{}{
|
|
"agents": []string{"new-agent1", "new-agent2", "new-agent3"},
|
|
"mcp_tools": []string{"new-tool1"},
|
|
"authorized_senders": []string{"sender1@test.com", "sender2@test.com"},
|
|
"email_filter_rules": []string{".*@allowed\\.com$"},
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update array fields",
|
|
nil,
|
|
},
|
|
{
|
|
"update non-existent robot",
|
|
func() (string, string) { return "non-existent-member-id", "" },
|
|
map[string]interface{}{
|
|
"name": "Should Fail",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent robot",
|
|
nil,
|
|
},
|
|
{
|
|
"update regular user member as robot",
|
|
func() (string, string) {
|
|
// Create a regular user member instead of robot
|
|
memberID := createTestMember(t, serverURL, baseURL, teamID, tokenInfo.AccessToken, "regular-user-"+testUUID)
|
|
return memberID, ""
|
|
},
|
|
map[string]interface{}{
|
|
"name": "Should Fail",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should reject updating non-robot member",
|
|
nil,
|
|
},
|
|
{
|
|
"update robot in non-existent team",
|
|
func() (string, string) { return createTestRobot("10") },
|
|
map[string]interface{}{
|
|
"name": "Should Fail",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent team",
|
|
nil,
|
|
},
|
|
{
|
|
"update robot with invalid JSON",
|
|
func() (string, string) { return createTestRobot("11") },
|
|
nil, // Will send invalid JSON
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should handle invalid JSON",
|
|
nil,
|
|
},
|
|
{
|
|
"update robot with empty body",
|
|
func() (string, string) { return createTestRobot("12") },
|
|
map[string]interface{}{},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should handle empty update (no-op)",
|
|
nil,
|
|
},
|
|
{
|
|
"update robot avatar",
|
|
func() (string, string) { return createTestRobot("13") },
|
|
map[string]interface{}{
|
|
"name": "Robot with Avatar",
|
|
"robot_email": fmt.Sprintf("robot-avatar-%s@robot.test.com", testUUID),
|
|
"avatar": fmt.Sprintf("https://example.com/avatars/robot-%s.png", testUUID),
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update robot avatar successfully",
|
|
func(t *testing.T, memberID string) {
|
|
// Verify the avatar was updated
|
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
client := &http.Client{}
|
|
getResp, err := client.Do(getReq)
|
|
assert.NoError(t, err)
|
|
if getResp != nil {
|
|
defer getResp.Body.Close()
|
|
if getResp.StatusCode == 200 {
|
|
var member map[string]interface{}
|
|
body, _ := io.ReadAll(getResp.Body)
|
|
json.Unmarshal(body, &member)
|
|
assert.Equal(t, "Robot with Avatar", member["display_name"])
|
|
assert.Equal(t, fmt.Sprintf("https://example.com/avatars/robot-%s.png", testUUID), member["avatar"], "Should have correct avatar URL")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"update only robot avatar",
|
|
func() (string, string) { return createTestRobot("14") },
|
|
map[string]interface{}{
|
|
"avatar": fmt.Sprintf("https://example.com/avatars/updated-%s.png", testUUID),
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update only avatar without affecting other fields",
|
|
func(t *testing.T, memberID string) {
|
|
// Verify only avatar was updated
|
|
getMemberURL := serverURL + baseURL + "/user/teams/" + teamID + "/members/" + memberID
|
|
getReq, _ := http.NewRequest("GET", getMemberURL, nil)
|
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
client := &http.Client{}
|
|
getResp, err := client.Do(getReq)
|
|
assert.NoError(t, err)
|
|
if getResp != nil {
|
|
defer getResp.Body.Close()
|
|
if getResp.StatusCode == 200 {
|
|
var member map[string]interface{}
|
|
body, _ := io.ReadAll(getResp.Body)
|
|
json.Unmarshal(body, &member)
|
|
// Avatar should be updated
|
|
assert.Equal(t, fmt.Sprintf("https://example.com/avatars/updated-%s.png", testUUID), member["avatar"], "Should have updated avatar URL")
|
|
// Original fields should remain
|
|
assert.Equal(t, "Test Robot 14", member["display_name"], "Name should remain unchanged")
|
|
assert.Equal(t, "gpt-3.5-turbo", member["language_model"], "LLM should remain unchanged")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
memberID, _ := tc.setupFunc()
|
|
|
|
// Use non-existent team ID for the specific test case
|
|
targetTeamID := teamID
|
|
if tc.name == "update robot in non-existent team" {
|
|
targetTeamID = "non-existent-team-id"
|
|
}
|
|
|
|
requestURL := serverURL + baseURL + "/user/teams/" + targetTeamID + "/members/robots/" + memberID
|
|
|
|
var req *http.Request
|
|
var err error
|
|
|
|
if tc.body == nil {
|
|
// Send invalid JSON for invalid JSON test case
|
|
req, err = http.NewRequest("PUT", requestURL, bytes.NewBufferString("invalid json"))
|
|
} else {
|
|
bodyBytes, _ := json.Marshal(tc.body)
|
|
req, err = http.NewRequest("PUT", requestURL, bytes.NewBuffer(bodyBytes))
|
|
}
|
|
assert.NoError(t, err, "Should create HTTP request")
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Add headers
|
|
for key, value := range tc.headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
assert.NoError(t, err, "HTTP request should succeed")
|
|
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read response body")
|
|
|
|
if resp.StatusCode == 200 {
|
|
// Parse response as success message
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(body, &response)
|
|
assert.NoError(t, err, "Should parse JSON response")
|
|
|
|
assert.Contains(t, response, "message", "Should have success message")
|
|
assert.Equal(t, "Robot member updated successfully", response["message"], "Should have correct success message")
|
|
|
|
// Run custom validation if provided
|
|
if tc.validateFn != nil {
|
|
tc.validateFn(t, memberID)
|
|
}
|
|
}
|
|
|
|
t.Logf("Robot member update test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMemberProfileGet tests the GET /user/teams/:team_id/members/:user_id/profile endpoint
|
|
func TestMemberProfileGet(t *testing.T) {
|
|
// Initialize test environment
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register a test client for OAuth authentication
|
|
testClient := testutils.RegisterTestClient(t, "Member Profile Get Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
|
|
|
// Obtain access token with root permission
|
|
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile system:root")
|
|
|
|
// Create a test team
|
|
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Member Profile Get Test Team")
|
|
teamID := getTeamID(createdTeam)
|
|
|
|
// The creator is automatically a member, so we can use their user_id
|
|
userID := tokenInfo.UserID
|
|
|
|
// Update the member profile first to have test data
|
|
provider := testutils.GetUserProvider(t)
|
|
ctx := context.Background()
|
|
updateData := maps.MapStrAny{
|
|
"display_name": "Test Display Name",
|
|
"bio": "Test bio description",
|
|
"avatar": "https://example.com/test-avatar.png",
|
|
"email": "test-member@example.com",
|
|
}
|
|
err := provider.UpdateMember(ctx, teamID, userID, updateData)
|
|
assert.NoError(t, err, "Should update member profile for testing")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
teamID string
|
|
userID string
|
|
headers map[string]string
|
|
expectCode int
|
|
expectMsg string
|
|
validateFn func(*testing.T, map[string]interface{}) // Optional validation function
|
|
}{
|
|
{
|
|
"get profile without authentication",
|
|
teamID,
|
|
userID,
|
|
map[string]string{},
|
|
401,
|
|
"should require authentication",
|
|
nil,
|
|
},
|
|
{
|
|
"get own profile successfully",
|
|
teamID,
|
|
userID,
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should return own profile successfully",
|
|
func(t *testing.T, profile map[string]interface{}) {
|
|
// Verify profile structure
|
|
assert.Contains(t, profile, "user_id", "Should have user_id")
|
|
assert.Contains(t, profile, "team_id", "Should have team_id")
|
|
assert.Contains(t, profile, "display_name", "Should have display_name")
|
|
assert.Contains(t, profile, "bio", "Should have bio")
|
|
assert.Contains(t, profile, "avatar", "Should have avatar")
|
|
assert.Contains(t, profile, "email", "Should have email")
|
|
|
|
// Verify values
|
|
assert.Equal(t, userID, profile["user_id"], "Should have correct user_id")
|
|
assert.Equal(t, teamID, profile["team_id"], "Should have correct team_id")
|
|
assert.Equal(t, "Test Display Name", profile["display_name"], "Should have correct display_name")
|
|
assert.Equal(t, "Test bio description", profile["bio"], "Should have correct bio")
|
|
assert.Equal(t, "https://example.com/test-avatar.png", profile["avatar"], "Should have correct avatar")
|
|
assert.Equal(t, "test-member@example.com", profile["email"], "Should have correct email")
|
|
},
|
|
},
|
|
{
|
|
"get profile from non-existent team",
|
|
"non-existent-team-id",
|
|
userID,
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent team",
|
|
nil,
|
|
},
|
|
{
|
|
"get profile for non-existent user",
|
|
teamID,
|
|
"non-existent-user-id",
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent user",
|
|
nil,
|
|
},
|
|
{
|
|
"get profile with minimal data",
|
|
teamID,
|
|
userID,
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should return profile even with minimal data",
|
|
func(t *testing.T, profile map[string]interface{}) {
|
|
// Should always have these fields, even if empty
|
|
assert.Contains(t, profile, "user_id", "Should have user_id field")
|
|
assert.Contains(t, profile, "team_id", "Should have team_id field")
|
|
assert.Contains(t, profile, "display_name", "Should have display_name field")
|
|
assert.Contains(t, profile, "bio", "Should have bio field")
|
|
assert.Contains(t, profile, "avatar", "Should have avatar field")
|
|
assert.Contains(t, profile, "email", "Should have email field")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members/" + tc.userID + "/profile"
|
|
|
|
req, err := http.NewRequest("GET", requestURL, nil)
|
|
assert.NoError(t, err, "Should create HTTP request")
|
|
|
|
// Add headers
|
|
for key, value := range tc.headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
assert.NoError(t, err, "HTTP request should succeed")
|
|
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read response body")
|
|
|
|
if resp.StatusCode == 200 {
|
|
// Parse response as profile object
|
|
var profile map[string]interface{}
|
|
err = json.Unmarshal(body, &profile)
|
|
assert.NoError(t, err, "Should parse JSON response")
|
|
|
|
// Run custom validation if provided
|
|
if tc.validateFn != nil {
|
|
tc.validateFn(t, profile)
|
|
}
|
|
}
|
|
|
|
t.Logf("Member profile get test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMemberProfileUpdate tests the PUT /user/teams/:team_id/members/:user_id/profile endpoint
|
|
func TestMemberProfileUpdate(t *testing.T) {
|
|
// Initialize test environment
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register a test client for OAuth authentication
|
|
testClient := testutils.RegisterTestClient(t, "Member Profile Update Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
|
|
|
// Obtain access token with root permission and explicit member profile scope
|
|
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile system:root member:profile:update:own")
|
|
|
|
// Create a test team
|
|
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Member Profile Update Test Team")
|
|
teamID := getTeamID(createdTeam)
|
|
|
|
// The creator is automatically a member, so we can use their user_id
|
|
userID := tokenInfo.UserID
|
|
|
|
testCases := []struct {
|
|
name string
|
|
teamID string
|
|
userID string
|
|
body map[string]interface{}
|
|
headers map[string]string
|
|
expectCode int
|
|
expectMsg string
|
|
validateFn func(*testing.T, string) // Optional validation function with userID
|
|
}{
|
|
{
|
|
"update profile without authentication",
|
|
teamID,
|
|
userID,
|
|
map[string]interface{}{
|
|
"display_name": "New Name",
|
|
},
|
|
map[string]string{},
|
|
401,
|
|
"should require authentication",
|
|
nil,
|
|
},
|
|
{
|
|
"update display_name",
|
|
teamID,
|
|
userID,
|
|
map[string]interface{}{
|
|
"display_name": "Updated Display Name",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update display_name successfully",
|
|
func(t *testing.T, uid string) {
|
|
// Verify the update by getting member details
|
|
provider := testutils.GetUserProvider(t)
|
|
member, err := provider.GetMember(context.Background(), teamID, uid)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Updated Display Name", member["display_name"])
|
|
},
|
|
},
|
|
{
|
|
"update bio",
|
|
teamID,
|
|
userID,
|
|
map[string]interface{}{
|
|
"bio": "This is my updated bio",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update bio successfully",
|
|
func(t *testing.T, uid string) {
|
|
provider := testutils.GetUserProvider(t)
|
|
member, err := provider.GetMember(context.Background(), teamID, uid)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "This is my updated bio", member["bio"])
|
|
},
|
|
},
|
|
{
|
|
"update avatar",
|
|
teamID,
|
|
userID,
|
|
map[string]interface{}{
|
|
"avatar": "https://example.com/avatar.png",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update avatar successfully",
|
|
func(t *testing.T, uid string) {
|
|
provider := testutils.GetUserProvider(t)
|
|
member, err := provider.GetMember(context.Background(), teamID, uid)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "https://example.com/avatar.png", member["avatar"])
|
|
},
|
|
},
|
|
{
|
|
"update email",
|
|
teamID,
|
|
userID,
|
|
map[string]interface{}{
|
|
"email": "newemail@example.com",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update email successfully",
|
|
func(t *testing.T, uid string) {
|
|
provider := testutils.GetUserProvider(t)
|
|
member, err := provider.GetMember(context.Background(), teamID, uid)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "newemail@example.com", member["email"])
|
|
},
|
|
},
|
|
{
|
|
"update all fields at once",
|
|
teamID,
|
|
userID,
|
|
map[string]interface{}{
|
|
"display_name": "Complete Update",
|
|
"bio": "All fields updated",
|
|
"avatar": "https://example.com/complete.png",
|
|
"email": "complete@example.com",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should update all fields successfully",
|
|
func(t *testing.T, uid string) {
|
|
provider := testutils.GetUserProvider(t)
|
|
member, err := provider.GetMember(context.Background(), teamID, uid)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Complete Update", member["display_name"])
|
|
assert.Equal(t, "All fields updated", member["bio"])
|
|
assert.Equal(t, "https://example.com/complete.png", member["avatar"])
|
|
assert.Equal(t, "complete@example.com", member["email"])
|
|
},
|
|
},
|
|
{
|
|
"update with empty body",
|
|
teamID,
|
|
userID,
|
|
map[string]interface{}{},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should reject empty update",
|
|
nil,
|
|
},
|
|
{
|
|
"update other user's profile should fail",
|
|
teamID,
|
|
"other-user-id",
|
|
map[string]interface{}{
|
|
"display_name": "Should Fail",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent user (member not found)",
|
|
nil,
|
|
},
|
|
{
|
|
"update profile in non-existent team",
|
|
"non-existent-team-id",
|
|
userID,
|
|
map[string]interface{}{
|
|
"display_name": "Should Fail",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
404,
|
|
"should return not found for non-existent team",
|
|
nil,
|
|
},
|
|
{
|
|
"update with invalid JSON",
|
|
teamID,
|
|
userID,
|
|
nil, // Will send invalid JSON
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
400,
|
|
"should handle invalid JSON",
|
|
nil,
|
|
},
|
|
{
|
|
"partial update - single field",
|
|
teamID,
|
|
userID,
|
|
map[string]interface{}{
|
|
"display_name": "Partial Update",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should handle partial update with single field",
|
|
func(t *testing.T, uid string) {
|
|
provider := testutils.GetUserProvider(t)
|
|
member, err := provider.GetMember(context.Background(), teamID, uid)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Partial Update", member["display_name"])
|
|
// Other fields should remain unchanged
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members/" + tc.userID + "/profile"
|
|
|
|
var req *http.Request
|
|
var err error
|
|
|
|
if tc.body == nil {
|
|
// Send invalid JSON for invalid JSON test case
|
|
req, err = http.NewRequest("PUT", requestURL, bytes.NewBufferString("invalid json"))
|
|
} else {
|
|
bodyBytes, _ := json.Marshal(tc.body)
|
|
req, err = http.NewRequest("PUT", requestURL, bytes.NewBuffer(bodyBytes))
|
|
}
|
|
assert.NoError(t, err, "Should create HTTP request")
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Add headers
|
|
for key, value := range tc.headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
assert.NoError(t, err, "HTTP request should succeed")
|
|
|
|
if resp != nil {
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err, "Should read response body")
|
|
|
|
if resp.StatusCode == 200 {
|
|
// Parse response as success message
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(body, &response)
|
|
assert.NoError(t, err, "Should parse JSON response")
|
|
|
|
assert.Contains(t, response, "user_id", "Should have user_id")
|
|
assert.Contains(t, response, "message", "Should have success message")
|
|
assert.Equal(t, tc.userID, response["user_id"], "Should have correct user_id")
|
|
assert.Equal(t, "Member profile updated successfully", response["message"], "Should have correct success message")
|
|
|
|
// Run custom validation if provided
|
|
if tc.validateFn != nil {
|
|
tc.validateFn(t, tc.userID)
|
|
}
|
|
}
|
|
|
|
t.Logf("Member profile update test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Note: getTeamID function is already defined in team_test.go
|