Implement existence checks for various entities in update functions

- Added functions to check the existence of invitation codes, members, OAuth accounts, roles, teams, and user types before performing updates, enhancing error handling and user feedback.
- Updated relevant update functions to utilize these existence checks, ensuring accurate error messages when no changes are made or when entities do not exist.
- Refactored tests to validate the new existence check logic, improving overall test coverage and reliability.
This commit is contained in:
Max 2025-10-26 11:09:07 +08:00
parent 5f6f8d7911
commit f71ce2ac9a
12 changed files with 339 additions and 267 deletions

View file

@ -11,6 +11,24 @@ import (
// Invitation Code Resource (Official Platform Invitation Codes)
// invitationCodeExists checks if an invitation code exists by code
func (u *DefaultUser) invitationCodeExists(ctx context.Context, code string) (bool, error) {
m := model.Select(u.invitationModel)
invitations, err := m.Get(model.QueryParam{
Select: []interface{}{"id"},
Wheres: []model.QueryWhere{
{Column: "code", Value: code},
},
Limit: 1,
})
if err != nil {
return false, fmt.Errorf("failed to check invitation code existence: %w", err)
}
return len(invitations) > 0, nil
}
// CreateInvitationCodes creates invitation codes in batch
// Supports creating multiple invitation codes at once for efficiency
func (u *DefaultUser) CreateInvitationCodes(ctx context.Context, codeData []maps.MapStrAny) ([]string, error) {
@ -162,7 +180,15 @@ func (u *DefaultUser) UseInvitationCode(ctx context.Context, code string, userID
}
if affected == 0 {
return fmt.Errorf(ErrInvitationCodeNotFound)
// Check if invitation code still exists
exists, checkErr := u.invitationCodeExists(ctx, code)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUseInvitationCode, checkErr)
}
if !exists {
return fmt.Errorf(ErrInvitationCodeNotFound)
}
// Invitation code exists but no changes were made (already in this state)
}
return nil

View file

@ -183,6 +183,60 @@ func (u *DefaultUser) MemberExistsByTeamEmail(ctx context.Context, teamID string
return len(members) > 0, nil
}
// MemberExistsByMemberID checks if a member exists by member_id (business ID)
func (u *DefaultUser) MemberExistsByMemberID(ctx context.Context, memberID string) (bool, error) {
m := model.Select(u.memberModel)
members, err := m.Get(model.QueryParam{
Select: []interface{}{"id"}, // Only select ID for existence check
Wheres: []model.QueryWhere{
{Column: "member_id", Value: memberID},
},
Limit: 1,
})
if err != nil {
return false, fmt.Errorf(ErrFailedToGetMember, err)
}
return len(members) > 0, nil
}
// memberExistsByID checks if a member exists by internal database ID
func (u *DefaultUser) memberExistsByID(ctx context.Context, id int64) (bool, error) {
m := model.Select(u.memberModel)
members, err := m.Get(model.QueryParam{
Select: []interface{}{"id"}, // Only select ID for existence check
Wheres: []model.QueryWhere{
{Column: "id", Value: id},
},
Limit: 1,
})
if err != nil {
return false, fmt.Errorf(ErrFailedToGetMember, err)
}
return len(members) > 0, nil
}
// memberExistsByInvitationID checks if a member exists by invitation_id
func (u *DefaultUser) memberExistsByInvitationID(ctx context.Context, invitationID string) (bool, error) {
m := model.Select(u.memberModel)
members, err := m.Get(model.QueryParam{
Select: []interface{}{"id"}, // Only select ID for existence check
Wheres: []model.QueryWhere{
{Column: "invitation_id", Value: invitationID},
},
Limit: 1,
})
if err != nil {
return false, fmt.Errorf(ErrFailedToGetMember, err)
}
return len(members) > 0, nil
}
// CreateMember creates a new team member (user type)
func (u *DefaultUser) CreateMember(ctx context.Context, memberData maps.MapStrAny) (string, error) {
// Validate required fields for user members
@ -470,14 +524,22 @@ func (u *DefaultUser) UpdateMember(ctx context.Context, teamID string, userID st
}
if affected == 0 {
return fmt.Errorf(ErrMemberNotFound)
// Check if member exists
exists, checkErr := u.MemberExists(ctx, teamID, userID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateMember, checkErr)
}
if !exists {
return fmt.Errorf(ErrMemberNotFound)
}
// Member exists but no changes were made
}
return nil
}
// UpdateMemberByID updates a member by internal ID
func (u *DefaultUser) UpdateMemberByID(ctx context.Context, memberID int64, memberData maps.MapStrAny) error {
// UpdateMemberByID updates a member by internal database ID
func (u *DefaultUser) UpdateMemberByID(ctx context.Context, id int64, memberData maps.MapStrAny) error {
// Remove sensitive fields that should not be updated directly
sensitiveFields := []string{"id", "member_id", "team_id", "user_id", "created_at", "invitation_token"}
for _, field := range sensitiveFields {
@ -492,7 +554,7 @@ func (u *DefaultUser) UpdateMemberByID(ctx context.Context, memberID int64, memb
m := model.Select(u.memberModel)
affected, err := m.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "id", Value: memberID},
{Column: "id", Value: id},
},
Limit: 1,
}, memberData)
@ -502,7 +564,15 @@ func (u *DefaultUser) UpdateMemberByID(ctx context.Context, memberID int64, memb
}
if affected == 0 {
return fmt.Errorf(ErrMemberNotFound)
// Check if member exists
exists, checkErr := u.memberExistsByID(ctx, id)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateMember, checkErr)
}
if !exists {
return fmt.Errorf(ErrMemberNotFound)
}
// Member exists but no changes were made
}
return nil
@ -533,8 +603,21 @@ func (u *DefaultUser) UpdateMemberByMemberID(ctx context.Context, memberID strin
return fmt.Errorf(ErrFailedToUpdateMember, err)
}
// Note: affected=0 can mean either:
// 1. No record found with the given member_id
// 2. Record exists but no fields were changed (values are the same)
// We verify the member exists first to provide a more accurate error
if affected == 0 {
return fmt.Errorf(ErrMemberNotFound)
// Check if member exists
exists, checkErr := u.MemberExistsByMemberID(ctx, memberID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateMember, checkErr)
}
if !exists {
return fmt.Errorf(ErrMemberNotFound)
}
// Member exists but no changes were made (values are the same)
// This is not an error, just return nil
}
return nil
@ -791,14 +874,14 @@ func (u *DefaultUser) UpdateMemberLastActivityByMemberID(ctx context.Context, me
return u.UpdateMemberByMemberID(ctx, memberID, updateData)
}
// UpdateRobotActivity updates robot member's last activity and status
func (u *DefaultUser) UpdateRobotActivity(ctx context.Context, memberID int64, robotStatus string) error {
// UpdateRobotActivity updates robot member's last activity and status by internal database ID
func (u *DefaultUser) UpdateRobotActivity(ctx context.Context, id int64, robotStatus string) error {
updateData := maps.MapStrAny{
"last_robot_activity": time.Now(),
"robot_status": robotStatus,
}
return u.UpdateMemberByID(ctx, memberID, updateData)
return u.UpdateMemberByID(ctx, id, updateData)
}
// UpdateMemberByInvitationID updates a member by invitation_id
@ -828,7 +911,15 @@ func (u *DefaultUser) UpdateMemberByInvitationID(ctx context.Context, invitation
}
if affected == 0 {
return fmt.Errorf(ErrMemberNotFound)
// Check if member exists
exists, checkErr := u.memberExistsByInvitationID(ctx, invitationID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateMember, checkErr)
}
if !exists {
return fmt.Errorf(ErrMemberNotFound)
}
// Member exists but no changes were made
}
return nil

View file

@ -420,12 +420,7 @@ func TestRobotMemberOperations(t *testing.T) {
"autonomous_mode": true,
"status": "active",
})
if err != nil {
// If update fails, log the error and skip the test
t.Logf("Failed to update robot member: %v", err)
t.Skip("Robot member update failed, skipping GetActiveRobotMembers test")
return
}
assert.NoError(t, err)
robots, err := testProvider.GetActiveRobotMembers(ctx)
assert.NoError(t, err)

View file

@ -124,7 +124,15 @@ func (u *DefaultUser) UpdateOAuthAccount(ctx context.Context, provider string, s
}
if affected == 0 {
return fmt.Errorf("oauth account not found for provider %s with subject %s", provider, subject)
// Check if OAuth account exists
exists, checkErr := u.OAuthAccountExists(ctx, provider, subject)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateOAuth, checkErr)
}
if !exists {
return fmt.Errorf("oauth account not found for provider %s with subject %s", provider, subject)
}
// OAuth account exists but no changes were made
}
return nil

View file

