Merge pull request #1146 from trheyi/main

Add team and member management capabilities to user provider
This commit is contained in:
Max 2025-09-16 17:34:12 +08:00 committed by GitHub
commit 4a2eab202b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 2392 additions and 0 deletions

View file

@ -11,6 +11,8 @@ const (
ErrRoleNotFound = "role not found"
ErrTypeNotFound = "type not found"
ErrOAuthAccountNotFound = "oauth account not found"
ErrTeamNotFound = "team not found"
ErrMemberNotFound = "member not found"
ErrInvalidIdentifierType = "invalid identifier type: %s"
ErrNoPasswordHash = "no password hash found"
ErrFailedToGenerateUserID = "failed to generate user_id: %w"
@ -21,18 +23,26 @@ const (
ErrFailedToGetRole = "failed to get role: %w"
ErrFailedToGetType = "failed to get type: %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"
ErrFailedToCreateRole = "failed to create role: %w"
ErrFailedToCreateType = "failed to create type: %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"
ErrFailedToUpdateRole = "failed to update role: %w"
ErrFailedToUpdateType = "failed to update type: %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"
ErrFailedToDeleteRole = "failed to delete role: %w"
ErrFailedToDeleteType = "failed to delete type: %w"
ErrFailedToDeleteOAuth = "failed to delete oauth account: %w"
ErrFailedToDeleteTeam = "failed to delete team: %w"
ErrFailedToDeleteMember = "failed to delete member: %w"
// MFA related errors
ErrMFANotEnabled = "MFA is not enabled for this user"
@ -116,6 +126,40 @@ var (
"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 = &types.MFAOptions{
Issuer: "Yao App Engine",
@ -135,6 +179,8 @@ type DefaultUser struct {
roleModel string
typeModel string
oauthAccountModel string
teamModel string
memberModel string
cache store.Store
// ID Generation Configuration
@ -159,6 +205,14 @@ type DefaultUser struct {
typeFields []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
mfaOptions *types.MFAOptions // configurable MFA settings
}
@ -180,6 +234,8 @@ type DefaultUserOptions struct {
RoleModel string // bind to a specific role model
TypeModel string // bind to a specific type 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
// ID Generation Strategy
@ -203,6 +259,14 @@ type DefaultUserOptions struct {
TypeFields []interface{} // basic type fields
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)
MFAOptions *types.MFAOptions // MFA settings
}
@ -230,6 +294,16 @@ func NewDefaultUser(options *DefaultUserOptions) *DefaultUser {
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
idStrategy := options.IDStrategy
if idStrategy == "" {
@ -283,6 +357,28 @@ func NewDefaultUser(options *DefaultUserOptions) *DefaultUser {
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
mfaOptions := options.MFAOptions
if mfaOptions == nil {
@ -295,6 +391,8 @@ func NewDefaultUser(options *DefaultUserOptions) *DefaultUser {
roleModel: roleModel,
typeModel: typeModel,
oauthAccountModel: oauthAccountModel,
teamModel: teamModel,
memberModel: memberModel,
cache: options.Cache,
idStrategy: idStrategy,
idPrefix: idPrefix,
@ -315,6 +413,14 @@ func NewDefaultUser(options *DefaultUserOptions) *DefaultUser {
typeFields: typeFields,
typeDetailFields: typeDetailFields,
// Team field lists
teamFields: teamFields,
teamDetailFields: teamDetailFields,
// Member field lists
memberFields: memberFields,
memberDetailFields: memberDetailFields,
// MFA Configuration
mfaOptions: mfaOptions,
}

View 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
}

View 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

View 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)
}

View 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)
}

View file

@ -247,6 +247,70 @@ type UserProvider interface {
GetTypeConfiguration(ctx context.Context, typeID string) (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
// ============================================================================