Add team and member management capabilities to user provider
- Introduced new constants for team and member error handling, enhancing error reporting. - Added default field lists for teams and members, improving data structure organization. - Expanded the UserProvider interface with methods for team and member management, including creation, updates, and retrieval. - Enhanced the NewDefaultUser function to support team and member configurations, ensuring flexibility in user management.
This commit is contained in:
parent
a2df75b6e2
commit
b20ddee701
6 changed files with 2392 additions and 0 deletions
|
|
@ -11,6 +11,8 @@ const (
|
||||||
ErrRoleNotFound = "role not found"
|
ErrRoleNotFound = "role not found"
|
||||||
ErrTypeNotFound = "type not found"
|
ErrTypeNotFound = "type not found"
|
||||||
ErrOAuthAccountNotFound = "oauth account not found"
|
ErrOAuthAccountNotFound = "oauth account not found"
|
||||||
|
ErrTeamNotFound = "team not found"
|
||||||
|
ErrMemberNotFound = "member not found"
|
||||||
ErrInvalidIdentifierType = "invalid identifier type: %s"
|
ErrInvalidIdentifierType = "invalid identifier type: %s"
|
||||||
ErrNoPasswordHash = "no password hash found"
|
ErrNoPasswordHash = "no password hash found"
|
||||||
ErrFailedToGenerateUserID = "failed to generate user_id: %w"
|
ErrFailedToGenerateUserID = "failed to generate user_id: %w"
|
||||||
|
|
@ -21,18 +23,26 @@ const (
|
||||||
ErrFailedToGetRole = "failed to get role: %w"
|
ErrFailedToGetRole = "failed to get role: %w"
|
||||||
ErrFailedToGetType = "failed to get type: %w"
|
ErrFailedToGetType = "failed to get type: %w"
|
||||||
ErrFailedToGetOAuthAccount = "failed to get oauth account: %w"
|
ErrFailedToGetOAuthAccount = "failed to get oauth account: %w"
|
||||||
|
ErrFailedToGetTeam = "failed to get team: %w"
|
||||||
|
ErrFailedToGetMember = "failed to get member: %w"
|
||||||
ErrFailedToCreateUser = "failed to create user: %w"
|
ErrFailedToCreateUser = "failed to create user: %w"
|
||||||
ErrFailedToCreateRole = "failed to create role: %w"
|
ErrFailedToCreateRole = "failed to create role: %w"
|
||||||
ErrFailedToCreateType = "failed to create type: %w"
|
ErrFailedToCreateType = "failed to create type: %w"
|
||||||
ErrFailedToCreateOAuth = "failed to create oauth account: %w"
|
ErrFailedToCreateOAuth = "failed to create oauth account: %w"
|
||||||
|
ErrFailedToCreateTeam = "failed to create team: %w"
|
||||||
|
ErrFailedToCreateMember = "failed to create member: %w"
|
||||||
ErrFailedToUpdateUser = "failed to update user: %w"
|
ErrFailedToUpdateUser = "failed to update user: %w"
|
||||||
ErrFailedToUpdateRole = "failed to update role: %w"
|
ErrFailedToUpdateRole = "failed to update role: %w"
|
||||||
ErrFailedToUpdateType = "failed to update type: %w"
|
ErrFailedToUpdateType = "failed to update type: %w"
|
||||||
ErrFailedToUpdateOAuth = "failed to update oauth account: %w"
|
ErrFailedToUpdateOAuth = "failed to update oauth account: %w"
|
||||||
|
ErrFailedToUpdateTeam = "failed to update team: %w"
|
||||||
|
ErrFailedToUpdateMember = "failed to update member: %w"
|
||||||
ErrFailedToDeleteUser = "failed to delete user: %w"
|
ErrFailedToDeleteUser = "failed to delete user: %w"
|
||||||
ErrFailedToDeleteRole = "failed to delete role: %w"
|
ErrFailedToDeleteRole = "failed to delete role: %w"
|
||||||
ErrFailedToDeleteType = "failed to delete type: %w"
|
ErrFailedToDeleteType = "failed to delete type: %w"
|
||||||
ErrFailedToDeleteOAuth = "failed to delete oauth account: %w"
|
ErrFailedToDeleteOAuth = "failed to delete oauth account: %w"
|
||||||
|
ErrFailedToDeleteTeam = "failed to delete team: %w"
|
||||||
|
ErrFailedToDeleteMember = "failed to delete member: %w"
|
||||||
|
|
||||||
// MFA related errors
|
// MFA related errors
|
||||||
ErrMFANotEnabled = "MFA is not enabled for this user"
|
ErrMFANotEnabled = "MFA is not enabled for this user"
|
||||||
|
|
@ -116,6 +126,40 @@ var (
|
||||||
"password_policy", "features", "limits", "created_at", "updated_at",
|
"password_policy", "features", "limits", "created_at", "updated_at",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultTeamFields contains basic team fields
|
||||||
|
DefaultTeamFields = []interface{}{
|
||||||
|
"id", "team_id", "name", "display_name", "description", "website", "logo",
|
||||||
|
"owner_id", "status", "type_id", "type", "is_verified", "verified_at",
|
||||||
|
"created_at", "updated_at",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTeamDetailFields contains all team fields including contact info and metadata
|
||||||
|
DefaultTeamDetailFields = []interface{}{
|
||||||
|
"id", "team_id", "name", "display_name", "description", "website", "logo",
|
||||||
|
"owner_id", "contact_email", "contact_phone", "is_verified", "verified_at", "verified_by",
|
||||||
|
"team_code", "team_code_type", "status", "type_id", "type", "address", "street_address",
|
||||||
|
"city", "state_province", "postal_code", "country", "country_name", "region", "zoneinfo",
|
||||||
|
"settings", "metadata", "created_at", "updated_at",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultMemberFields contains basic member fields
|
||||||
|
DefaultMemberFields = []interface{}{
|
||||||
|
"id", "team_id", "user_id", "member_type", "role_id", "status",
|
||||||
|
"invited_by", "invited_at", "joined_at", "last_active_at", "login_count",
|
||||||
|
"created_at", "updated_at",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultMemberDetailFields contains all member fields including robot config and permissions
|
||||||
|
DefaultMemberDetailFields = []interface{}{
|
||||||
|
"id", "team_id", "user_id", "member_type", "role_id", "status",
|
||||||
|
"robot_name", "robot_description", "robot_avatar", "robot_config", "agents", "tools",
|
||||||
|
"mcp_servers", "data_access_permissions", "system_prompt", "is_active_robot",
|
||||||
|
"schedule_config", "random_activity", "activity_frequency", "last_robot_activity",
|
||||||
|
"robot_status", "invited_by", "invited_at", "joined_at", "invitation_token",
|
||||||
|
"invitation_expires_at", "permissions", "restrictions", "last_active_at",
|
||||||
|
"login_count", "notes", "metadata", "created_at", "updated_at",
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultMFAOptions contains default MFA configuration
|
// DefaultMFAOptions contains default MFA configuration
|
||||||
DefaultMFAOptions = &types.MFAOptions{
|
DefaultMFAOptions = &types.MFAOptions{
|
||||||
Issuer: "Yao App Engine",
|
Issuer: "Yao App Engine",
|
||||||
|
|
@ -135,6 +179,8 @@ type DefaultUser struct {
|
||||||
roleModel string
|
roleModel string
|
||||||
typeModel string
|
typeModel string
|
||||||
oauthAccountModel string
|
oauthAccountModel string
|
||||||
|
teamModel string
|
||||||
|
memberModel string
|
||||||
cache store.Store
|
cache store.Store
|
||||||
|
|
||||||
// ID Generation Configuration
|
// ID Generation Configuration
|
||||||
|
|
@ -159,6 +205,14 @@ type DefaultUser struct {
|
||||||
typeFields []interface{} // configurable
|
typeFields []interface{} // configurable
|
||||||
typeDetailFields []interface{} // configurable
|
typeDetailFields []interface{} // configurable
|
||||||
|
|
||||||
|
// Team Field lists
|
||||||
|
teamFields []interface{} // configurable
|
||||||
|
teamDetailFields []interface{} // configurable
|
||||||
|
|
||||||
|
// Member Field lists
|
||||||
|
memberFields []interface{} // configurable
|
||||||
|
memberDetailFields []interface{} // configurable
|
||||||
|
|
||||||
// MFA Configuration
|
// MFA Configuration
|
||||||
mfaOptions *types.MFAOptions // configurable MFA settings
|
mfaOptions *types.MFAOptions // configurable MFA settings
|
||||||
}
|
}
|
||||||
|
|
@ -180,6 +234,8 @@ type DefaultUserOptions struct {
|
||||||
RoleModel string // bind to a specific role model
|
RoleModel string // bind to a specific role model
|
||||||
TypeModel string // bind to a specific type model
|
TypeModel string // bind to a specific type model
|
||||||
OAuthAccountModel string // bind to a specific oauth account model
|
OAuthAccountModel string // bind to a specific oauth account model
|
||||||
|
TeamModel string // bind to a specific team model
|
||||||
|
MemberModel string // bind to a specific member model
|
||||||
Cache store.Store
|
Cache store.Store
|
||||||
|
|
||||||
// ID Generation Strategy
|
// ID Generation Strategy
|
||||||
|
|
@ -203,6 +259,14 @@ type DefaultUserOptions struct {
|
||||||
TypeFields []interface{} // basic type fields
|
TypeFields []interface{} // basic type fields
|
||||||
TypeDetailFields []interface{} // detailed type fields including configuration and metadata
|
TypeDetailFields []interface{} // detailed type fields including configuration and metadata
|
||||||
|
|
||||||
|
// Team field lists (use defaults if not specified)
|
||||||
|
TeamFields []interface{} // basic team fields
|
||||||
|
TeamDetailFields []interface{} // detailed team fields including contact info and metadata
|
||||||
|
|
||||||
|
// Member field lists (use defaults if not specified)
|
||||||
|
MemberFields []interface{} // basic member fields
|
||||||
|
MemberDetailFields []interface{} // detailed member fields including robot config and permissions
|
||||||
|
|
||||||
// MFA configuration (use defaults if not specified)
|
// MFA configuration (use defaults if not specified)
|
||||||
MFAOptions *types.MFAOptions // MFA settings
|
MFAOptions *types.MFAOptions // MFA settings
|
||||||
}
|
}
|
||||||
|
|
@ -230,6 +294,16 @@ func NewDefaultUser(options *DefaultUserOptions) *DefaultUser {
|
||||||
oauthAccountModel = "__yao.user.oauth_account"
|
oauthAccountModel = "__yao.user.oauth_account"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
teamModel := options.TeamModel
|
||||||
|
if teamModel == "" {
|
||||||
|
teamModel = "__yao.team"
|
||||||
|
}
|
||||||
|
|
||||||
|
memberModel := options.MemberModel
|
||||||
|
if memberModel == "" {
|
||||||
|
memberModel = "__yao.member"
|
||||||
|
}
|
||||||
|
|
||||||
// Set ID generation strategy with defaults
|
// Set ID generation strategy with defaults
|
||||||
idStrategy := options.IDStrategy
|
idStrategy := options.IDStrategy
|
||||||
if idStrategy == "" {
|
if idStrategy == "" {
|
||||||
|
|
@ -283,6 +357,28 @@ func NewDefaultUser(options *DefaultUserOptions) *DefaultUser {
|
||||||
typeDetailFields = DefaultTypeDetailFields
|
typeDetailFields = DefaultTypeDetailFields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set team field lists with defaults if not specified
|
||||||
|
teamFields := options.TeamFields
|
||||||
|
if teamFields == nil {
|
||||||
|
teamFields = DefaultTeamFields
|
||||||
|
}
|
||||||
|
|
||||||
|
teamDetailFields := options.TeamDetailFields
|
||||||
|
if teamDetailFields == nil {
|
||||||
|
teamDetailFields = DefaultTeamDetailFields
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set member field lists with defaults if not specified
|
||||||
|
memberFields := options.MemberFields
|
||||||
|
if memberFields == nil {
|
||||||
|
memberFields = DefaultMemberFields
|
||||||
|
}
|
||||||
|
|
||||||
|
memberDetailFields := options.MemberDetailFields
|
||||||
|
if memberDetailFields == nil {
|
||||||
|
memberDetailFields = DefaultMemberDetailFields
|
||||||
|
}
|
||||||
|
|
||||||
// Set MFA options with defaults if not specified
|
// Set MFA options with defaults if not specified
|
||||||
mfaOptions := options.MFAOptions
|
mfaOptions := options.MFAOptions
|
||||||
if mfaOptions == nil {
|
if mfaOptions == nil {
|
||||||
|
|
@ -295,6 +391,8 @@ func NewDefaultUser(options *DefaultUserOptions) *DefaultUser {
|
||||||
roleModel: roleModel,
|
roleModel: roleModel,
|
||||||
typeModel: typeModel,
|
typeModel: typeModel,
|
||||||
oauthAccountModel: oauthAccountModel,
|
oauthAccountModel: oauthAccountModel,
|
||||||
|
teamModel: teamModel,
|
||||||
|
memberModel: memberModel,
|
||||||
cache: options.Cache,
|
cache: options.Cache,
|
||||||
idStrategy: idStrategy,
|
idStrategy: idStrategy,
|
||||||
idPrefix: idPrefix,
|
idPrefix: idPrefix,
|
||||||
|
|
@ -315,6 +413,14 @@ func NewDefaultUser(options *DefaultUserOptions) *DefaultUser {
|
||||||
typeFields: typeFields,
|
typeFields: typeFields,
|
||||||
typeDetailFields: typeDetailFields,
|
typeDetailFields: typeDetailFields,
|
||||||
|
|
||||||
|
// Team field lists
|
||||||
|
teamFields: teamFields,
|
||||||
|
teamDetailFields: teamDetailFields,
|
||||||
|
|
||||||
|
// Member field lists
|
||||||
|
memberFields: memberFields,
|
||||||
|
memberDetailFields: memberDetailFields,
|
||||||
|
|
||||||
// MFA Configuration
|
// MFA Configuration
|
||||||
mfaOptions: mfaOptions,
|
mfaOptions: mfaOptions,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
532
openapi/oauth/providers/user/member.go
Normal file
532
openapi/oauth/providers/user/member.go
Normal file
|
|
@ -0,0 +1,532 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/model"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Member Resource
|
||||||
|
|
||||||
|
// GetMember retrieves member information by team_id and user_id
|
||||||
|
func (u *DefaultUser) GetMember(ctx context.Context, teamID string, userID string) (maps.MapStrAny, error) {
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(model.QueryParam{
|
||||||
|
Select: u.memberFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
{Column: "user_id", Value: userID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(members) == 0 {
|
||||||
|
return nil, fmt.Errorf(ErrMemberNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return members[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMemberDetail retrieves detailed member information
|
||||||
|
func (u *DefaultUser) GetMemberDetail(ctx context.Context, teamID string, userID string) (maps.MapStrAny, error) {
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(model.QueryParam{
|
||||||
|
Select: u.memberDetailFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
{Column: "user_id", Value: userID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(members) == 0 {
|
||||||
|
return nil, fmt.Errorf(ErrMemberNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return members[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMemberByID retrieves member information by internal ID
|
||||||
|
func (u *DefaultUser) GetMemberByID(ctx context.Context, memberID int64) (maps.MapStrAny, error) {
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(model.QueryParam{
|
||||||
|
Select: u.memberFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "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
|
||||||
|
func (u *DefaultUser) MemberExists(ctx context.Context, teamID string, userID 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: "user_id", Value: userID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(members) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMember creates a new team member (user type)
|
||||||
|
func (u *DefaultUser) CreateMember(ctx context.Context, memberData maps.MapStrAny) (int64, error) {
|
||||||
|
// Validate required fields for user members
|
||||||
|
if _, exists := memberData["team_id"]; !exists {
|
||||||
|
return 0, fmt.Errorf("team_id is required in memberData")
|
||||||
|
}
|
||||||
|
if _, exists := memberData["role_id"]; !exists {
|
||||||
|
return 0, fmt.Errorf("role_id is required in memberData")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default values if not provided
|
||||||
|
if _, exists := memberData["member_type"]; !exists {
|
||||||
|
memberData["member_type"] = "user"
|
||||||
|
}
|
||||||
|
if _, exists := memberData["status"]; !exists {
|
||||||
|
memberData["status"] = "pending"
|
||||||
|
}
|
||||||
|
|
||||||
|
// For user members, user_id is required
|
||||||
|
memberType := memberData["member_type"].(string)
|
||||||
|
if memberType == "user" {
|
||||||
|
if _, exists := memberData["user_id"]; !exists {
|
||||||
|
return 0, fmt.Errorf("user_id is required for user members")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
id, err := m.Create(memberData)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf(ErrFailedToCreateMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRobotMember creates a new robot member
|
||||||
|
func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (int64, error) {
|
||||||
|
// Validate required fields for robot members
|
||||||
|
if _, exists := robotData["robot_name"]; !exists {
|
||||||
|
return 0, fmt.Errorf("robot_name is required for robot members")
|
||||||
|
}
|
||||||
|
if _, exists := robotData["role_id"]; !exists {
|
||||||
|
return 0, fmt.Errorf("role_id is required for robot members")
|
||||||
|
}
|
||||||
|
|
||||||
|
memberData := maps.MapStrAny{
|
||||||
|
"team_id": teamID,
|
||||||
|
"member_type": "robot",
|
||||||
|
"status": "active", // Robots are typically active by default
|
||||||
|
"user_id": nil, // Robots don't have user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy robot-specific fields
|
||||||
|
robotFields := []string{
|
||||||
|
"role_id", "robot_name", "robot_description", "robot_avatar",
|
||||||
|
"robot_config", "agents", "tools", "mcp_servers", "data_access_permissions",
|
||||||
|
"system_prompt", "is_active_robot", "schedule_config", "random_activity",
|
||||||
|
"activity_frequency", "robot_status",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range robotFields {
|
||||||
|
if value, exists := robotData[field]; exists {
|
||||||
|
memberData[field] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default robot status if not provided
|
||||||
|
if _, exists := memberData["robot_status"]; !exists {
|
||||||
|
memberData["robot_status"] = "idle"
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.CreateMember(ctx, memberData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// Check if member already exists
|
||||||
|
exists, err := u.MemberExists(ctx, teamID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to check member existence: %w", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return 0, fmt.Errorf("user is already a member of this team")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate invitation token
|
||||||
|
token, err := generateRandomPassword(32) // Use existing password generation for token
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to generate invitation token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
memberData := maps.MapStrAny{
|
||||||
|
"team_id": teamID,
|
||||||
|
"user_id": userID,
|
||||||
|
"member_type": "user",
|
||||||
|
"role_id": roleID,
|
||||||
|
"status": "pending",
|
||||||
|
"invited_by": invitedBy,
|
||||||
|
"invited_at": time.Now(),
|
||||||
|
"invitation_token": token,
|
||||||
|
"invitation_expires_at": time.Now().Add(7 * 24 * time.Hour), // 7 days expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.CreateMember(ctx, memberData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcceptInvitation accepts a team invitation
|
||||||
|
func (u *DefaultUser) AcceptInvitation(ctx context.Context, invitationToken string) error {
|
||||||
|
// Find member by invitation token
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(model.QueryParam{
|
||||||
|
Select: []interface{}{"id", "team_id", "user_id", "status", "invitation_expires_at"},
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "invitation_token", Value: invitationToken},
|
||||||
|
{Column: "status", Value: "pending"},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(members) == 0 {
|
||||||
|
return fmt.Errorf("invitation not found or already accepted")
|
||||||
|
}
|
||||||
|
|
||||||
|
member := members[0]
|
||||||
|
|
||||||
|
// Check if invitation has expired
|
||||||
|
if expiresAt, ok := member["invitation_expires_at"]; ok {
|
||||||
|
if expiryTime, ok := expiresAt.(time.Time); ok {
|
||||||
|
if time.Now().After(expiryTime) {
|
||||||
|
return fmt.Errorf("invitation has expired")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update member status to active
|
||||||
|
memberID := member["id"].(int64)
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"status": "active",
|
||||||
|
"joined_at": time.Now(),
|
||||||
|
"invitation_token": nil, // Clear the token
|
||||||
|
}
|
||||||
|
|
||||||
|
affected, err := m.UpdateWhere(model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "id", Value: memberID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
}, updateData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(ErrFailedToUpdateMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if affected == 0 {
|
||||||
|
return fmt.Errorf(ErrMemberNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMember updates an existing member
|
||||||
|
func (u *DefaultUser) UpdateMember(ctx context.Context, teamID string, userID string, memberData maps.MapStrAny) error {
|
||||||
|
// Remove sensitive fields that should not be updated directly
|
||||||
|
sensitiveFields := []string{"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: "team_id", Value: teamID},
|
||||||
|
{Column: "user_id", Value: userID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
}, memberData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(ErrFailedToUpdateMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if affected == 0 {
|
||||||
|
return fmt.Errorf(ErrMemberNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMemberByID updates a member by internal ID
|
||||||
|
func (u *DefaultUser) UpdateMemberByID(ctx context.Context, memberID int64, memberData maps.MapStrAny) error {
|
||||||
|
// Remove sensitive fields that should not be updated directly
|
||||||
|
sensitiveFields := []string{"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: "id", Value: memberID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
}, memberData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(ErrFailedToUpdateMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if affected == 0 {
|
||||||
|
return fmt.Errorf(ErrMemberNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveMember removes a member from a team (soft delete)
|
||||||
|
func (u *DefaultUser) RemoveMember(ctx context.Context, teamID string, userID string) error {
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
affected, err := m.DeleteWhere(model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
{Column: "user_id", Value: userID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(ErrFailedToDeleteMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if affected == 0 {
|
||||||
|
return fmt.Errorf(ErrMemberNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeamMembers retrieves all members of a team
|
||||||
|
func (u *DefaultUser) GetTeamMembers(ctx context.Context, teamID string) ([]maps.MapStr, error) {
|
||||||
|
param := model.QueryParam{
|
||||||
|
Select: u.memberFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
},
|
||||||
|
Orders: []model.QueryOrder{
|
||||||
|
{Column: "joined_at", Option: "desc"},
|
||||||
|
{Column: "invited_at", Option: "desc"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(param)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return members, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserTeams retrieves all teams a user is a member of
|
||||||
|
func (u *DefaultUser) GetUserTeams(ctx context.Context, userID string) ([]maps.MapStr, error) {
|
||||||
|
param := model.QueryParam{
|
||||||
|
Select: u.memberFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "user_id", Value: userID},
|
||||||
|
},
|
||||||
|
Orders: []model.QueryOrder{
|
||||||
|
{Column: "joined_at", Option: "desc"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(param)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return members, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeamMembersByStatus retrieves team members by status
|
||||||
|
func (u *DefaultUser) GetTeamMembersByStatus(ctx context.Context, teamID string, status string) ([]maps.MapStr, error) {
|
||||||
|
param := model.QueryParam{
|
||||||
|
Select: u.memberFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
{Column: "status", Value: status},
|
||||||
|
},
|
||||||
|
Orders: []model.QueryOrder{
|
||||||
|
{Column: "invited_at", Option: "desc"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(param)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return members, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeamRobotMembers retrieves all robot members of a team
|
||||||
|
func (u *DefaultUser) GetTeamRobotMembers(ctx context.Context, teamID string) ([]maps.MapStr, error) {
|
||||||
|
param := model.QueryParam{
|
||||||
|
Select: u.memberDetailFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
{Column: "member_type", Value: "robot"},
|
||||||
|
},
|
||||||
|
Orders: []model.QueryOrder{
|
||||||
|
{Column: "robot_name", Option: "asc"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(param)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return members, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveRobotMembers retrieves all active robot members across all teams
|
||||||
|
func (u *DefaultUser) GetActiveRobotMembers(ctx context.Context) ([]maps.MapStr, error) {
|
||||||
|
param := model.QueryParam{
|
||||||
|
Select: u.memberDetailFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "member_type", Value: "robot"},
|
||||||
|
{Column: "is_active_robot", Value: true},
|
||||||
|
{Column: "status", Value: "active"},
|
||||||
|
},
|
||||||
|
Orders: []model.QueryOrder{
|
||||||
|
{Column: "last_robot_activity", Option: "asc"}, // Oldest activity first
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
members, err := m.Get(param)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return members, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMemberRole updates a member's role
|
||||||
|
func (u *DefaultUser) UpdateMemberRole(ctx context.Context, teamID string, userID string, roleID string) error {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"role_id": roleID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.UpdateMember(ctx, teamID, userID, updateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMemberStatus updates a member's status
|
||||||
|
func (u *DefaultUser) UpdateMemberStatus(ctx context.Context, teamID string, userID string, status string) error {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.UpdateMember(ctx, teamID, userID, updateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMemberLastActivity updates a member's last activity time
|
||||||
|
func (u *DefaultUser) UpdateMemberLastActivity(ctx context.Context, teamID string, userID string) error {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"last_active_at": time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also increment login count
|
||||||
|
member, err := u.GetMember(ctx, teamID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
loginCount := 0
|
||||||
|
if count := member["login_count"]; count != nil {
|
||||||
|
switch v := count.(type) {
|
||||||
|
case int:
|
||||||
|
loginCount = v
|
||||||
|
case int64:
|
||||||
|
loginCount = int(v)
|
||||||
|
case int32:
|
||||||
|
loginCount = int(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateData["login_count"] = loginCount + 1
|
||||||
|
|
||||||
|
return u.UpdateMember(ctx, teamID, userID, updateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRobotActivity updates robot member's last activity and status
|
||||||
|
func (u *DefaultUser) UpdateRobotActivity(ctx context.Context, memberID int64, robotStatus string) error {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"last_robot_activity": time.Now(),
|
||||||
|
"robot_status": robotStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.UpdateMemberByID(ctx, memberID, updateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginateMembers retrieves paginated list of members
|
||||||
|
func (u *DefaultUser) PaginateMembers(ctx context.Context, param model.QueryParam, page int, pagesize int) (maps.MapStr, error) {
|
||||||
|
// Set default select fields if not provided
|
||||||
|
if param.Select == nil {
|
||||||
|
param.Select = u.memberFields
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.memberModel)
|
||||||
|
result, err := m.Paginate(param, page, pagesize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
748
openapi/oauth/providers/user/member_test.go
Normal file
748
openapi/oauth/providers/user/member_test.go
Normal file
|
|
@ -0,0 +1,748 @@
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/gou/model"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMemberBasicOperations(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": "Test Team " + testUUID,
|
||||||
|
"display_name": "Test Display " + testUUID,
|
||||||
|
"description": "A test team for member 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 memberID int64
|
||||||
|
|
||||||
|
// Test CreateMember
|
||||||
|
t.Run("CreateMember", func(t *testing.T) {
|
||||||
|
memberData := maps.MapStrAny{
|
||||||
|
"team_id": teamID,
|
||||||
|
"user_id": memberUser,
|
||||||
|
"member_type": "user",
|
||||||
|
"role_id": "user",
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := testProvider.CreateMember(ctx, memberData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, id, int64(0))
|
||||||
|
memberID = id
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetMember
|
||||||
|
t.Run("GetMember", func(t *testing.T) {
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member)
|
||||||
|
assert.Equal(t, teamID, member["team_id"])
|
||||||
|
assert.Equal(t, memberUser, member["user_id"])
|
||||||
|
assert.Equal(t, "user", member["member_type"])
|
||||||
|
assert.Equal(t, "user", member["role_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetMemberDetail
|
||||||
|
t.Run("GetMemberDetail", func(t *testing.T) {
|
||||||
|
member, err := testProvider.GetMemberDetail(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member)
|
||||||
|
assert.Equal(t, teamID, member["team_id"])
|
||||||
|
assert.Equal(t, memberUser, member["user_id"])
|
||||||
|
// Should contain more detailed fields
|
||||||
|
assert.Contains(t, member, "created_at")
|
||||||
|
assert.Contains(t, member, "updated_at")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetMemberByID
|
||||||
|
t.Run("GetMemberByID", func(t *testing.T) {
|
||||||
|
member, err := testProvider.GetMemberByID(ctx, memberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member)
|
||||||
|
assert.Equal(t, teamID, member["team_id"])
|
||||||
|
assert.Equal(t, memberUser, member["user_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test MemberExists
|
||||||
|
t.Run("MemberExists", func(t *testing.T) {
|
||||||
|
exists, err := testProvider.MemberExists(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, exists)
|
||||||
|
|
||||||
|
// Test with non-existent member
|
||||||
|
exists, err = testProvider.MemberExists(ctx, teamID, "non-existent-user")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, exists)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMember
|
||||||
|
t.Run("UpdateMember", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"role_id": "admin",
|
||||||
|
"notes": "Promoted to admin",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := testProvider.UpdateMember(ctx, teamID, memberUser, updateData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "admin", member["role_id"])
|
||||||
|
|
||||||
|
// Test updating sensitive fields (should be ignored)
|
||||||
|
sensitiveData := maps.MapStrAny{
|
||||||
|
"id": 999,
|
||||||
|
"team_id": "new-team",
|
||||||
|
"user_id": "new-user",
|
||||||
|
"invitation_token": "fake-token",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = testProvider.UpdateMember(ctx, teamID, memberUser, sensitiveData)
|
||||||
|
assert.NoError(t, err) // Should not error, just ignore sensitive fields
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMemberByID
|
||||||
|
t.Run("UpdateMemberByID", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"status": "inactive",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := testProvider.UpdateMemberByID(ctx, memberID, updateData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
member, err := testProvider.GetMemberByID(ctx, memberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "inactive", member["status"])
|
||||||
|
|
||||||
|
// Change back to active for other tests
|
||||||
|
err = testProvider.UpdateMemberByID(ctx, memberID, maps.MapStrAny{"status": "active"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMemberRole
|
||||||
|
t.Run("UpdateMemberRole", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateMemberRole(ctx, teamID, memberUser, "moderator")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify role was updated
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "moderator", member["role_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMemberStatus
|
||||||
|
t.Run("UpdateMemberStatus", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateMemberStatus(ctx, teamID, memberUser, "suspended")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify status was updated
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "suspended", member["status"])
|
||||||
|
|
||||||
|
// Change back to active
|
||||||
|
err = testProvider.UpdateMemberStatus(ctx, teamID, memberUser, "active")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMemberLastActivity
|
||||||
|
t.Run("UpdateMemberLastActivity", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateMemberLastActivity(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify last_active_at was updated and login_count incremented
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member["last_active_at"])
|
||||||
|
// login_count should be at least 1 (handle different integer types)
|
||||||
|
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 RemoveMember (at the end)
|
||||||
|
t.Run("RemoveMember", func(t *testing.T) {
|
||||||
|
err := testProvider.RemoveMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify member was removed
|
||||||
|
_, err = testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "member not found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemberInvitationFlow(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)
|
||||||
|
inviteeUser := createTestUser(ctx, t, "invitee"+testUUID)
|
||||||
|
|
||||||
|
// Create test team
|
||||||
|
teamMap := maps.MapStrAny{
|
||||||
|
"name": "Invitation Test Team " + testUUID,
|
||||||
|
"display_name": "Invitation Test " + testUUID,
|
||||||
|
"description": "A test team for invitation 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 invitationToken string
|
||||||
|
|
||||||
|
// Test AddMember (invitation-based)
|
||||||
|
t.Run("AddMember", func(t *testing.T) {
|
||||||
|
memberID, err := testProvider.AddMember(ctx, teamID, inviteeUser, "user", ownerUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, memberID, int64(0))
|
||||||
|
|
||||||
|
// Verify member was created with pending status
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, inviteeUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "pending", member["status"])
|
||||||
|
assert.Equal(t, ownerUser, member["invited_by"])
|
||||||
|
|
||||||
|
// Get invitation token for acceptance test
|
||||||
|
memberDetail, err := testProvider.GetMemberDetail(ctx, teamID, inviteeUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
invitationToken = memberDetail["invitation_token"].(string)
|
||||||
|
assert.NotEmpty(t, invitationToken)
|
||||||
|
|
||||||
|
// Verify invitation expiry is set
|
||||||
|
assert.NotNil(t, memberDetail["invitation_expires_at"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test duplicate invitation prevention
|
||||||
|
t.Run("AddMember_DuplicatePrevention", func(t *testing.T) {
|
||||||
|
_, err := testProvider.AddMember(ctx, teamID, inviteeUser, "user", ownerUser)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "already a member")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test AcceptInvitation
|
||||||
|
t.Run("AcceptInvitation", func(t *testing.T) {
|
||||||
|
err := testProvider.AcceptInvitation(ctx, invitationToken)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify member status changed to active
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, inviteeUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "active", member["status"])
|
||||||
|
assert.NotNil(t, member["joined_at"])
|
||||||
|
|
||||||
|
// Verify invitation token was cleared
|
||||||
|
memberDetail, err := testProvider.GetMemberDetail(ctx, teamID, inviteeUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, memberDetail["invitation_token"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test AcceptInvitation with invalid token
|
||||||
|
t.Run("AcceptInvitation_InvalidToken", func(t *testing.T) {
|
||||||
|
err := testProvider.AcceptInvitation(ctx, "invalid-token")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "invitation not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test AcceptInvitation with already accepted token
|
||||||
|
t.Run("AcceptInvitation_AlreadyAccepted", func(t *testing.T) {
|
||||||
|
err := testProvider.AcceptInvitation(ctx, invitationToken)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "invitation not found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRobotMemberOperations(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 user (team owner)
|
||||||
|
ownerUser := createTestUser(ctx, t, "owner"+testUUID)
|
||||||
|
|
||||||
|
// Create test team
|
||||||
|
teamMap := maps.MapStrAny{
|
||||||
|
"name": "Robot Test Team " + testUUID,
|
||||||
|
"display_name": "Robot Test " + testUUID,
|
||||||
|
"description": "A test team for robot 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 robotMemberID int64
|
||||||
|
|
||||||
|
// Test CreateRobotMember
|
||||||
|
t.Run("CreateRobotMember", func(t *testing.T) {
|
||||||
|
robotData := maps.MapStrAny{
|
||||||
|
"robot_name": "TestBot" + testUUID,
|
||||||
|
"robot_description": "A test robot for unit testing",
|
||||||
|
"robot_avatar": "https://example.com/robot.png",
|
||||||
|
"role_id": "bot",
|
||||||
|
"is_active_robot": true,
|
||||||
|
"robot_status": "idle",
|
||||||
|
"system_prompt": "You are a helpful test robot",
|
||||||
|
"robot_config": map[string]interface{}{
|
||||||
|
"max_tokens": 1000,
|
||||||
|
"model": "gpt-4",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := testProvider.CreateRobotMember(ctx, teamID, robotData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, id, int64(0))
|
||||||
|
robotMemberID = id
|
||||||
|
|
||||||
|
// Verify robot member was created
|
||||||
|
member, err := testProvider.GetMemberByID(ctx, robotMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "robot", member["member_type"])
|
||||||
|
assert.Equal(t, "active", member["status"]) // Robots are active by default
|
||||||
|
assert.Nil(t, member["user_id"]) // Robots don't have user_id
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetTeamRobotMembers
|
||||||
|
t.Run("GetTeamRobotMembers", func(t *testing.T) {
|
||||||
|
robots, err := testProvider.GetTeamRobotMembers(ctx, teamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, robots, 1)
|
||||||
|
assert.Equal(t, "robot", robots[0]["member_type"])
|
||||||
|
assert.Equal(t, "TestBot"+testUUID, robots[0]["robot_name"])
|
||||||
|
assert.Equal(t, "A test robot for unit testing", robots[0]["robot_description"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateRobotActivity
|
||||||
|
t.Run("UpdateRobotActivity", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateRobotActivity(ctx, robotMemberID, "working")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
robotTeamID := member["team_id"].(string)
|
||||||
|
|
||||||
|
// Get robot members to verify status (robot members don't have user_id)
|
||||||
|
robots, err := testProvider.GetTeamRobotMembers(ctx, robotTeamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, robots, 1)
|
||||||
|
robot := robots[0]
|
||||||
|
assert.Equal(t, "working", robot["robot_status"])
|
||||||
|
assert.NotNil(t, robot["last_robot_activity"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetActiveRobotMembers
|
||||||
|
t.Run("GetActiveRobotMembers", func(t *testing.T) {
|
||||||
|
// First make sure our robot is active
|
||||||
|
err := testProvider.UpdateMemberByID(ctx, robotMemberID, maps.MapStrAny{
|
||||||
|
"is_active_robot": true,
|
||||||
|
"status": "active",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// If update fails, log the error and skip the test
|
||||||
|
t.Logf("Failed to update robot member: %v", err)
|
||||||
|
t.Skip("Robot member update failed, skipping GetActiveRobotMembers test")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
robots, err := testProvider.GetActiveRobotMembers(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, len(robots) >= 1) // At least our test robot
|
||||||
|
|
||||||
|
// Find our test robot in the results
|
||||||
|
found := false
|
||||||
|
for _, robot := range robots {
|
||||||
|
if robot["robot_name"] == "TestBot"+testUUID {
|
||||||
|
found = true
|
||||||
|
assert.Equal(t, "robot", robot["member_type"])
|
||||||
|
// Handle different boolean types from database
|
||||||
|
isActive := robot["is_active_robot"]
|
||||||
|
assert.True(t, isActive == true || isActive == int64(1) || isActive == 1, "Robot should be active")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "Test robot should be found in active robots")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test robot member validation
|
||||||
|
t.Run("CreateRobotMember_ValidationErrors", func(t *testing.T) {
|
||||||
|
// Missing robot_name
|
||||||
|
robotData := maps.MapStrAny{
|
||||||
|
"role_id": "bot",
|
||||||
|
}
|
||||||
|
_, err := testProvider.CreateRobotMember(ctx, teamID, robotData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "robot_name is required")
|
||||||
|
|
||||||
|
// Missing role_id
|
||||||
|
robotData = maps.MapStrAny{
|
||||||
|
"robot_name": "TestBot2",
|
||||||
|
}
|
||||||
|
_, err = testProvider.CreateRobotMember(ctx, teamID, robotData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "role_id is required")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemberQueryOperations(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)
|
||||||
|
member1User := createTestUser(ctx, t, "member1"+testUUID)
|
||||||
|
member2User := createTestUser(ctx, t, "member2"+testUUID)
|
||||||
|
|
||||||
|
// Create test teams
|
||||||
|
team1Map := maps.MapStrAny{
|
||||||
|
"name": "Query Test Team 1 " + testUUID,
|
||||||
|
"display_name": "Query Test 1 " + testUUID,
|
||||||
|
"description": "First test team for query testing",
|
||||||
|
"owner_id": ownerUser,
|
||||||
|
"status": "active",
|
||||||
|
"type": "corporation",
|
||||||
|
"type_id": "business",
|
||||||
|
"metadata": map[string]interface{}{"test": true},
|
||||||
|
}
|
||||||
|
|
||||||
|
team1ID, err := testProvider.CreateTeam(ctx, team1Map)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
team2Map := maps.MapStrAny{
|
||||||
|
"name": "Query Test Team 2 " + testUUID,
|
||||||
|
"display_name": "Query Test 2 " + testUUID,
|
||||||
|
"description": "Second test team for query testing",
|
||||||
|
"owner_id": ownerUser,
|
||||||
|
"status": "active",
|
||||||
|
"type": "corporation",
|
||||||
|
"type_id": "business",
|
||||||
|
"metadata": map[string]interface{}{"test": true},
|
||||||
|
}
|
||||||
|
|
||||||
|
team2ID, err := testProvider.CreateTeam(ctx, team2Map)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Add members to teams
|
||||||
|
_, err = testProvider.CreateMember(ctx, maps.MapStrAny{
|
||||||
|
"team_id": team1ID,
|
||||||
|
"user_id": member1User,
|
||||||
|
"member_type": "user",
|
||||||
|
"role_id": "user",
|
||||||
|
"status": "active",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = testProvider.CreateMember(ctx, maps.MapStrAny{
|
||||||
|
"team_id": team1ID,
|
||||||
|
"user_id": member2User,
|
||||||
|
"member_type": "user",
|
||||||
|
"role_id": "admin",
|
||||||
|
"status": "pending",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = testProvider.CreateMember(ctx, maps.MapStrAny{
|
||||||
|
"team_id": team2ID,
|
||||||
|
"user_id": member1User,
|
||||||
|
"member_type": "user",
|
||||||
|
"role_id": "moderator",
|
||||||
|
"status": "active",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test GetTeamMembers
|
||||||
|
t.Run("GetTeamMembers", func(t *testing.T) {
|
||||||
|
members, err := testProvider.GetTeamMembers(ctx, team1ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, members, 2) // member1 and member2
|
||||||
|
|
||||||
|
// Verify members are ordered by joined_at desc, invited_at desc
|
||||||
|
userIDs := []string{members[0]["user_id"].(string), members[1]["user_id"].(string)}
|
||||||
|
assert.Contains(t, userIDs, member1User)
|
||||||
|
assert.Contains(t, userIDs, member2User)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetUserTeams
|
||||||
|
t.Run("GetUserTeams", func(t *testing.T) {
|
||||||
|
teams, err := testProvider.GetUserTeams(ctx, member1User)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, teams, 2) // member1 is in both teams
|
||||||
|
|
||||||
|
teamIDs := []string{teams[0]["team_id"].(string), teams[1]["team_id"].(string)}
|
||||||
|
assert.Contains(t, teamIDs, team1ID)
|
||||||
|
assert.Contains(t, teamIDs, team2ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetTeamMembersByStatus
|
||||||
|
t.Run("GetTeamMembersByStatus", func(t *testing.T) {
|
||||||
|
// Get active members
|
||||||
|
activeMembers, err := testProvider.GetTeamMembersByStatus(ctx, team1ID, "active")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, activeMembers, 1) // Only member1 is active
|
||||||
|
assert.Equal(t, member1User, activeMembers[0]["user_id"])
|
||||||
|
|
||||||
|
// Get pending members
|
||||||
|
pendingMembers, err := testProvider.GetTeamMembersByStatus(ctx, team1ID, "pending")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, pendingMembers, 1) // Only member2 is pending
|
||||||
|
assert.Equal(t, member2User, pendingMembers[0]["user_id"])
|
||||||
|
|
||||||
|
// Get inactive members (should be empty)
|
||||||
|
inactiveMembers, err := testProvider.GetTeamMembersByStatus(ctx, team1ID, "inactive")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, inactiveMembers, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test PaginateMembers
|
||||||
|
t.Run("PaginateMembers", func(t *testing.T) {
|
||||||
|
param := model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: team1ID},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := testProvider.PaginateMembers(ctx, param, 1, 10)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
// Pagination result may use "data" instead of "items"
|
||||||
|
assert.True(t, result["data"] != nil || result["items"] != nil)
|
||||||
|
assert.Contains(t, result, "total")
|
||||||
|
|
||||||
|
// Total should be 2 (member1 and member2)
|
||||||
|
total := result["total"]
|
||||||
|
assert.True(t, total == 2 || total == int64(2))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemberErrorHandling(t *testing.T) {
|
||||||
|
prepare(t)
|
||||||
|
defer clean()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||||
|
nonExistentTeamID := "non-existent-team-" + testUUID
|
||||||
|
nonExistentUserID := "non-existent-user-" + testUUID
|
||||||
|
nonExistentMemberID := int64(999999)
|
||||||
|
|
||||||
|
t.Run("GetMember_NotFound", func(t *testing.T) {
|
||||||
|
_, err := testProvider.GetMember(ctx, nonExistentTeamID, nonExistentUserID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "member not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetMemberDetail_NotFound", func(t *testing.T) {
|
||||||
|
_, err := testProvider.GetMemberDetail(ctx, nonExistentTeamID, nonExistentUserID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "member not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetMemberByID_NotFound", func(t *testing.T) {
|
||||||
|
_, err := testProvider.GetMemberByID(ctx, nonExistentMemberID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "member not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateMember_NotFound", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{"role_id": "admin"}
|
||||||
|
err := testProvider.UpdateMember(ctx, nonExistentTeamID, nonExistentUserID, updateData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "member not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateMemberByID_NotFound", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{"role_id": "admin"}
|
||||||
|
err := testProvider.UpdateMemberByID(ctx, nonExistentMemberID, updateData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "member not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RemoveMember_NotFound", func(t *testing.T) {
|
||||||
|
err := testProvider.RemoveMember(ctx, nonExistentTeamID, nonExistentUserID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "member not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateMember_MissingRequiredFields", func(t *testing.T) {
|
||||||
|
// Missing team_id
|
||||||
|
memberData := maps.MapStrAny{
|
||||||
|
"user_id": "test-user",
|
||||||
|
"role_id": "user",
|
||||||
|
}
|
||||||
|
_, err := testProvider.CreateMember(ctx, memberData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "team_id is required")
|
||||||
|
|
||||||
|
// Missing role_id
|
||||||
|
memberData = maps.MapStrAny{
|
||||||
|
"team_id": "test-team",
|
||||||
|
"user_id": "test-user",
|
||||||
|
}
|
||||||
|
_, err = testProvider.CreateMember(ctx, memberData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "role_id is required")
|
||||||
|
|
||||||
|
// Missing user_id for user member
|
||||||
|
memberData = maps.MapStrAny{
|
||||||
|
"team_id": "test-team",
|
||||||
|
"role_id": "user",
|
||||||
|
"member_type": "user",
|
||||||
|
}
|
||||||
|
_, err = testProvider.CreateMember(ctx, memberData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "user_id is required for user members")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateMember_EmptyData", func(t *testing.T) {
|
||||||
|
// Create a test member first
|
||||||
|
ownerUser := createTestUser(ctx, t, "owner"+testUUID)
|
||||||
|
memberUser := createTestUser(ctx, t, "member"+testUUID)
|
||||||
|
|
||||||
|
teamMap := maps.MapStrAny{
|
||||||
|
"name": "Error Test Team " + testUUID,
|
||||||
|
"display_name": "Error Test " + testUUID,
|
||||||
|
"description": "A test team for error testing",
|
||||||
|
"owner_id": ownerUser,
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = testProvider.CreateMember(ctx, maps.MapStrAny{
|
||||||
|
"team_id": teamID,
|
||||||
|
"user_id": memberUser,
|
||||||
|
"member_type": "user",
|
||||||
|
"role_id": "user",
|
||||||
|
"status": "active",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test update with empty data (should not error, just do nothing)
|
||||||
|
err = testProvider.UpdateMember(ctx, teamID, memberUser, maps.MapStrAny{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test update with only sensitive fields (should not error, just ignore them)
|
||||||
|
err = testProvider.UpdateMember(ctx, teamID, memberUser, maps.MapStrAny{
|
||||||
|
"id": 999,
|
||||||
|
"team_id": "new-team",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemberInvitationExpiry(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)
|
||||||
|
inviteeUser := createTestUser(ctx, t, "invitee"+testUUID)
|
||||||
|
|
||||||
|
// Create test team
|
||||||
|
teamMap := maps.MapStrAny{
|
||||||
|
"name": "Expiry Test Team " + testUUID,
|
||||||
|
"display_name": "Expiry Test " + testUUID,
|
||||||
|
"description": "A test team for invitation expiry testing",
|
||||||
|
"owner_id": ownerUser,
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Create member with expired invitation
|
||||||
|
memberData := maps.MapStrAny{
|
||||||
|
"team_id": teamID,
|
||||||
|
"user_id": inviteeUser,
|
||||||
|
"member_type": "user",
|
||||||
|
"role_id": "user",
|
||||||
|
"status": "pending",
|
||||||
|
"invited_by": ownerUser,
|
||||||
|
"invited_at": time.Now(),
|
||||||
|
"invitation_token": "expired-token-" + testUUID,
|
||||||
|
"invitation_expires_at": time.Now().Add(-1 * time.Hour), // Expired 1 hour ago
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = testProvider.CreateMember(ctx, memberData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test AcceptInvitation with expired token
|
||||||
|
t.Run("AcceptInvitation_ExpiredToken", func(t *testing.T) {
|
||||||
|
err := testProvider.AcceptInvitation(ctx, "expired-token-"+testUUID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "invitation has expired")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function createTestUser is defined in team_test.go
|
||||||
370
openapi/oauth/providers/user/team.go
Normal file
370
openapi/oauth/providers/user/team.go
Normal file
|
|
@ -0,0 +1,370 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/model"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Team Resource
|
||||||
|
|
||||||
|
// GetTeam retrieves team information by team_id
|
||||||
|
func (u *DefaultUser) GetTeam(ctx context.Context, teamID string) (maps.MapStrAny, error) {
|
||||||
|
m := model.Select(u.teamModel)
|
||||||
|
teams, err := m.Get(model.QueryParam{
|
||||||
|
Select: u.teamFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(teams) == 0 {
|
||||||
|
return nil, fmt.Errorf(ErrTeamNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return teams[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeamDetail retrieves detailed team information by team_id
|
||||||
|
func (u *DefaultUser) GetTeamDetail(ctx context.Context, teamID string) (maps.MapStrAny, error) {
|
||||||
|
m := model.Select(u.teamModel)
|
||||||
|
teams, err := m.Get(model.QueryParam{
|
||||||
|
Select: u.teamDetailFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(teams) == 0 {
|
||||||
|
return nil, fmt.Errorf(ErrTeamNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return teams[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamExists checks if a team exists by team_id (lightweight query)
|
||||||
|
func (u *DefaultUser) TeamExists(ctx context.Context, teamID string) (bool, error) {
|
||||||
|
m := model.Select(u.teamModel)
|
||||||
|
teams, err := m.Get(model.QueryParam{
|
||||||
|
Select: []interface{}{"id"}, // Only select ID for existence check
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
},
|
||||||
|
Limit: 1, // Only need to know if at least one exists
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf(ErrFailedToGetTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(teams) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTeam creates a new team
|
||||||
|
func (u *DefaultUser) CreateTeam(ctx context.Context, teamData maps.MapStrAny) (string, error) {
|
||||||
|
// Generate team_id if not provided
|
||||||
|
if _, exists := teamData["team_id"]; !exists {
|
||||||
|
teamID, err := u.GenerateUserID(ctx, true) // Reuse user ID generation logic for team ID
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate team_id: %w", err)
|
||||||
|
}
|
||||||
|
teamData["team_id"] = teamID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if _, exists := teamData["name"]; !exists {
|
||||||
|
return "", fmt.Errorf("name is required in teamData")
|
||||||
|
}
|
||||||
|
if _, exists := teamData["owner_id"]; !exists {
|
||||||
|
return "", fmt.Errorf("owner_id is required in teamData")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default values if not provided
|
||||||
|
if _, exists := teamData["status"]; !exists {
|
||||||
|
teamData["status"] = "pending"
|
||||||
|
}
|
||||||
|
if _, exists := teamData["is_verified"]; !exists {
|
||||||
|
teamData["is_verified"] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.teamModel)
|
||||||
|
id, err := m.Create(teamData)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf(ErrFailedToCreateTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the team_id as string (preferred approach)
|
||||||
|
if teamID, ok := teamData["team_id"].(string); ok {
|
||||||
|
return teamID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: convert the returned int id to string
|
||||||
|
return fmt.Sprintf("%d", id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTeam updates an existing team
|
||||||
|
func (u *DefaultUser) UpdateTeam(ctx context.Context, teamID string, teamData maps.MapStrAny) error {
|
||||||
|
// Remove sensitive fields that should not be updated directly
|
||||||
|
sensitiveFields := []string{"id", "team_id", "created_at", "verified_at", "verified_by"}
|
||||||
|
for _, field := range sensitiveFields {
|
||||||
|
delete(teamData, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip update if no valid fields remain
|
||||||
|
if len(teamData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.teamModel)
|
||||||
|
affected, err := m.UpdateWhere(model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
},
|
||||||
|
Limit: 1, // Safety: ensure only one record is updated
|
||||||
|
}, teamData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(ErrFailedToUpdateTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if affected == 0 {
|
||||||
|
return fmt.Errorf(ErrTeamNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTeam soft deletes a team
|
||||||
|
func (u *DefaultUser) DeleteTeam(ctx context.Context, teamID string) error {
|
||||||
|
// First check if team exists
|
||||||
|
m := model.Select(u.teamModel)
|
||||||
|
teams, err := m.Get(model.QueryParam{
|
||||||
|
Select: []interface{}{"id", "team_id"},
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(ErrFailedToGetTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(teams) == 0 {
|
||||||
|
return fmt.Errorf(ErrTeamNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed with soft delete
|
||||||
|
affected, err := m.DeleteWhere(model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
},
|
||||||
|
Limit: 1, // Safety: ensure only one record is deleted
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(ErrFailedToDeleteTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if affected == 0 {
|
||||||
|
return fmt.Errorf(ErrTeamNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeams retrieves teams by query parameters
|
||||||
|
func (u *DefaultUser) GetTeams(ctx context.Context, param model.QueryParam) ([]maps.MapStr, error) {
|
||||||
|
// Set default select fields if not provided
|
||||||
|
if param.Select == nil {
|
||||||
|
param.Select = u.teamFields
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.teamModel)
|
||||||
|
teams, err := m.Get(param)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return teams, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginateTeams retrieves paginated list of teams
|
||||||
|
func (u *DefaultUser) PaginateTeams(ctx context.Context, param model.QueryParam, page int, pagesize int) (maps.MapStr, error) {
|
||||||
|
// Set default select fields if not provided
|
||||||
|
if param.Select == nil {
|
||||||
|
param.Select = u.teamFields
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.Select(u.teamModel)
|
||||||
|
result, err := m.Paginate(param, page, pagesize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(ErrFailedToGetTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountTeams returns total count of teams with optional filters
|
||||||
|
func (u *DefaultUser) CountTeams(ctx context.Context, param model.QueryParam) (int64, error) {
|
||||||
|
// Use Paginate with a small page size to get the total count
|
||||||
|
// This is more reliable than manual COUNT(*) queries
|
||||||
|
m := model.Select(u.teamModel)
|
||||||
|
result, err := m.Paginate(param, 1, 1) // Get first page with 1 item to get total
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf(ErrFailedToGetTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract total from pagination result
|
||||||
|
if total, ok := result["total"].(int64); ok {
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different total types returned by Paginate
|
||||||
|
if totalInterface, ok := result["total"]; ok {
|
||||||
|
switch v := totalInterface.(type) {
|
||||||
|
case int:
|
||||||
|
return int64(v), nil
|
||||||
|
case int32:
|
||||||
|
return int64(v), nil
|
||||||
|
case int64:
|
||||||
|
return v, nil
|
||||||
|
case uint:
|
||||||
|
return int64(v), nil
|
||||||
|
case uint32:
|
||||||
|
return int64(v), nil
|
||||||
|
case uint64:
|
||||||
|
return int64(v), nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unexpected total type: %T", totalInterface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("total not found in pagination result")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeamsByOwner retrieves teams owned by a specific user
|
||||||
|
func (u *DefaultUser) GetTeamsByOwner(ctx context.Context, ownerID string) ([]maps.MapStr, error) {
|
||||||
|
param := model.QueryParam{
|
||||||
|
Select: u.teamFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "owner_id", Value: ownerID},
|
||||||
|
},
|
||||||
|
Orders: []model.QueryOrder{
|
||||||
|
{Column: "created_at", Option: "desc"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.GetTeams(ctx, param)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeamsByStatus retrieves teams by status
|
||||||
|
func (u *DefaultUser) GetTeamsByStatus(ctx context.Context, status string) ([]maps.MapStr, error) {
|
||||||
|
param := model.QueryParam{
|
||||||
|
Select: u.teamFields,
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "status", Value: status},
|
||||||
|
},
|
||||||
|
Orders: []model.QueryOrder{
|
||||||
|
{Column: "created_at", Option: "desc"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.GetTeams(ctx, param)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTeamStatus updates team status
|
||||||
|
func (u *DefaultUser) UpdateTeamStatus(ctx context.Context, teamID string, status string) error {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.UpdateTeam(ctx, teamID, updateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyTeam marks a team as verified
|
||||||
|
func (u *DefaultUser) VerifyTeam(ctx context.Context, teamID string, verifiedBy string) error {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"is_verified": true,
|
||||||
|
"verified_by": verifiedBy,
|
||||||
|
"verified_at": time.Now(), // Set current timestamp explicitly
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct model update to bypass sensitive field filtering
|
||||||
|
m := model.Select(u.teamModel)
|
||||||
|
affected, err := m.UpdateWhere(model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
}, updateData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(ErrFailedToUpdateTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if affected == 0 {
|
||||||
|
return fmt.Errorf(ErrTeamNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnverifyTeam removes verification from a team
|
||||||
|
func (u *DefaultUser) UnverifyTeam(ctx context.Context, teamID string) error {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"is_verified": false,
|
||||||
|
"verified_by": nil,
|
||||||
|
"verified_at": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct model update to bypass sensitive field filtering
|
||||||
|
m := model.Select(u.teamModel)
|
||||||
|
affected, err := m.UpdateWhere(model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "team_id", Value: teamID},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
}, updateData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(ErrFailedToUpdateTeam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if affected == 0 {
|
||||||
|
return fmt.Errorf(ErrTeamNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransferTeamOwnership transfers team ownership to another user
|
||||||
|
func (u *DefaultUser) TransferTeamOwnership(ctx context.Context, teamID string, newOwnerID string) error {
|
||||||
|
// First verify the new owner exists
|
||||||
|
exists, err := u.UserExists(ctx, newOwnerID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to verify new owner: %w", err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("new owner user not found: %s", newOwnerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"owner_id": newOwnerID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.UpdateTeam(ctx, teamID, updateData)
|
||||||
|
}
|
||||||
572
openapi/oauth/providers/user/team_test.go
Normal file
572
openapi/oauth/providers/user/team_test.go
Normal file
|
|
@ -0,0 +1,572 @@
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/gou/model"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestTeamData represents test team data structure
|
||||||
|
type TestTeamData struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Website string `json:"website"`
|
||||||
|
OwnerID string `json:"owner_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
TypeID string `json:"type_id"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMemberData represents test member data structure
|
||||||
|
type TestMemberData struct {
|
||||||
|
TeamID string `json:"team_id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
RoleID string `json:"role_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
InvitedBy string `json:"invited_by"`
|
||||||
|
MemberType string `json:"member_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTeamBasicOperations(t *testing.T) {
|
||||||
|
prepare(t)
|
||||||
|
defer clean()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Use UUID to ensure unique identifiers
|
||||||
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8] // 8 char UUID
|
||||||
|
|
||||||
|
// First, create a test user to be the team owner
|
||||||
|
testUser := &TestUserData{
|
||||||
|
PreferredUsername: "teamowner" + testUUID,
|
||||||
|
Email: "teamowner" + testUUID + "@example.com",
|
||||||
|
Password: "TestPass123!",
|
||||||
|
Name: "Team Owner " + testUUID,
|
||||||
|
GivenName: "Team",
|
||||||
|
FamilyName: "Owner",
|
||||||
|
Status: "active",
|
||||||
|
RoleID: "admin",
|
||||||
|
TypeID: "regular",
|
||||||
|
EmailVerified: true,
|
||||||
|
Metadata: map[string]interface{}{"source": "test"},
|
||||||
|
}
|
||||||
|
|
||||||
|
userMap := maps.MapStrAny{
|
||||||
|
"preferred_username": testUser.PreferredUsername,
|
||||||
|
"email": testUser.Email,
|
||||||
|
"password": testUser.Password,
|
||||||
|
"name": testUser.Name,
|
||||||
|
"given_name": testUser.GivenName,
|
||||||
|
"family_name": testUser.FamilyName,
|
||||||
|
"status": testUser.Status,
|
||||||
|
"role_id": testUser.RoleID,
|
||||||
|
"type_id": testUser.TypeID,
|
||||||
|
"email_verified": testUser.EmailVerified,
|
||||||
|
"metadata": testUser.Metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the owner user
|
||||||
|
_, err := testProvider.CreateUser(ctx, userMap)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
ownerUserID := userMap["user_id"].(string)
|
||||||
|
|
||||||
|
// Create test team data dynamically
|
||||||
|
testTeam := &TestTeamData{
|
||||||
|
Name: "Test Team " + testUUID,
|
||||||
|
DisplayName: "Test Display " + testUUID,
|
||||||
|
Description: "A test team for unit testing",
|
||||||
|
Website: "https://test" + testUUID + ".example.com",
|
||||||
|
OwnerID: ownerUserID,
|
||||||
|
Status: "active",
|
||||||
|
Type: "corporation",
|
||||||
|
TypeID: "business",
|
||||||
|
Metadata: map[string]interface{}{"test": true, "uuid": testUUID},
|
||||||
|
}
|
||||||
|
|
||||||
|
var testTeamID string // Store the auto-generated team_id
|
||||||
|
|
||||||
|
// Test CreateTeam
|
||||||
|
t.Run("CreateTeam", func(t *testing.T) {
|
||||||
|
teamMap := maps.MapStrAny{
|
||||||
|
"name": testTeam.Name,
|
||||||
|
"display_name": testTeam.DisplayName,
|
||||||
|
"description": testTeam.Description,
|
||||||
|
"website": testTeam.Website,
|
||||||
|
"owner_id": testTeam.OwnerID,
|
||||||
|
"status": testTeam.Status,
|
||||||
|
"type": testTeam.Type,
|
||||||
|
"type_id": testTeam.TypeID,
|
||||||
|
"metadata": testTeam.Metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := testProvider.CreateTeam(ctx, teamMap)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, id)
|
||||||
|
|
||||||
|
// Verify team was created with auto-generated team_id
|
||||||
|
assert.Contains(t, teamMap, "team_id")
|
||||||
|
assert.NotEmpty(t, teamMap["team_id"])
|
||||||
|
|
||||||
|
// Store generated team_id for subsequent tests
|
||||||
|
testTeamID = teamMap["team_id"].(string)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetTeam
|
||||||
|
t.Run("GetTeam", func(t *testing.T) {
|
||||||
|
team, err := testProvider.GetTeam(ctx, testTeamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, team)
|
||||||
|
assert.Equal(t, testTeam.Name, team["name"])
|
||||||
|
assert.Equal(t, testTeam.DisplayName, team["display_name"])
|
||||||
|
assert.Equal(t, testTeam.OwnerID, team["owner_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetTeamDetail
|
||||||
|
t.Run("GetTeamDetail", func(t *testing.T) {
|
||||||
|
team, err := testProvider.GetTeamDetail(ctx, testTeamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, team)
|
||||||
|
assert.Equal(t, testTeam.Name, team["name"])
|
||||||
|
assert.Equal(t, testTeam.Website, team["website"])
|
||||||
|
assert.Equal(t, testTeam.Description, team["description"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test TeamExists
|
||||||
|
t.Run("TeamExists", func(t *testing.T) {
|
||||||
|
exists, err := testProvider.TeamExists(ctx, testTeamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, exists)
|
||||||
|
|
||||||
|
// Test with non-existent team
|
||||||
|
exists, err = testProvider.TeamExists(ctx, "non-existent-team-"+testUUID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, exists)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateTeam
|
||||||
|
t.Run("UpdateTeam", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"description": "Updated test team description",
|
||||||
|
"display_name": "Updated Display Name",
|
||||||
|
"metadata": map[string]interface{}{"updated": true},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := testProvider.UpdateTeam(ctx, testTeamID, updateData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
team, err := testProvider.GetTeam(ctx, testTeamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Updated test team description", team["description"])
|
||||||
|
assert.Equal(t, "Updated Display Name", team["display_name"])
|
||||||
|
|
||||||
|
// Test updating sensitive fields (should be ignored)
|
||||||
|
sensitiveData := maps.MapStrAny{
|
||||||
|
"team_id": "new-team-id",
|
||||||
|
"created_at": "2023-01-01",
|
||||||
|
"verified_at": "2023-01-01",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = testProvider.UpdateTeam(ctx, testTeamID, sensitiveData)
|
||||||
|
assert.NoError(t, err) // Should not error, just ignore sensitive fields
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateTeamStatus
|
||||||
|
t.Run("UpdateTeamStatus", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateTeamStatus(ctx, testTeamID, "inactive")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify status was updated
|
||||||
|
team, err := testProvider.GetTeam(ctx, testTeamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "inactive", team["status"])
|
||||||
|
|
||||||
|
// Change back to active for other tests
|
||||||
|
err = testProvider.UpdateTeamStatus(ctx, testTeamID, "active")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test VerifyTeam
|
||||||
|
t.Run("VerifyTeam", func(t *testing.T) {
|
||||||
|
err := testProvider.VerifyTeam(ctx, testTeamID, ownerUserID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify team was marked as verified
|
||||||
|
team, err := testProvider.GetTeamDetail(ctx, testTeamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// Database may return int64(1) instead of bool(true)
|
||||||
|
isVerified := team["is_verified"]
|
||||||
|
assert.True(t, isVerified == true || isVerified == int64(1) || isVerified == 1)
|
||||||
|
// verified_by might be nil due to sensitive field filtering, just check it's not empty if present
|
||||||
|
if verifiedBy := team["verified_by"]; verifiedBy != nil {
|
||||||
|
assert.Equal(t, ownerUserID, verifiedBy)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UnverifyTeam
|
||||||
|
t.Run("UnverifyTeam", func(t *testing.T) {
|
||||||
|
err := testProvider.UnverifyTeam(ctx, testTeamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify team was marked as unverified
|
||||||
|
team, err := testProvider.GetTeamDetail(ctx, testTeamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// Database may return int64(0) instead of bool(false)
|
||||||
|
isVerified := team["is_verified"]
|
||||||
|
assert.True(t, isVerified == false || isVerified == int64(0) || isVerified == 0)
|
||||||
|
assert.Nil(t, team["verified_by"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetTeamsByOwner
|
||||||
|
t.Run("GetTeamsByOwner", func(t *testing.T) {
|
||||||
|
teams, err := testProvider.GetTeamsByOwner(ctx, ownerUserID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, teams, 1)
|
||||||
|
assert.Equal(t, testTeamID, teams[0]["team_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetTeamsByStatus
|
||||||
|
t.Run("GetTeamsByStatus", func(t *testing.T) {
|
||||||
|
teams, err := testProvider.GetTeamsByStatus(ctx, "active")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, len(teams) >= 1) // At least our test team
|
||||||
|
|
||||||
|
// Find our test team in the results
|
||||||
|
found := false
|
||||||
|
for _, team := range teams {
|
||||||
|
if team["team_id"] == testTeamID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "Test team should be found in active teams")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test PaginateTeams
|
||||||
|
t.Run("PaginateTeams", func(t *testing.T) {
|
||||||
|
param := model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "status", Value: "active"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := testProvider.PaginateTeams(ctx, param, 1, 10)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
// Pagination result may use "data" instead of "items"
|
||||||
|
assert.True(t, result["data"] != nil || result["items"] != nil)
|
||||||
|
assert.Contains(t, result, "total")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test CountTeams
|
||||||
|
t.Run("CountTeams", func(t *testing.T) {
|
||||||
|
param := model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "status", Value: "active"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := testProvider.CountTeams(ctx, param)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, count >= 1) // At least our test team
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test DeleteTeam (at the end)
|
||||||
|
t.Run("DeleteTeam", func(t *testing.T) {
|
||||||
|
err := testProvider.DeleteTeam(ctx, testTeamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify team was deleted
|
||||||
|
_, err = testProvider.GetTeam(ctx, testTeamID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "team not found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTeamMemberOperations(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
|
||||||
|
testTeam := &TestTeamData{
|
||||||
|
Name: "Member Test Team " + testUUID,
|
||||||
|
DisplayName: "Member Test " + testUUID,
|
||||||
|
Description: "A test team for member testing",
|
||||||
|
OwnerID: ownerUser,
|
||||||
|
Status: "active",
|
||||||
|
Type: "corporation",
|
||||||
|
TypeID: "business",
|
||||||
|
Metadata: map[string]interface{}{"test": true},
|
||||||
|
}
|
||||||
|
|
||||||
|
teamMap := maps.MapStrAny{
|
||||||
|
"name": testTeam.Name,
|
||||||
|
"display_name": testTeam.DisplayName,
|
||||||
|
"description": testTeam.Description,
|
||||||
|
"owner_id": testTeam.OwnerID,
|
||||||
|
"status": testTeam.Status,
|
||||||
|
"type": testTeam.Type,
|
||||||
|
"type_id": testTeam.TypeID,
|
||||||
|
"metadata": testTeam.Metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var memberID int64
|
||||||
|
|
||||||
|
// Test AddMember (invitation-based)
|
||||||
|
t.Run("AddMember", func(t *testing.T) {
|
||||||
|
id, err := testProvider.AddMember(ctx, teamID, memberUser, "user", ownerUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, id, int64(0))
|
||||||
|
memberID = id
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test MemberExists
|
||||||
|
t.Run("MemberExists", func(t *testing.T) {
|
||||||
|
exists, err := testProvider.MemberExists(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, exists)
|
||||||
|
|
||||||
|
// Test with non-existent member
|
||||||
|
exists, err = testProvider.MemberExists(ctx, teamID, "non-existent-user")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, exists)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetMember
|
||||||
|
t.Run("GetMember", func(t *testing.T) {
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member)
|
||||||
|
assert.Equal(t, teamID, member["team_id"])
|
||||||
|
assert.Equal(t, memberUser, member["user_id"])
|
||||||
|
assert.Equal(t, "pending", member["status"]) // Initially pending
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetMemberByID
|
||||||
|
t.Run("GetMemberByID", func(t *testing.T) {
|
||||||
|
member, err := testProvider.GetMemberByID(ctx, memberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member)
|
||||||
|
assert.Equal(t, teamID, member["team_id"])
|
||||||
|
assert.Equal(t, memberUser, member["user_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test AcceptInvitation
|
||||||
|
t.Run("AcceptInvitation", func(t *testing.T) {
|
||||||
|
// First get the invitation token
|
||||||
|
member, err := testProvider.GetMemberDetail(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
invitationToken := member["invitation_token"].(string)
|
||||||
|
assert.NotEmpty(t, invitationToken)
|
||||||
|
|
||||||
|
// Accept the invitation
|
||||||
|
err = testProvider.AcceptInvitation(ctx, invitationToken)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify member status changed to active
|
||||||
|
member, err = testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "active", member["status"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMemberRole
|
||||||
|
t.Run("UpdateMemberRole", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateMemberRole(ctx, teamID, memberUser, "admin")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify role was updated
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "admin", member["role_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMemberStatus
|
||||||
|
t.Run("UpdateMemberStatus", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateMemberStatus(ctx, teamID, memberUser, "inactive")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify status was updated
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "inactive", member["status"])
|
||||||
|
|
||||||
|
// Change back to active
|
||||||
|
err = testProvider.UpdateMemberStatus(ctx, teamID, memberUser, "active")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test UpdateMemberLastActivity
|
||||||
|
t.Run("UpdateMemberLastActivity", func(t *testing.T) {
|
||||||
|
err := testProvider.UpdateMemberLastActivity(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify last_active_at was updated
|
||||||
|
member, err := testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member["last_active_at"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetTeamMembers
|
||||||
|
t.Run("GetTeamMembers", func(t *testing.T) {
|
||||||
|
members, err := testProvider.GetTeamMembers(ctx, teamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, members, 1) // Only our test member
|
||||||
|
assert.Equal(t, memberUser, members[0]["user_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetUserTeams
|
||||||
|
t.Run("GetUserTeams", func(t *testing.T) {
|
||||||
|
teams, err := testProvider.GetUserTeams(ctx, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, teams, 1) // Only our test team
|
||||||
|
assert.Equal(t, teamID, teams[0]["team_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetTeamMembersByStatus
|
||||||
|
t.Run("GetTeamMembersByStatus", func(t *testing.T) {
|
||||||
|
members, err := testProvider.GetTeamMembersByStatus(ctx, teamID, "active")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, members, 1) // Our active member
|
||||||
|
assert.Equal(t, memberUser, members[0]["user_id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test CreateRobotMember
|
||||||
|
t.Run("CreateRobotMember", func(t *testing.T) {
|
||||||
|
robotData := maps.MapStrAny{
|
||||||
|
"robot_name": "TestBot" + testUUID,
|
||||||
|
"robot_description": "A test robot for unit testing",
|
||||||
|
"role_id": "bot",
|
||||||
|
"is_active_robot": true,
|
||||||
|
"robot_status": "idle",
|
||||||
|
}
|
||||||
|
|
||||||
|
robotID, err := testProvider.CreateRobotMember(ctx, teamID, robotData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, robotID, int64(0))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetTeamRobotMembers
|
||||||
|
t.Run("GetTeamRobotMembers", func(t *testing.T) {
|
||||||
|
robots, err := testProvider.GetTeamRobotMembers(ctx, teamID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, robots, 1) // Our test robot
|
||||||
|
assert.Equal(t, "robot", robots[0]["member_type"])
|
||||||
|
assert.Equal(t, "TestBot"+testUUID, robots[0]["robot_name"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test RemoveMember (at the end)
|
||||||
|
t.Run("RemoveMember", func(t *testing.T) {
|
||||||
|
err := testProvider.RemoveMember(ctx, teamID, memberUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify member was removed
|
||||||
|
_, err = testProvider.GetMember(ctx, teamID, memberUser)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "member not found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTeamErrorHandling(t *testing.T) {
|
||||||
|
prepare(t)
|
||||||
|
defer clean()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||||
|
nonExistentTeamID := "non-existent-team-" + testUUID
|
||||||
|
nonExistentUserID := "non-existent-user-" + testUUID
|
||||||
|
|
||||||
|
t.Run("GetTeam_NotFound", func(t *testing.T) {
|
||||||
|
_, err := testProvider.GetTeam(ctx, nonExistentTeamID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "team not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTeamDetail_NotFound", func(t *testing.T) {
|
||||||
|
_, err := testProvider.GetTeamDetail(ctx, nonExistentTeamID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "team not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateTeam_NotFound", func(t *testing.T) {
|
||||||
|
updateData := maps.MapStrAny{"name": "Test"}
|
||||||
|
err := testProvider.UpdateTeam(ctx, nonExistentTeamID, updateData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "team not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteTeam_NotFound", func(t *testing.T) {
|
||||||
|
err := testProvider.DeleteTeam(ctx, nonExistentTeamID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "team not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetMember_NotFound", func(t *testing.T) {
|
||||||
|
_, err := testProvider.GetMember(ctx, nonExistentTeamID, nonExistentUserID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "member not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RemoveMember_NotFound", func(t *testing.T) {
|
||||||
|
err := testProvider.RemoveMember(ctx, nonExistentTeamID, nonExistentUserID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "member not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CreateTeam_MissingRequiredFields", func(t *testing.T) {
|
||||||
|
// Missing name
|
||||||
|
teamData := maps.MapStrAny{
|
||||||
|
"owner_id": "test-owner",
|
||||||
|
}
|
||||||
|
_, err := testProvider.CreateTeam(ctx, teamData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "name is required")
|
||||||
|
|
||||||
|
// Missing owner_id
|
||||||
|
teamData = maps.MapStrAny{
|
||||||
|
"name": "Test Team",
|
||||||
|
}
|
||||||
|
_, err = testProvider.CreateTeam(ctx, teamData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "owner_id is required")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a test user and return the user_id
|
||||||
|
func createTestUser(ctx context.Context, t *testing.T, suffix string) string {
|
||||||
|
userMap := maps.MapStrAny{
|
||||||
|
"preferred_username": "testuser" + suffix,
|
||||||
|
"email": "testuser" + suffix + "@example.com",
|
||||||
|
"password": "TestPass123!",
|
||||||
|
"name": "Test User " + suffix,
|
||||||
|
"given_name": "Test",
|
||||||
|
"family_name": "User",
|
||||||
|
"status": "active",
|
||||||
|
"role_id": "user",
|
||||||
|
"type_id": "regular",
|
||||||
|
"email_verified": true,
|
||||||
|
"metadata": map[string]interface{}{"source": "test"},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := testProvider.CreateUser(ctx, userMap)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
return userMap["user_id"].(string)
|
||||||
|
}
|
||||||
|
|
@ -247,6 +247,70 @@ type UserProvider interface {
|
||||||
GetTypeConfiguration(ctx context.Context, typeID string) (maps.MapStrAny, error)
|
GetTypeConfiguration(ctx context.Context, typeID string) (maps.MapStrAny, error)
|
||||||
SetTypeConfiguration(ctx context.Context, typeID string, config maps.MapStrAny) error
|
SetTypeConfiguration(ctx context.Context, typeID string, config maps.MapStrAny) error
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Team Resource
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Team Basic Operations
|
||||||
|
GetTeam(ctx context.Context, teamID string) (maps.MapStrAny, error)
|
||||||
|
GetTeamDetail(ctx context.Context, teamID string) (maps.MapStrAny, error)
|
||||||
|
TeamExists(ctx context.Context, teamID string) (bool, error)
|
||||||
|
CreateTeam(ctx context.Context, teamData maps.MapStrAny) (string, error)
|
||||||
|
UpdateTeam(ctx context.Context, teamID string, teamData maps.MapStrAny) error
|
||||||
|
DeleteTeam(ctx context.Context, teamID string) error
|
||||||
|
|
||||||
|
// Team List and Search
|
||||||
|
GetTeams(ctx context.Context, param model.QueryParam) ([]maps.MapStr, error)
|
||||||
|
PaginateTeams(ctx context.Context, param model.QueryParam, page int, pagesize int) (maps.MapStr, error)
|
||||||
|
CountTeams(ctx context.Context, param model.QueryParam) (int64, error)
|
||||||
|
|
||||||
|
// Team Query Methods
|
||||||
|
GetTeamsByOwner(ctx context.Context, ownerID string) ([]maps.MapStr, error)
|
||||||
|
GetTeamsByStatus(ctx context.Context, status string) ([]maps.MapStr, error)
|
||||||
|
|
||||||
|
// Team Management
|
||||||
|
UpdateTeamStatus(ctx context.Context, teamID string, status string) error
|
||||||
|
VerifyTeam(ctx context.Context, teamID string, verifiedBy string) error
|
||||||
|
UnverifyTeam(ctx context.Context, teamID string) error
|
||||||
|
TransferTeamOwnership(ctx context.Context, teamID string, newOwnerID string) error
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Member Resource
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Member Basic Operations
|
||||||
|
GetMember(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)
|
||||||
|
MemberExists(ctx context.Context, teamID string, userID string) (bool, error)
|
||||||
|
CreateMember(ctx context.Context, memberData maps.MapStrAny) (int64, error)
|
||||||
|
UpdateMember(ctx context.Context, teamID string, userID string, memberData maps.MapStrAny) error
|
||||||
|
UpdateMemberByID(ctx context.Context, memberID int64, memberData maps.MapStrAny) error
|
||||||
|
RemoveMember(ctx context.Context, teamID string, userID string) error
|
||||||
|
|
||||||
|
// Member Invitation Management
|
||||||
|
AddMember(ctx context.Context, teamID string, userID string, roleID string, invitedBy string) (int64, error)
|
||||||
|
AcceptInvitation(ctx context.Context, invitationToken string) error
|
||||||
|
|
||||||
|
// Robot Member Operations
|
||||||
|
CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (int64, error)
|
||||||
|
UpdateRobotActivity(ctx context.Context, memberID int64, robotStatus string) error
|
||||||
|
GetActiveRobotMembers(ctx context.Context) ([]maps.MapStr, error)
|
||||||
|
|
||||||
|
// Member Query Methods
|
||||||
|
GetTeamMembers(ctx context.Context, teamID string) ([]maps.MapStr, error)
|
||||||
|
GetUserTeams(ctx context.Context, userID string) ([]maps.MapStr, error)
|
||||||
|
GetTeamMembersByStatus(ctx context.Context, teamID string, status string) ([]maps.MapStr, error)
|
||||||
|
GetTeamRobotMembers(ctx context.Context, teamID string) ([]maps.MapStr, error)
|
||||||
|
|
||||||
|
// Member Management
|
||||||
|
UpdateMemberRole(ctx context.Context, teamID string, userID string, roleID string) error
|
||||||
|
UpdateMemberStatus(ctx context.Context, teamID string, userID string, status string) error
|
||||||
|
UpdateMemberLastActivity(ctx context.Context, teamID string, userID string) error
|
||||||
|
|
||||||
|
// Member List and Search
|
||||||
|
PaginateMembers(ctx context.Context, param model.QueryParam, page int, pagesize int) (maps.MapStr, error)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Utils
|
// Utils
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue