Merge pull request #1237 from trheyi/main
Enhance member management with member_id and email checks
This commit is contained in:
commit
fb4df03b02
22 changed files with 1311 additions and 497 deletions
284
data/bindata.go
284
data/bindata.go
File diff suppressed because one or more lines are too long
|
|
@ -157,14 +157,14 @@ var (
|
||||||
|
|
||||||
// DefaultMemberFields contains basic member fields
|
// DefaultMemberFields contains basic member fields
|
||||||
DefaultMemberFields = []interface{}{
|
DefaultMemberFields = []interface{}{
|
||||||
"team_id", "user_id", "member_type", "display_name", "bio", "avatar", "email", "role_id", "status",
|
"member_id", "team_id", "user_id", "member_type", "display_name", "bio", "avatar", "email", "role_id", "status",
|
||||||
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token", "invitation_expires_at",
|
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token", "invitation_expires_at",
|
||||||
"last_active_at", "login_count", "created_at", "updated_at",
|
"last_active_at", "login_count", "created_at", "updated_at",
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultMemberDetailFields contains all member fields including robot config and permissions
|
// DefaultMemberDetailFields contains all member fields including robot config and permissions
|
||||||
DefaultMemberDetailFields = []interface{}{
|
DefaultMemberDetailFields = []interface{}{
|
||||||
"team_id", "user_id", "member_type", "display_name", "bio", "avatar", "email", "role_id", "status",
|
"member_id", "team_id", "user_id", "member_type", "display_name", "bio", "avatar", "email", "role_id", "status",
|
||||||
"system_prompt", "manager_id", "robot_config", "agents", "mcp_servers",
|
"system_prompt", "manager_id", "robot_config", "agents", "mcp_servers",
|
||||||
"language_model", "cost_limit", "autonomous_mode", "last_robot_activity", "robot_status",
|
"language_model", "cost_limit", "autonomous_mode", "last_robot_activity", "robot_status",
|
||||||
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token",
|
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,24 @@ import (
|
||||||
|
|
||||||
// Invitation Code Resource (Official Platform Invitation Codes)
|
// 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
|
// CreateInvitationCodes creates invitation codes in batch
|
||||||
// Supports creating multiple invitation codes at once for efficiency
|
// Supports creating multiple invitation codes at once for efficiency
|
||||||
func (u *DefaultUser) CreateInvitationCodes(ctx context.Context, codeData []maps.MapStrAny) ([]string, error) {
|
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 {
|
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
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,50 @@ func (u *DefaultUser) GetMemberByInvitationID(ctx context.Context, invitationID
|
||||||
return members[0], nil
|
return members[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMemberByMemberID retrieves member information by member_id (business ID)
|
||||||
|
func (u *DefaultUser) GetMemberByMemberID(ctx context.Context, memberID string) (maps.MapStrAny, error) {
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(model.QueryParam{
|
||||||
|
Select: u.memberFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "member_id", Value: memberID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(members) == 0 {
|
||||||
|
return nil, fmt.Errorf(ErrMemberNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return members[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMemberDetailByMemberID retrieves detailed member information by member_id (business ID)
|
||||||
|
func (u *DefaultUser) GetMemberDetailByMemberID(ctx context.Context, memberID string) (maps.MapStrAny, error) {
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(model.QueryParam{
|
||||||
|
Select: u.memberDetailFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "member_id", Value: memberID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(members) == 0 {
|
||||||
|
return nil, fmt.Errorf(ErrMemberNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return members[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
// MemberExists checks if a member exists by team_id and user_id
|
// MemberExists checks if a member exists by team_id and user_id
|
||||||
func (u *DefaultUser) MemberExists(ctx context.Context, teamID string, userID string) (bool, error) {
|
func (u *DefaultUser) MemberExists(ctx context.Context, teamID string, userID string) (bool, error) {
|
||||||
m := model.Select(u.memberModel)
|
m := model.Select(u.memberModel)
|
||||||
|
|
@ -120,14 +164,100 @@ func (u *DefaultUser) MemberExists(ctx context.Context, teamID string, userID st
|
||||||
return len(members) > 0, nil
|
return len(members) > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MemberExistsByTeamEmail checks if a member exists by team_id and email
|
||||||
|
func (u *DefaultUser) MemberExistsByTeamEmail(ctx context.Context, teamID string, email 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: "team_id", Value: teamID},
|
||||||
|
{Column: "email", Value: email},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
// CreateMember creates a new team member (user type)
|
||||||
func (u *DefaultUser) CreateMember(ctx context.Context, memberData maps.MapStrAny) (int64, error) {
|
func (u *DefaultUser) CreateMember(ctx context.Context, memberData maps.MapStrAny) (string, error) {
|
||||||
// Validate required fields for user members
|
// Validate required fields for user members
|
||||||
if _, exists := memberData["team_id"]; !exists {
|
if _, exists := memberData["team_id"]; !exists {
|
||||||
return 0, fmt.Errorf("team_id is required in memberData")
|
return "", fmt.Errorf("team_id is required in memberData")
|
||||||
}
|
}
|
||||||
if _, exists := memberData["role_id"]; !exists {
|
if _, exists := memberData["role_id"]; !exists {
|
||||||
return 0, fmt.Errorf("role_id is required in memberData")
|
return "", fmt.Errorf("role_id is required in memberData")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate member_id if not provided
|
||||||
|
var generatedMemberID string
|
||||||
|
if _, exists := memberData["member_id"]; !exists || memberData["member_id"] == nil || memberData["member_id"] == "" {
|
||||||
|
memberID, err := u.generateMemberIDWithRetry(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate member ID: %w", err)
|
||||||
|
}
|
||||||
|
memberData["member_id"] = memberID
|
||||||
|
generatedMemberID = memberID
|
||||||
|
} else {
|
||||||
|
generatedMemberID = memberData["member_id"].(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add __yao_team_id to the member data
|
// Add __yao_team_id to the member data
|
||||||
|
|
@ -147,7 +277,7 @@ func (u *DefaultUser) CreateMember(ctx context.Context, memberData maps.MapStrAn
|
||||||
|
|
||||||
if memberType == "user" && status != "pending" {
|
if memberType == "user" && status != "pending" {
|
||||||
if _, exists := memberData["user_id"]; !exists {
|
if _, exists := memberData["user_id"]; !exists {
|
||||||
return 0, fmt.Errorf("user_id is required for active user members")
|
return "", fmt.Errorf("user_id is required for active user members")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,7 +285,7 @@ func (u *DefaultUser) CreateMember(ctx context.Context, memberData maps.MapStrAn
|
||||||
if status == "pending" && memberData["invitation_id"] == nil {
|
if status == "pending" && memberData["invitation_id"] == nil {
|
||||||
invitationID, err := u.generateInvitationID()
|
invitationID, err := u.generateInvitationID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to generate invitation ID: %w", err)
|
return "", fmt.Errorf("failed to generate invitation ID: %w", err)
|
||||||
}
|
}
|
||||||
memberData["invitation_id"] = invitationID
|
memberData["invitation_id"] = invitationID
|
||||||
}
|
}
|
||||||
|
|
@ -175,22 +305,22 @@ func (u *DefaultUser) CreateMember(ctx context.Context, memberData maps.MapStrAn
|
||||||
}
|
}
|
||||||
|
|
||||||
m := model.Select(u.memberModel)
|
m := model.Select(u.memberModel)
|
||||||
id, err := m.Create(memberData)
|
_, err := m.Create(memberData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf(ErrFailedToCreateMember, err)
|
return "", fmt.Errorf(ErrFailedToCreateMember, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return int64(id), nil
|
return generatedMemberID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRobotMember creates a new robot member
|
// CreateRobotMember creates a new robot member
|
||||||
func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (int64, error) {
|
func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (string, error) {
|
||||||
// Validate required fields for robot members
|
// Validate required fields for robot members
|
||||||
if _, exists := robotData["display_name"]; !exists {
|
if _, exists := robotData["display_name"]; !exists {
|
||||||
return 0, fmt.Errorf("display_name is required for robot members")
|
return "", fmt.Errorf("display_name is required for robot members")
|
||||||
}
|
}
|
||||||
if _, exists := robotData["role_id"]; !exists {
|
if _, exists := robotData["role_id"]; !exists {
|
||||||
return 0, fmt.Errorf("role_id is required for robot members")
|
return "", fmt.Errorf("role_id is required for robot members")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if email already exists in this team
|
// Check if email already exists in this team
|
||||||
|
|
@ -206,10 +336,10 @@ func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robo
|
||||||
Limit: 1,
|
Limit: 1,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to check email uniqueness: %w", err)
|
return "", fmt.Errorf("failed to check email uniqueness: %w", err)
|
||||||
}
|
}
|
||||||
if len(existingMembers) > 0 {
|
if len(existingMembers) > 0 {
|
||||||
return 0, fmt.Errorf("email %s already exists in this team", emailStr)
|
return "", fmt.Errorf("email %s already exists in this team", emailStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,20 +386,20 @@ func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robo
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMember adds a user to a team (invitation-based)
|
// AddMember adds a user to a team (invitation-based)
|
||||||
func (u *DefaultUser) AddMember(ctx context.Context, teamID string, userID string, roleID string, invitedBy string) (int64, error) {
|
func (u *DefaultUser) AddMember(ctx context.Context, teamID string, userID string, roleID string, invitedBy string) (string, error) {
|
||||||
// Check if member already exists
|
// Check if member already exists
|
||||||
exists, err := u.MemberExists(ctx, teamID, userID)
|
exists, err := u.MemberExists(ctx, teamID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to check member existence: %w", err)
|
return "", fmt.Errorf("failed to check member existence: %w", err)
|
||||||
}
|
}
|
||||||
if exists {
|
if exists {
|
||||||
return 0, fmt.Errorf("user is already a member of this team")
|
return "", fmt.Errorf("user is already a member of this team")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate invitation token
|
// Generate invitation token
|
||||||
token, err := generateRandomPassword(32) // Use existing password generation for token
|
token, err := generateRandomPassword(32) // Use existing password generation for token
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to generate invitation token: %w", err)
|
return "", fmt.Errorf("failed to generate invitation token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
memberData := maps.MapStrAny{
|
memberData := maps.MapStrAny{
|
||||||
|
|
@ -370,7 +500,7 @@ func (u *DefaultUser) AcceptInvitation(ctx context.Context, invitationID string,
|
||||||
// UpdateMember updates an existing member
|
// UpdateMember updates an existing member
|
||||||
func (u *DefaultUser) UpdateMember(ctx context.Context, teamID string, userID string, memberData maps.MapStrAny) error {
|
func (u *DefaultUser) UpdateMember(ctx context.Context, teamID string, userID string, memberData maps.MapStrAny) error {
|
||||||
// Remove sensitive fields that should not be updated directly
|
// Remove sensitive fields that should not be updated directly
|
||||||
sensitiveFields := []string{"id", "team_id", "user_id", "created_at", "invitation_token"}
|
sensitiveFields := []string{"id", "member_id", "team_id", "user_id", "created_at", "invitation_token"}
|
||||||
for _, field := range sensitiveFields {
|
for _, field := range sensitiveFields {
|
||||||
delete(memberData, field)
|
delete(memberData, field)
|
||||||
}
|
}
|
||||||
|
|
@ -394,16 +524,24 @@ func (u *DefaultUser) UpdateMember(ctx context.Context, teamID string, userID st
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateMemberByID updates a member by internal ID
|
// UpdateMemberByID updates a member by internal database ID
|
||||||
func (u *DefaultUser) UpdateMemberByID(ctx context.Context, memberID int64, memberData maps.MapStrAny) error {
|
func (u *DefaultUser) UpdateMemberByID(ctx context.Context, id int64, memberData maps.MapStrAny) error {
|
||||||
// Remove sensitive fields that should not be updated directly
|
// Remove sensitive fields that should not be updated directly
|
||||||
sensitiveFields := []string{"id", "team_id", "user_id", "created_at", "invitation_token"}
|
sensitiveFields := []string{"id", "member_id", "team_id", "user_id", "created_at", "invitation_token"}
|
||||||
for _, field := range sensitiveFields {
|
for _, field := range sensitiveFields {
|
||||||
delete(memberData, field)
|
delete(memberData, field)
|
||||||
}
|
}
|
||||||
|
|
@ -416,7 +554,7 @@ func (u *DefaultUser) UpdateMemberByID(ctx context.Context, memberID int64, memb
|
||||||
m := model.Select(u.memberModel)
|
m := model.Select(u.memberModel)
|
||||||
affected, err := m.UpdateWhere(model.QueryParam{
|
affected, err := m.UpdateWhere(model.QueryParam{
|
||||||
Wheres: []model.QueryWhere{
|
Wheres: []model.QueryWhere{
|
||||||
{Column: "id", Value: memberID},
|
{Column: "id", Value: id},
|
||||||
},
|
},
|
||||||
Limit: 1,
|
Limit: 1,
|
||||||
}, memberData)
|
}, memberData)
|
||||||
|
|
@ -426,7 +564,60 @@ func (u *DefaultUser) UpdateMemberByID(ctx context.Context, memberID int64, memb
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMemberByMemberID updates a member by member_id (business ID)
|
||||||
|
func (u *DefaultUser) UpdateMemberByMemberID(ctx context.Context, memberID string, 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 {
|
||||||
|
delete(memberData, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip update if no valid fields remain
|
||||||
|
if len(memberData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
affected, err := m.UpdateWhere(model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "member_id", Value: memberID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
}, memberData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
|
// 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
|
return nil
|
||||||
|
|
@ -454,6 +645,27 @@ func (u *DefaultUser) RemoveMember(ctx context.Context, teamID string, userID st
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveMemberByMemberID removes a member by member_id (business ID, soft delete)
|
||||||
|
func (u *DefaultUser) RemoveMemberByMemberID(ctx context.Context, memberID string) error {
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
affected, err := m.DeleteWhere(model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "member_id", Value: memberID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(ErrFailedToDeleteMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if affected == 0 {
|
||||||
|
return fmt.Errorf(ErrMemberNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveAllTeamMembers removes all members from a team (used when deleting team)
|
// RemoveAllTeamMembers removes all members from a team (used when deleting team)
|
||||||
func (u *DefaultUser) RemoveAllTeamMembers(ctx context.Context, teamID string) error {
|
func (u *DefaultUser) RemoveAllTeamMembers(ctx context.Context, teamID string) error {
|
||||||
m := model.Select(u.memberModel)
|
m := model.Select(u.memberModel)
|
||||||
|
|
@ -621,14 +833,55 @@ func (u *DefaultUser) UpdateMemberLastActivity(ctx context.Context, teamID strin
|
||||||
return u.UpdateMember(ctx, teamID, userID, updateData)
|
return u.UpdateMember(ctx, teamID, userID, updateData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRobotActivity updates robot member's last activity and status
|
// UpdateMemberRoleByMemberID updates a member's role by member_id
|
||||||
func (u *DefaultUser) UpdateRobotActivity(ctx context.Context, memberID int64, robotStatus string) error {
|
func (u *DefaultUser) UpdateMemberRoleByMemberID(ctx context.Context, memberID string, roleID string) error {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"role_id": roleID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.UpdateMemberByMemberID(ctx, memberID, updateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMemberStatusByMemberID updates a member's status by member_id
|
||||||
|
func (u *DefaultUser) UpdateMemberStatusByMemberID(ctx context.Context, memberID string, status string) error {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.UpdateMemberByMemberID(ctx, memberID, updateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMemberLastActivityByMemberID updates a member's last activity time by member_id
|
||||||
|
func (u *DefaultUser) UpdateMemberLastActivityByMemberID(ctx context.Context, memberID string) error {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"last_active_at": time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also increment login count
|
||||||
|
member, err := u.GetMemberByMemberID(ctx, memberID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
loginCount := int64(0)
|
||||||
|
if count := member["login_count"]; count != nil {
|
||||||
|
if parsedCount, err := parseIntFromDB(count); err == nil {
|
||||||
|
loginCount = parsedCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateData["login_count"] = loginCount + 1
|
||||||
|
|
||||||
|
return u.UpdateMemberByMemberID(ctx, memberID, updateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{
|
updateData := maps.MapStrAny{
|
||||||
"last_robot_activity": time.Now(),
|
"last_robot_activity": time.Now(),
|
||||||
"robot_status": robotStatus,
|
"robot_status": robotStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
return u.UpdateMemberByID(ctx, memberID, updateData)
|
return u.UpdateMemberByID(ctx, id, updateData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateMemberByInvitationID updates a member by invitation_id
|
// UpdateMemberByInvitationID updates a member by invitation_id
|
||||||
|
|
@ -658,7 +911,15 @@ func (u *DefaultUser) UpdateMemberByInvitationID(ctx context.Context, invitation
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ func TestMemberBasicOperations(t *testing.T) {
|
||||||
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var memberID int64
|
var businessMemberID string
|
||||||
|
|
||||||
// Test CreateMember
|
// Test CreateMember
|
||||||
t.Run("CreateMember", func(t *testing.T) {
|
t.Run("CreateMember", func(t *testing.T) {
|
||||||
|
|
@ -52,10 +52,10 @@ func TestMemberBasicOperations(t *testing.T) {
|
||||||
"status": "active",
|
"status": "active",
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := testProvider.CreateMember(ctx, memberData)
|
memberID, err := testProvider.CreateMember(ctx, memberData)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Greater(t, id, int64(0))
|
assert.NotEmpty(t, memberID)
|
||||||
memberID = id
|
businessMemberID = memberID
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test GetMember
|
// Test GetMember
|
||||||
|
|
@ -81,13 +81,15 @@ func TestMemberBasicOperations(t *testing.T) {
|
||||||
assert.Contains(t, member, "updated_at")
|
assert.Contains(t, member, "updated_at")
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test GetMemberByID
|
// Test GetMemberByMemberID
|
||||||
t.Run("GetMemberByID", func(t *testing.T) {
|
t.Run("GetMemberByMemberID", func(t *testing.T) {
|
||||||
member, err := testProvider.GetMemberByID(ctx, memberID)
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, member)
|
assert.NotNil(t, member)
|
||||||
assert.Equal(t, teamID, member["team_id"])
|
assert.Equal(t, teamID, member["team_id"])
|
||||||
assert.Equal(t, memberUser, member["user_id"])
|
assert.Equal(t, memberUser, member["user_id"])
|
||||||
|
// Verify member_id is returned
|
||||||
|
assert.Equal(t, businessMemberID, member["member_id"])
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test MemberExists
|
// Test MemberExists
|
||||||
|
|
@ -129,22 +131,22 @@ func TestMemberBasicOperations(t *testing.T) {
|
||||||
assert.NoError(t, err) // Should not error, just ignore sensitive fields
|
assert.NoError(t, err) // Should not error, just ignore sensitive fields
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test UpdateMemberByID
|
// Test UpdateMemberByMemberID
|
||||||
t.Run("UpdateMemberByID", func(t *testing.T) {
|
t.Run("UpdateMemberByMemberID", func(t *testing.T) {
|
||||||
updateData := maps.MapStrAny{
|
updateData := maps.MapStrAny{
|
||||||
"status": "inactive",
|
"status": "inactive",
|
||||||
}
|
}
|
||||||
|
|
||||||
err := testProvider.UpdateMemberByID(ctx, memberID, updateData)
|
err := testProvider.UpdateMemberByMemberID(ctx, businessMemberID, updateData)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Verify update
|
// Verify update
|
||||||
member, err := testProvider.GetMemberByID(ctx, memberID)
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "inactive", member["status"])
|
assert.Equal(t, "inactive", member["status"])
|
||||||
|
|
||||||
// Change back to active for other tests
|
// Change back to active for other tests
|
||||||
err = testProvider.UpdateMemberByID(ctx, memberID, maps.MapStrAny{"status": "active"})
|
err = testProvider.UpdateMemberByMemberID(ctx, businessMemberID, maps.MapStrAny{"status": "active"})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -249,7 +251,7 @@ func TestMemberInvitationFlow(t *testing.T) {
|
||||||
t.Run("AddMember", func(t *testing.T) {
|
t.Run("AddMember", func(t *testing.T) {
|
||||||
memberID, err := testProvider.AddMember(ctx, teamID, inviteeUser, "user", ownerUser)
|
memberID, err := testProvider.AddMember(ctx, teamID, inviteeUser, "user", ownerUser)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Greater(t, memberID, int64(0))
|
assert.NotEmpty(t, memberID)
|
||||||
|
|
||||||
// Verify member was created with pending status
|
// Verify member was created with pending status
|
||||||
member, err := testProvider.GetMember(ctx, teamID, inviteeUser)
|
member, err := testProvider.GetMember(ctx, teamID, inviteeUser)
|
||||||
|
|
@ -335,7 +337,7 @@ func TestRobotMemberOperations(t *testing.T) {
|
||||||
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var robotMemberID int64
|
var robotBusinessMemberID string
|
||||||
|
|
||||||
// Test CreateRobotMember
|
// Test CreateRobotMember
|
||||||
t.Run("CreateRobotMember", func(t *testing.T) {
|
t.Run("CreateRobotMember", func(t *testing.T) {
|
||||||
|
|
@ -355,13 +357,13 @@ func TestRobotMemberOperations(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := testProvider.CreateRobotMember(ctx, teamID, robotData)
|
memberID, err := testProvider.CreateRobotMember(ctx, teamID, robotData)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Greater(t, id, int64(0))
|
assert.NotEmpty(t, memberID)
|
||||||
robotMemberID = id
|
robotBusinessMemberID = memberID
|
||||||
|
|
||||||
// Verify robot member was created
|
// Verify robot member was created
|
||||||
member, err := testProvider.GetMemberByID(ctx, robotMemberID)
|
member, err := testProvider.GetMemberByMemberID(ctx, robotBusinessMemberID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "robot", member["member_type"])
|
assert.Equal(t, "robot", member["member_type"])
|
||||||
assert.Equal(t, "active", member["status"]) // Robots are active by default
|
assert.Equal(t, "active", member["status"]) // Robots are active by default
|
||||||
|
|
@ -378,17 +380,30 @@ func TestRobotMemberOperations(t *testing.T) {
|
||||||
assert.Equal(t, "A test robot for unit testing", robots[0]["bio"])
|
assert.Equal(t, "A test robot for unit testing", robots[0]["bio"])
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test UpdateRobotActivity
|
// Test UpdateRobotActivity - needs internal database ID
|
||||||
t.Run("UpdateRobotActivity", func(t *testing.T) {
|
t.Run("UpdateRobotActivity", func(t *testing.T) {
|
||||||
err := testProvider.UpdateRobotActivity(ctx, robotMemberID, "working")
|
// Get internal database ID from member_id
|
||||||
assert.NoError(t, err)
|
member, err := testProvider.GetMemberByMemberID(ctx, robotBusinessMemberID)
|
||||||
|
|
||||||
// Verify robot activity was updated (use GetMemberDetail for full fields)
|
|
||||||
// First get team_id for the robot
|
|
||||||
member, err := testProvider.GetMemberByID(ctx, robotMemberID)
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
robotTeamID := member["team_id"].(string)
|
robotTeamID := member["team_id"].(string)
|
||||||
|
|
||||||
|
// UpdateRobotActivity still uses internal database ID
|
||||||
|
// We need to query the database to get it
|
||||||
|
m := model.Select("__yao.member")
|
||||||
|
result, err := m.Get(model.QueryParam{
|
||||||
|
Select: []interface{}{"id"},
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "member_id", Value: robotBusinessMemberID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, result, 1)
|
||||||
|
robotDBID := result[0]["id"].(int64)
|
||||||
|
|
||||||
|
err = testProvider.UpdateRobotActivity(ctx, robotDBID, "working")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Get robot members to verify status (robot members don't have user_id)
|
// Get robot members to verify status (robot members don't have user_id)
|
||||||
robots, err := testProvider.GetTeamRobotMembers(ctx, robotTeamID)
|
robots, err := testProvider.GetTeamRobotMembers(ctx, robotTeamID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
@ -401,16 +416,11 @@ func TestRobotMemberOperations(t *testing.T) {
|
||||||
// Test GetActiveRobotMembers
|
// Test GetActiveRobotMembers
|
||||||
t.Run("GetActiveRobotMembers", func(t *testing.T) {
|
t.Run("GetActiveRobotMembers", func(t *testing.T) {
|
||||||
// First make sure our robot is active
|
// First make sure our robot is active
|
||||||
err := testProvider.UpdateMemberByID(ctx, robotMemberID, maps.MapStrAny{
|
err := testProvider.UpdateMemberByMemberID(ctx, robotBusinessMemberID, maps.MapStrAny{
|
||||||
"autonomous_mode": true,
|
"autonomous_mode": true,
|
||||||
"status": "active",
|
"status": "active",
|
||||||
})
|
})
|
||||||
if err != nil {
|
assert.NoError(t, err)
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
robots, err := testProvider.GetActiveRobotMembers(ctx)
|
robots, err := testProvider.GetActiveRobotMembers(ctx)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
@ -741,11 +751,11 @@ func TestMemberInvitationExpiry(t *testing.T) {
|
||||||
"invitation_expires_at": expiredTime, // Expired 2 hours ago
|
"invitation_expires_at": expiredTime, // Expired 2 hours ago
|
||||||
}
|
}
|
||||||
|
|
||||||
memberID, err := testProvider.CreateMember(ctx, memberData)
|
businessMemberID, err := testProvider.CreateMember(ctx, memberData)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Get the invitation_id
|
// Get the invitation_id
|
||||||
member, err := testProvider.GetMemberByID(ctx, memberID)
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
invitationID := member["invitation_id"].(string)
|
invitationID := member["invitation_id"].(string)
|
||||||
assert.NotEmpty(t, invitationID)
|
assert.NotEmpty(t, invitationID)
|
||||||
|
|
@ -799,12 +809,12 @@ func TestMemberInvitationIDOperations(t *testing.T) {
|
||||||
"invited_by": ownerUser,
|
"invited_by": ownerUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
memberID, err := testProvider.CreateMember(ctx, memberData)
|
businessMemberID, err := testProvider.CreateMember(ctx, memberData)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Greater(t, memberID, int64(0))
|
assert.NotEmpty(t, businessMemberID)
|
||||||
|
|
||||||
// Get the created member to verify invitation_id was generated
|
// Get the created member to verify invitation_id was generated
|
||||||
member, err := testProvider.GetMemberByID(ctx, memberID)
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, member["invitation_id"])
|
assert.NotNil(t, member["invitation_id"])
|
||||||
assert.NotEmpty(t, member["invitation_id"])
|
assert.NotEmpty(t, member["invitation_id"])
|
||||||
|
|
@ -937,11 +947,11 @@ func TestCreateMemberInvitationIDGeneration(t *testing.T) {
|
||||||
"invited_by": ownerUser,
|
"invited_by": ownerUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
memberID, err := testProvider.CreateMember(ctx, memberData)
|
businessMemberID, err := testProvider.CreateMember(ctx, memberData)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Get the created member
|
// Get the created member
|
||||||
member, err := testProvider.GetMemberByID(ctx, memberID)
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Verify invitation_id was generated
|
// Verify invitation_id was generated
|
||||||
|
|
@ -966,11 +976,11 @@ func TestCreateMemberInvitationIDGeneration(t *testing.T) {
|
||||||
"status": "active",
|
"status": "active",
|
||||||
}
|
}
|
||||||
|
|
||||||
memberID, err := testProvider.CreateMember(ctx, memberData)
|
businessMemberID, err := testProvider.CreateMember(ctx, memberData)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Get the created member
|
// Get the created member
|
||||||
member, err := testProvider.GetMemberByID(ctx, memberID)
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Verify invitation_id is nil for active members
|
// Verify invitation_id is nil for active members
|
||||||
|
|
@ -991,11 +1001,11 @@ func TestCreateMemberInvitationIDGeneration(t *testing.T) {
|
||||||
"invitation_id": explicitInvitationID,
|
"invitation_id": explicitInvitationID,
|
||||||
}
|
}
|
||||||
|
|
||||||
memberID, err := testProvider.CreateMember(ctx, memberData)
|
businessMemberID, err := testProvider.CreateMember(ctx, memberData)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Get the created member
|
// Get the created member
|
||||||
member, err := testProvider.GetMemberByID(ctx, memberID)
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Verify explicit invitation_id was preserved
|
// Verify explicit invitation_id was preserved
|
||||||
|
|
@ -1003,4 +1013,215 @@ func TestCreateMemberInvitationIDGeneration(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMemberIDOperations(t *testing.T) {
|
||||||
|
prepare(t)
|
||||||
|
defer clean()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Use UUID to ensure unique identifiers
|
||||||
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||||
|
|
||||||
|
// Create test users
|
||||||
|
ownerUser := createTestUser(ctx, t, "owner"+testUUID)
|
||||||
|
memberUser := createTestUser(ctx, t, "member"+testUUID)
|
||||||
|
|
||||||
|
// Create test team
|
||||||
|
teamMap := maps.MapStrAny{
|
||||||
|
"name": "Member ID Test Team " + testUUID,
|
||||||
|
"display_name": "Member ID Test " + testUUID,
|
||||||
|
"description": "A test team for member_id testing",
|
||||||
|
"owner_id": ownerUser,
|
||||||
|
"status": "active",
|
||||||
|
"type": "corporation",
|
||||||
|
"type_id": "business",
|
||||||
|
"metadata": map[string]interface{}{"test": true},
|
||||||
|
}
|
||||||
|
|
||||||
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var businessMemberID string
|
||||||
|
|
||||||
|
// Test CreateMember generates member_id
|
||||||
|
t.Run("CreateMember_GeneratesMemberID", func(t *testing.T) {
|
||||||
|
memberData := maps.MapStrAny{
|
||||||
|
"team_id": teamID,
|
||||||
|
"user_id": memberUser,
|
||||||
|
"member_type": "user",
|
||||||
|
"role_id": "user",
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := testProvider.CreateMember(ctx, memberData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Get the created member to verify member_id was generated
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member["member_id"])
|
||||||
|
assert.NotEmpty(t, member["member_id"])
|
||||||
|
|
||||||
|
businessMemberID = member["member_id"].(string)
|
||||||
|
t.Logf("Generated member_id: %s", businessMemberID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetMemberByMemberID
|
||||||
|
t.Run("GetMemberByMemberID", func(t *testing.T) {
|
||||||
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member)
|
||||||
|
assert.Equal(t, businessMemberID, member["member_id"])
|
||||||
|
assert.Equal(t, teamID, member["team_id"])
|
||||||
|
assert.Equal(t, memberUser, member["user_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetMemberDetailByMemberID
|
||||||
|
t.Run("GetMemberDetailByMemberID", func(t *testing.T) {
|
||||||
|
member, err := testProvider.GetMemberDetailByMemberID(ctx, businessMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member)
|
||||||
|
assert.Equal(t, businessMemberID, member["member_id"])
|
||||||
|
assert.Equal(t, teamID, member["team_id"])
|
||||||
|
// Should contain detailed fields
|
||||||
|
assert.Contains(t, member, "created_at")
|
||||||
|
assert.Contains(t, member, "updated_at")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMemberByMemberID
|
||||||
|
t.Run("UpdateMemberByMemberID", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"role_id": "admin",
|
||||||
|
"notes": "Updated via member_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := testProvider.UpdateMemberByMemberID(ctx, businessMemberID, updateData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "admin", member["role_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMemberRoleByMemberID
|
||||||
|
t.Run("UpdateMemberRoleByMemberID", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateMemberRoleByMemberID(ctx, businessMemberID, "moderator")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify role was updated
|
||||||
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "moderator", member["role_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMemberStatusByMemberID
|
||||||
|
t.Run("UpdateMemberStatusByMemberID", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateMemberStatusByMemberID(ctx, businessMemberID, "suspended")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify status was updated
|
||||||
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "suspended", member["status"])
|
||||||
|
|
||||||
|
// Change back to active
|
||||||
|
err = testProvider.UpdateMemberStatusByMemberID(ctx, businessMemberID, "active")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMemberLastActivityByMemberID
|
||||||
|
t.Run("UpdateMemberLastActivityByMemberID", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateMemberLastActivityByMemberID(ctx, businessMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify last_active_at was updated and login_count incremented
|
||||||
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member["last_active_at"])
|
||||||
|
// login_count should be at least 1
|
||||||
|
loginCount := member["login_count"]
|
||||||
|
if loginCount != nil {
|
||||||
|
switch v := loginCount.(type) {
|
||||||
|
case int:
|
||||||
|
assert.True(t, v >= 1, "login_count should be at least 1")
|
||||||
|
case int64:
|
||||||
|
assert.True(t, v >= 1, "login_count should be at least 1")
|
||||||
|
case int32:
|
||||||
|
assert.True(t, v >= 1, "login_count should be at least 1")
|
||||||
|
default:
|
||||||
|
t.Logf("Unexpected login_count type: %T, value: %v", loginCount, loginCount)
|
||||||
|
assert.True(t, false, "login_count should be a numeric type")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.True(t, false, "login_count should not be nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test RemoveMemberByMemberID (at the end)
|
||||||
|
t.Run("RemoveMemberByMemberID", func(t *testing.T) {
|
||||||
|
err := testProvider.RemoveMemberByMemberID(ctx, businessMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify member was removed
|
||||||
|
_, err = testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "member not found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemberExistsByTeamEmail(t *testing.T) {
|
||||||
|
prepare(t)
|
||||||
|
defer clean()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Use UUID to ensure unique identifiers
|
||||||
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||||
|
|
||||||
|
// Create test users
|
||||||
|
ownerUser := createTestUser(ctx, t, "owner"+testUUID)
|
||||||
|
|
||||||
|
// Create test team
|
||||||
|
teamMap := maps.MapStrAny{
|
||||||
|
"name": "Email Test Team " + testUUID,
|
||||||
|
"display_name": "Email Test " + testUUID,
|
||||||
|
"description": "A test team for email testing",
|
||||||
|
"owner_id": ownerUser,
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testEmail := "test" + testUUID + "@example.com"
|
||||||
|
|
||||||
|
// Create robot member with email
|
||||||
|
t.Run("CreateRobotMemberWithEmail", func(t *testing.T) {
|
||||||
|
robotData := maps.MapStrAny{
|
||||||
|
"display_name": "TestBot" + testUUID,
|
||||||
|
"email": testEmail,
|
||||||
|
"role_id": "bot",
|
||||||
|
}
|
||||||
|
|
||||||
|
businessMemberID, err := testProvider.CreateRobotMember(ctx, teamID, robotData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, businessMemberID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test MemberExistsByTeamEmail
|
||||||
|
t.Run("MemberExistsByTeamEmail_Exists", func(t *testing.T) {
|
||||||
|
exists, err := testProvider.MemberExistsByTeamEmail(ctx, teamID, testEmail)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, exists)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with non-existent email
|
||||||
|
t.Run("MemberExistsByTeamEmail_NotExists", func(t *testing.T) {
|
||||||
|
exists, err := testProvider.MemberExistsByTeamEmail(ctx, teamID, "nonexistent@example.com")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, exists)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function createTestUser is defined in team_test.go
|
// Helper function createTestUser is defined in team_test.go
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,15 @@ func (u *DefaultUser) UpdateOAuthAccount(ctx context.Context, provider string, s
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,15 @@ func (u *DefaultUser) UpdateRole(ctx context.Context, roleID string, roleData ma
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
@ -280,7 +288,15 @@ func (u *DefaultUser) SetRolePermissions(ctx context.Context, roleID string, per
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,15 @@ func (u *DefaultUser) UpdateTeam(ctx context.Context, teamID string, teamData ma
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
@ -440,7 +448,15 @@ func (u *DefaultUser) VerifyTeam(ctx context.Context, teamID string, verifiedBy
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
@ -468,7 +484,15 @@ func (u *DefaultUser) UnverifyTeam(ctx context.Context, teamID string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -330,14 +330,14 @@ func TestTeamMemberOperations(t *testing.T) {
|
||||||
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var memberID int64
|
var businessMemberID string
|
||||||
|
|
||||||
// Test AddMember (invitation-based)
|
// Test AddMember (invitation-based)
|
||||||
t.Run("AddMember", func(t *testing.T) {
|
t.Run("AddMember", func(t *testing.T) {
|
||||||
id, err := testProvider.AddMember(ctx, teamID, memberUser, "user", ownerUser)
|
memberID, err := testProvider.AddMember(ctx, teamID, memberUser, "user", ownerUser)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Greater(t, id, int64(0))
|
assert.NotEmpty(t, memberID)
|
||||||
memberID = id
|
businessMemberID = memberID
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test MemberExists
|
// Test MemberExists
|
||||||
|
|
@ -362,9 +362,9 @@ func TestTeamMemberOperations(t *testing.T) {
|
||||||
assert.Equal(t, "pending", member["status"]) // Initially pending
|
assert.Equal(t, "pending", member["status"]) // Initially pending
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test GetMemberByID
|
// Test GetMemberByMemberID
|
||||||
t.Run("GetMemberByID", func(t *testing.T) {
|
t.Run("GetMemberByMemberID", func(t *testing.T) {
|
||||||
member, err := testProvider.GetMemberByID(ctx, memberID)
|
member, err := testProvider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, member)
|
assert.NotNil(t, member)
|
||||||
assert.Equal(t, teamID, member["team_id"])
|
assert.Equal(t, teamID, member["team_id"])
|
||||||
|
|
@ -462,9 +462,9 @@ func TestTeamMemberOperations(t *testing.T) {
|
||||||
"robot_status": "idle",
|
"robot_status": "idle",
|
||||||
}
|
}
|
||||||
|
|
||||||
robotID, err := testProvider.CreateRobotMember(ctx, teamID, robotData)
|
robotMemberID, err := testProvider.CreateRobotMember(ctx, teamID, robotData)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Greater(t, robotID, int64(0))
|
assert.NotEmpty(t, robotMemberID)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test GetTeamRobotMembers
|
// Test GetTeamRobotMembers
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,15 @@ func (u *DefaultUser) UpdateType(ctx context.Context, typeID string, typeData ma
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
@ -285,7 +293,15 @@ func (u *DefaultUser) SetTypeConfiguration(ctx context.Context, typeID string, c
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
@ -424,7 +440,15 @@ func (u *DefaultUser) SetTypePricing(ctx context.Context, typeID string, pricing
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
@ -458,7 +482,15 @@ func (u *DefaultUser) UpdateTypeStatus(ctx context.Context, typeID string, statu
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,15 @@ func (u *DefaultUser) UpdatePassword(ctx context.Context, userID string, newPass
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
@ -327,7 +335,15 @@ func (u *DefaultUser) ResetPassword(ctx context.Context, userID string) (string,
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return randomPassword, nil
|
||||||
|
|
@ -400,7 +416,15 @@ func (u *DefaultUser) UpdateUser(ctx context.Context, userID string, userData ma
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,15 @@ func (u *DefaultUser) EnableMFA(ctx context.Context, userID string, secret strin
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
@ -272,7 +280,15 @@ func (u *DefaultUser) DisableMFA(ctx context.Context, userID string, code string
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
@ -416,7 +432,15 @@ func (u *DefaultUser) GenerateRecoveryCodes(ctx context.Context, userID string)
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
// Return all generated recovery codes
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,15 @@ func (u *DefaultUser) SetUserRole(ctx context.Context, userID string, roleID str
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
@ -270,7 +278,15 @@ func (u *DefaultUser) SetUserType(ctx context.Context, userID string, typeID str
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected == 0 {
|
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
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,79 @@ func (u *DefaultUser) generateInvitationID() (string, error) {
|
||||||
return prefix + id, nil
|
return prefix + id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateMemberID generates a new member_id based on configured strategy (internal use)
|
||||||
|
func (u *DefaultUser) generateMemberID() (string, error) {
|
||||||
|
var id string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch u.idStrategy {
|
||||||
|
case UUIDStrategy:
|
||||||
|
id, err = generateUUID()
|
||||||
|
case NanoIDStrategy:
|
||||||
|
id, err = generateNanoID(12) // 12 characters, URL-safe, readable
|
||||||
|
case NumericStrategy:
|
||||||
|
id, err = generateNumericID(12) // 12 characters, numeric, readable (default)
|
||||||
|
default:
|
||||||
|
id, err = generateNumericID(12) // 12 characters, URL-safe, readable
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add prefix if configured
|
||||||
|
if u.idPrefix != "" {
|
||||||
|
return u.idPrefix + id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateMemberIDWithRetry generates a unique member_id with collision detection
|
||||||
|
func (u *DefaultUser) generateMemberIDWithRetry(ctx context.Context) (string, error) {
|
||||||
|
const maxRetries = 10 // Prevent infinite loops
|
||||||
|
|
||||||
|
for i := 0; i < maxRetries; i++ {
|
||||||
|
// Generate new ID
|
||||||
|
id, err := u.generateMemberID()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate member_id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if ID already exists
|
||||||
|
exists, err := u.memberIDExists(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to check member_id existence: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return id, nil // Found unique ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID exists, retry with new generation
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("failed to generate unique member_id after %d retries", maxRetries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// memberIDExists checks if a member_id already exists in the database
|
||||||
|
func (u *DefaultUser) memberIDExists(ctx context.Context, memberID string) (bool, error) {
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(model.QueryParam{
|
||||||
|
Select: []interface{}{"id"}, // Just get primary key, minimal data
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "member_id", Value: memberID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(members) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
// userIDExists checks if a user_id already exists in the database
|
// userIDExists checks if a user_id already exists in the database
|
||||||
func (u *DefaultUser) userIDExists(ctx context.Context, userID string) (bool, error) {
|
func (u *DefaultUser) userIDExists(ctx context.Context, userID string) (bool, error) {
|
||||||
m := model.Select(u.model)
|
m := model.Select(u.model)
|
||||||
|
|
|
||||||
|
|
@ -296,22 +296,27 @@ type UserProvider interface {
|
||||||
GetMember(ctx context.Context, teamID string, userID string) (maps.MapStrAny, error)
|
GetMember(ctx context.Context, teamID string, userID string) (maps.MapStrAny, error)
|
||||||
GetMemberDetail(ctx context.Context, teamID string, userID string) (maps.MapStrAny, error)
|
GetMemberDetail(ctx context.Context, teamID string, userID string) (maps.MapStrAny, error)
|
||||||
GetMemberByID(ctx context.Context, memberID int64) (maps.MapStrAny, error)
|
GetMemberByID(ctx context.Context, memberID int64) (maps.MapStrAny, error)
|
||||||
|
GetMemberByMemberID(ctx context.Context, memberID string) (maps.MapStrAny, error)
|
||||||
|
GetMemberDetailByMemberID(ctx context.Context, memberID string) (maps.MapStrAny, error)
|
||||||
GetMemberByInvitationID(ctx context.Context, invitationID string) (maps.MapStrAny, error)
|
GetMemberByInvitationID(ctx context.Context, invitationID string) (maps.MapStrAny, error)
|
||||||
MemberExists(ctx context.Context, teamID string, userID string) (bool, error)
|
MemberExists(ctx context.Context, teamID string, userID string) (bool, error)
|
||||||
CreateMember(ctx context.Context, memberData maps.MapStrAny) (int64, error)
|
MemberExistsByTeamEmail(ctx context.Context, teamID string, email string) (bool, error)
|
||||||
|
CreateMember(ctx context.Context, memberData maps.MapStrAny) (string, error)
|
||||||
UpdateMember(ctx context.Context, teamID string, userID string, memberData maps.MapStrAny) error
|
UpdateMember(ctx context.Context, teamID string, userID string, memberData maps.MapStrAny) error
|
||||||
UpdateMemberByID(ctx context.Context, memberID int64, memberData maps.MapStrAny) error
|
UpdateMemberByID(ctx context.Context, memberID int64, memberData maps.MapStrAny) error
|
||||||
|
UpdateMemberByMemberID(ctx context.Context, memberID string, memberData maps.MapStrAny) error
|
||||||
UpdateMemberByInvitationID(ctx context.Context, invitationID string, memberData maps.MapStrAny) error
|
UpdateMemberByInvitationID(ctx context.Context, invitationID string, memberData maps.MapStrAny) error
|
||||||
RemoveMember(ctx context.Context, teamID string, userID string) error
|
RemoveMember(ctx context.Context, teamID string, userID string) error
|
||||||
|
RemoveMemberByMemberID(ctx context.Context, memberID string) error
|
||||||
RemoveMemberByInvitationID(ctx context.Context, invitationID string) error
|
RemoveMemberByInvitationID(ctx context.Context, invitationID string) error
|
||||||
RemoveAllTeamMembers(ctx context.Context, teamID string) error
|
RemoveAllTeamMembers(ctx context.Context, teamID string) error
|
||||||
|
|
||||||
// Member Invitation Management
|
// Member Invitation Management
|
||||||
AddMember(ctx context.Context, teamID string, userID string, roleID string, invitedBy string) (int64, error)
|
AddMember(ctx context.Context, teamID string, userID string, roleID string, invitedBy string) (string, error)
|
||||||
AcceptInvitation(ctx context.Context, invitationID string, invitationToken string, userID string) error
|
AcceptInvitation(ctx context.Context, invitationID string, invitationToken string, userID string) error
|
||||||
|
|
||||||
// Robot Member Operations
|
// Robot Member Operations
|
||||||
CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (int64, error)
|
CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (string, error)
|
||||||
UpdateRobotActivity(ctx context.Context, memberID int64, robotStatus string) error
|
UpdateRobotActivity(ctx context.Context, memberID int64, robotStatus string) error
|
||||||
GetActiveRobotMembers(ctx context.Context) ([]maps.MapStr, error)
|
GetActiveRobotMembers(ctx context.Context) ([]maps.MapStr, error)
|
||||||
|
|
||||||
|
|
@ -323,8 +328,11 @@ type UserProvider interface {
|
||||||
|
|
||||||
// Member Management
|
// Member Management
|
||||||
UpdateMemberRole(ctx context.Context, teamID string, userID string, roleID string) error
|
UpdateMemberRole(ctx context.Context, teamID string, userID string, roleID string) error
|
||||||
|
UpdateMemberRoleByMemberID(ctx context.Context, memberID string, roleID string) error
|
||||||
UpdateMemberStatus(ctx context.Context, teamID string, userID string, status string) error
|
UpdateMemberStatus(ctx context.Context, teamID string, userID string, status string) error
|
||||||
|
UpdateMemberStatusByMemberID(ctx context.Context, memberID string, status string) error
|
||||||
UpdateMemberLastActivity(ctx context.Context, teamID string, userID string) error
|
UpdateMemberLastActivity(ctx context.Context, teamID string, userID string) error
|
||||||
|
UpdateMemberLastActivityByMemberID(ctx context.Context, memberID string) error
|
||||||
|
|
||||||
// Member List and Search
|
// Member List and Search
|
||||||
PaginateMembers(ctx context.Context, param model.QueryParam, page int, pagesize int) (maps.MapStr, error)
|
PaginateMembers(ctx context.Context, param model.QueryParam, page int, pagesize int) (maps.MapStr, error)
|
||||||
|
|
|
||||||
|
|
@ -463,7 +463,7 @@ func setupACLTestData(t *testing.T, ctx context.Context, provider types.UserProv
|
||||||
data := &ACLTestData{
|
data := &ACLTestData{
|
||||||
UserIDs: make([]string, 0),
|
UserIDs: make([]string, 0),
|
||||||
TeamIDs: make([]string, 0),
|
TeamIDs: make([]string, 0),
|
||||||
MemberIDs: make([]int64, 0),
|
MemberIDs: make([]string, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up roles first
|
// Set up roles first
|
||||||
|
|
@ -577,7 +577,7 @@ func cleanupACLTestData(t *testing.T, ctx context.Context, provider types.UserPr
|
||||||
type ACLTestData struct {
|
type ACLTestData struct {
|
||||||
UserIDs []string
|
UserIDs []string
|
||||||
TeamIDs []string
|
TeamIDs []string
|
||||||
MemberIDs []int64
|
MemberIDs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestEnforceIntegration tests the complete enforcement flow
|
// TestEnforceIntegration tests the complete enforcement flow
|
||||||
|
|
|
||||||
|
|
@ -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)
|
t.Logf("Created test user: %s with subject: %s", testUserID, subject)
|
||||||
return 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package user_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -11,6 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
"github.com/yaoapp/yao/openapi"
|
"github.com/yaoapp/yao/openapi"
|
||||||
"github.com/yaoapp/yao/openapi/tests/testutils"
|
"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
|
// TestMemberUpdate tests the PUT /user/teams/:team_id/members/:member_id endpoint
|
||||||
func TestMemberUpdate(t *testing.T) {
|
func TestMemberUpdate(t *testing.T) {
|
||||||
// Initialize test environment
|
// Initialize test environment
|
||||||
|
|
@ -819,22 +634,6 @@ func TestMemberPermissionVerification(t *testing.T) {
|
||||||
200,
|
200,
|
||||||
"member should be able to get member details",
|
"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",
|
"owner can update members",
|
||||||
"/user/teams/" + teamID + "/members/" + memberID,
|
"/user/teams/" + teamID + "/members/" + memberID,
|
||||||
|
|
@ -944,42 +743,32 @@ func createTestTeam(t *testing.T, serverURL, baseURL, accessToken, teamName stri
|
||||||
return team
|
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 {
|
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,
|
"user_id": userID,
|
||||||
"member_type": "user",
|
"member_type": "user",
|
||||||
"role_id": "member",
|
"role_id": "team:member",
|
||||||
|
"status": "active",
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyBytes, err := json.Marshal(createMemberBody)
|
// Create member directly in database
|
||||||
assert.NoError(t, err, "Should marshal member creation body")
|
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))
|
t.Logf("Created test member directly in database: user_id=%s, member_id=%s, team_id=%s", userID, memberID, teamID)
|
||||||
assert.NoError(t, err, "Should create member creation request")
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
// Return user_id (which is used as member identifier in API context)
|
||||||
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 userID
|
return userID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1333,4 +1122,162 @@ func toString(v interface{}) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMemberCheckEmail tests the GET /user/teams/:team_id/members/check endpoint
|
||||||
|
func TestMemberCheckEmail(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 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, "Email Check Test Team "+testUUID)
|
||||||
|
teamID := getTeamID(createdTeam)
|
||||||
|
|
||||||
|
// Create a robot member with a known email
|
||||||
|
existingEmail := fmt.Sprintf("existing-robot-%s@test.com", testUUID)
|
||||||
|
robotBody := map[string]interface{}{
|
||||||
|
"name": "Existing Robot",
|
||||||
|
"email": existingEmail,
|
||||||
|
"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
|
||||||
|
email string
|
||||||
|
headers map[string]string
|
||||||
|
expectCode int
|
||||||
|
expectExists bool
|
||||||
|
expectMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"check email without authentication",
|
||||||
|
teamID,
|
||||||
|
existingEmail,
|
||||||
|
map[string]string{},
|
||||||
|
401,
|
||||||
|
false,
|
||||||
|
"should require authentication",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check existing email",
|
||||||
|
teamID,
|
||||||
|
existingEmail,
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
true,
|
||||||
|
"should return exists=true for existing email",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check non-existing email",
|
||||||
|
teamID,
|
||||||
|
fmt.Sprintf("nonexistent-%s@test.com", testUUID),
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
false,
|
||||||
|
"should return exists=false for non-existing email",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check email without email parameter",
|
||||||
|
teamID,
|
||||||
|
"",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
false,
|
||||||
|
"should require email parameter",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"check email in non-existent team",
|
||||||
|
"non-existent-team-id",
|
||||||
|
"test@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"
|
||||||
|
if tc.email != "" {
|
||||||
|
requestURL += "?email=" + tc.email
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
assert.Contains(t, response, "exists", "Should have exists field")
|
||||||
|
assert.Contains(t, response, "email", "Should have email field")
|
||||||
|
assert.Contains(t, response, "team_id", "Should have team_id field")
|
||||||
|
|
||||||
|
// Verify values
|
||||||
|
assert.Equal(t, tc.expectExists, response["exists"], "Should have correct exists value")
|
||||||
|
assert.Equal(t, tc.email, response["email"], "Should have correct email")
|
||||||
|
assert.Equal(t, teamID, response["team_id"], "Should have correct team_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Member check email test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Note: getTeamID function is already defined in team_test.go
|
// Note: getTeamID function is already defined in team_test.go
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,75 @@ func GinMemberList(c *gin.Context) {
|
||||||
response.RespondWithSuccess(c, http.StatusOK, result)
|
response.RespondWithSuccess(c, http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GinMemberCheckEmail handles GET /api/user/teams/:id/members/check?email=xxx - Check if email exists in team
|
||||||
|
func GinMemberCheckEmail(c *gin.Context) {
|
||||||
|
// Get authorized user info
|
||||||
|
authInfo := oauth.GetAuthorizedInfo(c)
|
||||||
|
if authInfo == nil || authInfo.UserID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidClient.Code,
|
||||||
|
ErrorDescription: "User not authenticated",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
teamID := c.Param("id")
|
||||||
|
if teamID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Team ID is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
email := c.Query("email")
|
||||||
|
if email == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Email query parameter is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call business logic
|
||||||
|
exists, err := memberCheckEmail(c.Request.Context(), authInfo.UserID, teamID, email)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to check member email: %v", err)
|
||||||
|
// Check error type for appropriate response
|
||||||
|
if strings.Contains(err.Error(), "not found") {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Team not found",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
} else if strings.Contains(err.Error(), "access denied") {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrAccessDenied.Code,
|
||||||
|
ErrorDescription: err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||||
|
} else {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: fmt.Sprintf("Failed to check member email: %v", err),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return result
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"exists": exists,
|
||||||
|
"email": email,
|
||||||
|
"team_id": teamID,
|
||||||
|
}
|
||||||
|
response.RespondWithSuccess(c, http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
// GinMemberGet handles GET /teams/:team_id/members/:member_id - Get team member details
|
// GinMemberGet handles GET /teams/:team_id/members/:member_id - Get team member details
|
||||||
func GinMemberGet(c *gin.Context) {
|
func GinMemberGet(c *gin.Context) {
|
||||||
// Get authorized user info
|
// Get authorized user info
|
||||||
|
|
@ -620,29 +689,57 @@ func memberGet(ctx context.Context, userID, teamID, memberID string) (maps.MapSt
|
||||||
return memberData, nil
|
return memberData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// memberCreateRobot handles the business logic for creating a robot member
|
// memberCheckEmail handles the business logic for checking if member exists by team_id and email
|
||||||
func memberCreateRobot(ctx context.Context, userID, teamID string, robotData maps.MapStrAny) (int64, error) {
|
func memberCheckEmail(ctx context.Context, userID, teamID, email string) (bool, error) {
|
||||||
// Check if user has access to the team (write permission: owner only)
|
// Check if user has access to the team (read permission: owner or member)
|
||||||
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
isOwner, isMember, err := checkTeamAccess(ctx, teamID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow access if user is owner
|
// Allow access if user is owner or member
|
||||||
if !isOwner {
|
if !isOwner && !isMember {
|
||||||
return 0, fmt.Errorf("access denied: only team owner can add robot members")
|
return false, fmt.Errorf("access denied: user is not a member of this team")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user provider instance
|
// Get user provider instance
|
||||||
provider, err := getUserProvider()
|
provider, err := getUserProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to get user provider: %w", err)
|
return false, fmt.Errorf("failed to get user provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if member exists by team_id and email
|
||||||
|
exists, err := provider.MemberExistsByTeamEmail(ctx, teamID, email)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to check member existence: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return exists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// memberCreateRobot handles the business logic for creating a robot member
|
||||||
|
func memberCreateRobot(ctx context.Context, userID, teamID string, robotData maps.MapStrAny) (string, error) {
|
||||||
|
// Check if user has access to the team (write permission: owner only)
|
||||||
|
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow access if user is owner
|
||||||
|
if !isOwner {
|
||||||
|
return "", fmt.Errorf("access denied: only team owner can add robot members")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get user provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use CreateRobotMember method which handles robot-specific logic
|
// Use CreateRobotMember method which handles robot-specific logic
|
||||||
memberID, err := provider.CreateRobotMember(ctx, teamID, robotData)
|
memberID, err := provider.CreateRobotMember(ctx, teamID, robotData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to create robot member: %w", err)
|
return "", fmt.Errorf("failed to create robot member: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return memberID, nil
|
return memberID, nil
|
||||||
|
|
|
||||||
|
|
@ -1162,13 +1162,13 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
||||||
invitationData["updated_at"] = time.Now()
|
invitationData["updated_at"] = time.Now()
|
||||||
|
|
||||||
// Create invitation (as a pending member)
|
// Create invitation (as a pending member)
|
||||||
memberID, err := provider.CreateMember(ctx, invitationData)
|
businessMemberID, err := provider.CreateMember(ctx, invitationData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to create invitation: %w", err)
|
return "", fmt.Errorf("failed to create invitation: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the created member to retrieve the generated invitation_id
|
// Get the created member to retrieve the generated invitation_id
|
||||||
createdMember, err := provider.GetMemberByID(ctx, memberID)
|
createdMember, err := provider.GetMemberByMemberID(ctx, businessMemberID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to retrieve created invitation: %w", err)
|
return "", fmt.Errorf("failed to retrieve created invitation: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,11 +133,12 @@ func attachTeam(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
team.GET("/current", GinTeamCurrent)
|
team.GET("/current", GinTeamCurrent)
|
||||||
|
|
||||||
// Team Members - Nested resource endpoints
|
// Team Members - Nested resource endpoints
|
||||||
team.GET("/:id/members", GinMemberList) // GET /teams/:id/members - List team members
|
team.GET("/:id/members", GinMemberList) // GET /api/user/teams/:id/members - List team members
|
||||||
team.POST("/:id/members/robots", GinMemberCreateRobot) // POST /teams/:id/members/robots - Add robot member
|
team.GET("/:id/members/check", GinMemberCheckEmail) // GET /api/user/teams/:id/members/check?email=xxx - Check if email exists in team
|
||||||
team.GET("/:id/members/:member_id", GinMemberGet) // GET /teams/:id/members/:member_id - Get member details
|
team.POST("/:id/members/robots", GinMemberCreateRobot) // POST /api/user/teams/:id/members/robots - Add robot member
|
||||||
team.PUT("/:id/members/:member_id", GinMemberUpdate) // PUT /teams/:id/members/:member_id - Update member
|
team.GET("/:id/members/:member_id", GinMemberGet) // GET /api/user/teams/:id/members/:member_id - Get member details
|
||||||
team.DELETE("/:id/members/:member_id", GinMemberDelete) // DELETE /teams/:id/members/:member_id - Remove member
|
team.PUT("/:id/members/:member_id", GinMemberUpdate) // PUT /api/user/teams/:id/members/:member_id - Update member
|
||||||
|
team.DELETE("/:id/members/:member_id", GinMemberDelete) // DELETE /api/user/teams/:id/members/:member_id - Remove member
|
||||||
|
|
||||||
// Team Invitations - Nested resource endpoints
|
// Team Invitations - Nested resource endpoints
|
||||||
team.GET("/:id/invitations", GinTeamInvitationList) // GET /teams/:id/invitations - List invitations
|
team.GET("/:id/invitations", GinTeamInvitationList) // GET /teams/:id/invitations - List invitations
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,15 @@
|
||||||
"comment": "Primary key identifier",
|
"comment": "Primary key identifier",
|
||||||
"primary": true
|
"primary": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "member_id",
|
||||||
|
"type": "string",
|
||||||
|
"label": "Member ID",
|
||||||
|
"comment": "Global unique member identifier",
|
||||||
|
"length": 255,
|
||||||
|
"unique": true,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "team_id",
|
"name": "team_id",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue