- Introduced a new `workspace` field across various robot-related structures, including `CreateRobotRequest`, `UpdateRobotRequest`, and `RobotResponse`, allowing for better organization and management of robots within specific workspaces. - Updated database queries and response mappings to accommodate the new workspace field, ensuring seamless integration with existing functionalities. - Enhanced agent execution context to include workspace information, improving the contextual awareness of agents during operations. - Added tests to validate the creation and updating of robots with workspace data, ensuring robust functionality and backward compatibility.
2525 lines
79 KiB
Go
2525 lines
79 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,
|
|
"workspace": "ws-test-create",
|
|
},
|
|
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")
|
|
}
|
|
if tc.body["workspace"] != nil {
|
|
assert.Equal(t, "ws-test-create", member["workspace"], "Should have correct workspace")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
"workspace": "ws-initial",
|
|
}
|
|
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",
|
|
"workspace": "ws-updated",
|
|
},
|
|
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"])
|
|
assert.Equal(t, "ws-updated", member["workspace"], "Should have correct workspace")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"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")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"update workspace to unbind",
|
|
func() (string, string) { return createTestRobot("15") },
|
|
map[string]interface{}{
|
|
"workspace": "",
|
|
},
|
|
map[string]string{
|
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
|
},
|
|
200,
|
|
"should unbind workspace by setting to empty string",
|
|
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.Empty(t, member["workspace"], "Workspace should be empty after unbinding")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
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
|