@ -115,7 +115,15 @@ func (u *DefaultUser) UpdateRole(ctx context.Context, roleID string, roleData ma
}
if affected == 0 {
return fmt.Errorf(ErrRoleNotFound)
// Check if role exists
exists, checkErr := u.RoleExists(ctx, roleID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateRole, checkErr)
}
if !exists {
return fmt.Errorf(ErrRoleNotFound)
}
// Role exists but no changes were made
}
return nil
@ -280,7 +288,15 @@ func (u *DefaultUser) SetRolePermissions(ctx context.Context, roleID string, per
}
if affected == 0 {
return fmt.Errorf(ErrRoleNotFound)
// Check if role exists
exists, checkErr := u.RoleExists(ctx, roleID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateRole, checkErr)
}
if !exists {
return fmt.Errorf(ErrRoleNotFound)
}
// Role exists but no changes were made (same permissions)
}
return nil

View file

@ -142,7 +142,15 @@ func (u *DefaultUser) UpdateTeam(ctx context.Context, teamID string, teamData ma
}
if affected == 0 {
return fmt.Errorf(ErrTeamNotFound)
// Check if team exists
exists, checkErr := u.TeamExists(ctx, teamID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateTeam, checkErr)
}
if !exists {
return fmt.Errorf(ErrTeamNotFound)
}
// Team exists but no changes were made
}
return nil
@ -440,7 +448,15 @@ func (u *DefaultUser) VerifyTeam(ctx context.Context, teamID string, verifiedBy
}
if affected == 0 {
return fmt.Errorf(ErrTeamNotFound)
// Check if team exists
exists, checkErr := u.TeamExists(ctx, teamID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateTeam, checkErr)
}
if !exists {
return fmt.Errorf(ErrTeamNotFound)
}
// Team exists but no changes were made (already verified)
}
return nil
@ -468,7 +484,15 @@ func (u *DefaultUser) UnverifyTeam(ctx context.Context, teamID string) error {
}
if affected == 0 {
return fmt.Errorf(ErrTeamNotFound)
// Check if team exists
exists, checkErr := u.TeamExists(ctx, teamID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateTeam, checkErr)
}
if !exists {
return fmt.Errorf(ErrTeamNotFound)
}
// Team exists but no changes were made (already unverified)
}
return nil

View file

@ -115,7 +115,15 @@ func (u *DefaultUser) UpdateType(ctx context.Context, typeID string, typeData ma
}
if affected == 0 {
return fmt.Errorf(ErrTypeNotFound)
// Check if type exists
exists, checkErr := u.TypeExists(ctx, typeID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateType, checkErr)
}
if !exists {
return fmt.Errorf(ErrTypeNotFound)
}
// Type exists but no changes were made
}
return nil
@ -285,7 +293,15 @@ func (u *DefaultUser) SetTypeConfiguration(ctx context.Context, typeID string, c
}
if affected == 0 {
return fmt.Errorf(ErrTypeNotFound)
// Check if type exists
exists, checkErr := u.TypeExists(ctx, typeID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateType, checkErr)
}
if !exists {
return fmt.Errorf(ErrTypeNotFound)
}
// Type exists but no changes were made
}
return nil
@ -424,7 +440,15 @@ func (u *DefaultUser) SetTypePricing(ctx context.Context, typeID string, pricing
}
if affected == 0 {
return fmt.Errorf(ErrTypeNotFound)
// Check if type exists
exists, checkErr := u.TypeExists(ctx, typeID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateType, checkErr)
}
if !exists {
return fmt.Errorf(ErrTypeNotFound)
}
// Type exists but no changes were made
}
return nil
@ -458,7 +482,15 @@ func (u *DefaultUser) UpdateTypeStatus(ctx context.Context, typeID string, statu
}
if affected == 0 {
return fmt.Errorf(ErrTypeNotFound)
// Check if type exists
exists, checkErr := u.TypeExists(ctx, typeID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateType, checkErr)
}
if !exists {
return fmt.Errorf(ErrTypeNotFound)
}
// Type exists but no changes were made (already has this status)
}
return nil

View file

@ -295,7 +295,15 @@ func (u *DefaultUser) UpdatePassword(ctx context.Context, userID string, newPass
}
if affected == 0 {
return fmt.Errorf(ErrUserNotFound)
// Check if user exists
exists, checkErr := u.UserExists(ctx, userID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateUser, checkErr)
}
if !exists {
return fmt.Errorf(ErrUserNotFound)
}
// User exists but no changes were made (same password)
}
return nil
@ -327,7 +335,15 @@ func (u *DefaultUser) ResetPassword(ctx context.Context, userID string) (string,
}
if affected == 0 {
return "", fmt.Errorf(ErrUserNotFound)
// Check if user exists
exists, checkErr := u.UserExists(ctx, userID)
if checkErr != nil {
return "", fmt.Errorf(ErrFailedToUpdateUser, checkErr)
}
if !exists {
return "", fmt.Errorf(ErrUserNotFound)
}
// User exists but no changes were made
}
return randomPassword, nil
@ -400,7 +416,15 @@ func (u *DefaultUser) UpdateUser(ctx context.Context, userID string, userData ma
}
if affected == 0 {
return fmt.Errorf(ErrUserNotFound)
// Check if user exists
exists, checkErr := u.UserExists(ctx, userID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateUser, checkErr)
}
if !exists {
return fmt.Errorf(ErrUserNotFound)
}
// User exists but no changes were made
}
return nil

View file

@ -199,7 +199,15 @@ func (u *DefaultUser) EnableMFA(ctx context.Context, userID string, secret strin
}
if affected == 0 {
return fmt.Errorf(ErrUserNotFound)
// Check if user exists
exists, checkErr := u.UserExists(ctx, userID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateMFAStatus, checkErr)
}
if !exists {
return fmt.Errorf(ErrUserNotFound)
}
// User exists but no changes were made (already enabled with same secret)
}
return nil
@ -272,7 +280,15 @@ func (u *DefaultUser) DisableMFA(ctx context.Context, userID string, code string
}
if affected == 0 {
return fmt.Errorf(ErrUserNotFound)
// Check if user exists
exists, checkErr := u.UserExists(ctx, userID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateMFAStatus, checkErr)
}
if !exists {
return fmt.Errorf(ErrUserNotFound)
}
// User exists but no changes were made (already disabled)
}
return nil
@ -416,7 +432,15 @@ func (u *DefaultUser) GenerateRecoveryCodes(ctx context.Context, userID string)
}
if affected == 0 {
return nil, fmt.Errorf(ErrUserNotFound)
// Check if user exists
exists, checkErr := u.UserExists(ctx, userID)
if checkErr != nil {
return nil, fmt.Errorf(ErrFailedToUpdateMFAStatus, checkErr)
}
if !exists {
return nil, fmt.Errorf(ErrUserNotFound)
}
// User exists but no changes were made
}
// Return all generated recovery codes

View file

@ -105,7 +105,15 @@ func (u *DefaultUser) SetUserRole(ctx context.Context, userID string, roleID str
}
if affected == 0 {
return fmt.Errorf(ErrUserNotFound)
// Check if user exists
exists, checkErr := u.UserExists(ctx, userID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateUser, checkErr)
}
if !exists {
return fmt.Errorf(ErrUserNotFound)
}
// User exists but no changes were made (already has this role)
}
return nil
@ -270,7 +278,15 @@ func (u *DefaultUser) SetUserType(ctx context.Context, userID string, typeID str
}
if affected == 0 {
return fmt.Errorf(ErrUserNotFound)
// Check if user exists
exists, checkErr := u.UserExists(ctx, userID)
if checkErr != nil {
return fmt.Errorf(ErrFailedToUpdateUser, checkErr)
}
if !exists {
return fmt.Errorf(ErrUserNotFound)
}
// User exists but no changes were made (already has this type)
}
return nil

View file

@ -1036,3 +1036,30 @@ func createTestUser(t *testing.T, server *openapi.OpenAPI, clientID string) (str
t.Logf("Created test user: %s with subject: %s", testUserID, subject)
return testUserID, subject
}
// GetUserProvider returns the UserProvider instance for direct database operations in tests.
// This is useful for creating test data directly without going through API endpoints.
//
// USAGE:
//
// provider := testutils.GetUserProvider(t)
// memberID, err := provider.CreateMember(ctx, memberData)
//
// ERROR HANDLING:
// If the provider is not available, the test will fail immediately with a descriptive error message.
func GetUserProvider(t *testing.T) types.UserProvider {
testMutex.RLock()
defer testMutex.RUnlock()
oauthService := oauth.OAuth
if oauthService == nil {
t.Fatal("Global OAuth service not initialized. Call Prepare(t) first.")
}
provider, err := oauthService.GetUserProvider()
if err != nil || provider == nil {
t.Fatalf("UserProvider not available: %v", err)
}
return provider
}

View file

@ -2,6 +2,7 @@ package user_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
@ -11,6 +12,7 @@ import (
"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"
)
@ -269,193 +271,6 @@ func TestMemberGet(t *testing.T) {
}
}
// TestMemberCreateDirect tests the POST /user/teams/:team_id/members endpoint
func TestMemberCreateDirect(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 Create 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
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Member Create Test Team")
teamID := getTeamID(createdTeam)
testCases := []struct {
name string
teamID string
body map[string]interface{}
headers map[string]string
expectCode int
expectMsg string
}{
{
"create member without authentication",
teamID,
map[string]interface{}{
"user_id": "test-user-123",
"role_id": "member",
},
map[string]string{},
401,
"should require authentication",
},
{
"create member with valid data",
teamID,
map[string]interface{}{
"user_id": "test-user-123",
"member_type": "user",
"role_id": "member",
},
map[string]string{
"Authorization": "Bearer " + tokenInfo.AccessToken,
},
201,
"should create member successfully",
},
{
"create member with settings",
teamID,
map[string]interface{}{
"user_id": "test-user-456",
"member_type": "user",
"role_id": "admin",
"settings": map[string]interface{}{
"notifications": true,
"permissions": []string{"read", "write"},
},
},
map[string]string{
"Authorization": "Bearer " + tokenInfo.AccessToken,
},
201,
"should create member with settings",
},
{
"create member without user_id",
teamID,
map[string]interface{}{
"role_id": "member",
},
map[string]string{
"Authorization": "Bearer " + tokenInfo.AccessToken,
},
400,
"should require user_id",
},
{
"create member without role_id",
teamID,
map[string]interface{}{
"user_id": "test-user-789",
},
map[string]string{
"Authorization": "Bearer " + tokenInfo.AccessToken,
},
400,
"should require role_id",
},
{
"create duplicate member",
teamID,
map[string]interface{}{
"user_id": "test-user-123", // Same as first successful case
"role_id": "member",
},
map[string]string{
"Authorization": "Bearer " + tokenInfo.AccessToken,
},
409,
"should reject duplicate member",
},
{
"create member in non-existent team",
"non-existent-team-id",
map[string]interface{}{
"user_id": "test-user-999",
"role_id": "member",
},
map[string]string{
"Authorization": "Bearer " + tokenInfo.AccessToken,
},
404,
"should return not found for non-existent team",
},
{
"create member 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"
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")
}
t.Logf("Member create 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
@ -819,22 +634,6 @@ func TestMemberPermissionVerification(t *testing.T) {
200,
"member should be able to get member details",
},
{
"owner can create members",
"/user/teams/" + teamID + "/members",
"POST",
ownerToken.AccessToken,
201, // Will create successfully
"owner should be able to create members",
},
{
"member cannot create members",
"/user/teams/" + teamID + "/members",
"POST",
nonOwnerToken.AccessToken,
403,
"member should not be able to create members",
},
{
"owner can update members",
"/user/teams/" + teamID + "/members/" + memberID,
@ -944,42 +743,32 @@ func createTestTeam(t *testing.T, serverURL, baseURL, accessToken, teamName stri
return team
}
// createTestMember creates a member for testing and returns the user_id (which serves as member_id in API context)
// 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 user_id which serves as member_id in API context.
func createTestMember(t *testing.T, serverURL, baseURL, teamID, accessToken, userID string) string {
createMemberBody := map[string]interface{}{
// 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": "member",
"role_id": "team:member",
"status": "active",
}
bodyBytes, err := json.Marshal(createMemberBody)
assert.NoError(t, err, "Should marshal member creation body")
// 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")
req, err := http.NewRequest("POST", serverURL+baseURL+"/user/teams/"+teamID+"/members", bytes.NewBuffer(bodyBytes))
assert.NoError(t, err, "Should create member creation request")
t.Logf("Created test member directly in database: user_id=%s, member_id=%s, team_id=%s", userID, memberID, teamID)
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 member creation request")
defer resp.Body.Close()
assert.Equal(t, 201, resp.StatusCode, "Should create member successfully")
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err, "Should read member creation response")
var response map[string]interface{}
err = json.Unmarshal(body, &response)
assert.NoError(t, err, "Should parse member creation response")
_, ok := response["member_id"]
assert.True(t, ok, "Should have member_id in response")
// For API purposes, the member_id is the user_id in the context of team_id
// So we return the user_id that was used to create the member
// Return user_id (which is used as member identifier in API context)
return userID
}