- Modified the AcceptInvitation method to accept an optional user ID parameter, allowing for updates to the user ID when accepting invitations without an existing user ID. - Adjusted related tests to include the new user ID parameter, ensuring comprehensive coverage of invitation acceptance scenarios. - Updated the team invitation acceptance logic to utilize the new parameter, enhancing the invitation flow and user management capabilities.
1004 lines
32 KiB
Go
1004 lines
32 KiB
Go
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
|
|
var invitationID 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 and invitation_id for acceptance test
|
|
memberDetail, err := testProvider.GetMemberDetail(ctx, teamID, inviteeUser)
|
|
assert.NoError(t, err)
|
|
invitationToken = memberDetail["invitation_token"].(string)
|
|
assert.NotEmpty(t, invitationToken)
|
|
invitationID = memberDetail["invitation_id"].(string)
|
|
assert.NotEmpty(t, invitationID)
|
|
|
|
// 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, invitationID, 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, invitationID, "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, invitationID, 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 active user member
|
|
memberData = maps.MapStrAny{
|
|
"team_id": "test-team",
|
|
"role_id": "user",
|
|
"member_type": "user",
|
|
"status": "active", // Explicitly set to active to trigger validation
|
|
}
|
|
_, err = testProvider.CreateMember(ctx, memberData)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user_id is required for active 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
|
|
expiredTime := time.Now().Add(-2 * time.Hour) // Expired 2 hours ago to be safe
|
|
memberData := maps.MapStrAny{
|
|
"team_id": teamID,
|
|
"user_id": inviteeUser,
|
|
"member_type": "user",
|
|
"role_id": "user",
|
|
"status": "pending",
|
|
"invited_by": ownerUser,
|
|
"invited_at": expiredTime.Add(-1 * time.Hour), // Invited 3 hours ago
|
|
"invitation_token": "expired-token-" + testUUID,
|
|
"invitation_expires_at": expiredTime, // Expired 2 hours ago
|
|
}
|
|
|
|
memberID, err := testProvider.CreateMember(ctx, memberData)
|
|
assert.NoError(t, err)
|
|
|
|
// Get the invitation_id
|
|
member, err := testProvider.GetMemberByID(ctx, memberID)
|
|
assert.NoError(t, err)
|
|
invitationID := member["invitation_id"].(string)
|
|
assert.NotEmpty(t, invitationID)
|
|
|
|
// Test AcceptInvitation with expired token
|
|
t.Run("AcceptInvitation_ExpiredToken", func(t *testing.T) {
|
|
err := testProvider.AcceptInvitation(ctx, invitationID, "expired-token-"+testUUID, "")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invitation has expired")
|
|
})
|
|
}
|
|
|
|
func TestMemberInvitationIDOperations(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 ID Test Team " + testUUID,
|
|
"display_name": "Invitation ID Test " + testUUID,
|
|
"description": "A test team for invitation_id testing",
|
|
"owner_id": ownerUser,
|
|
"status": "active",
|
|
"type": "corporation",
|
|
"type_id": "business",
|
|
"metadata": map[string]interface{}{"test": true},
|
|
}
|
|
|
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
|
assert.NoError(t, err)
|
|
|
|
var invitationID string
|
|
|
|
// Test CreateMember with pending status (should generate invitation_id)
|
|
t.Run("CreateMember_GeneratesInvitationID", func(t *testing.T) {
|
|
memberData := maps.MapStrAny{
|
|
"team_id": teamID,
|
|
"user_id": nil, // Simulate invitation to unregistered user
|
|
"member_type": "user",
|
|
"role_id": "user",
|
|
"status": "pending",
|
|
"invited_by": ownerUser,
|
|
}
|
|
|
|
memberID, err := testProvider.CreateMember(ctx, memberData)
|
|
assert.NoError(t, err)
|
|
assert.Greater(t, memberID, int64(0))
|
|
|
|
// Get the created member to verify invitation_id was generated
|
|
member, err := testProvider.GetMemberByID(ctx, memberID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, member["invitation_id"])
|
|
assert.NotEmpty(t, member["invitation_id"])
|
|
|
|
invitationID = member["invitation_id"].(string)
|
|
t.Logf("Generated invitation_id: %s", invitationID)
|
|
assert.True(t, strings.Contains(invitationID, "inv_"), "invitation_id should contain inv_ prefix, got: "+invitationID)
|
|
})
|
|
|
|
// Test GetMemberByInvitationID
|
|
t.Run("GetMemberByInvitationID", func(t *testing.T) {
|
|
member, err := testProvider.GetMemberByInvitationID(ctx, invitationID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, member)
|
|
assert.Equal(t, invitationID, member["invitation_id"])
|
|
assert.Equal(t, teamID, member["team_id"])
|
|
assert.Equal(t, "pending", member["status"])
|
|
assert.Equal(t, ownerUser, member["invited_by"])
|
|
})
|
|
|
|
// Test GetMemberByInvitationID with non-existent invitation
|
|
t.Run("GetMemberByInvitationID_NotFound", func(t *testing.T) {
|
|
_, err := testProvider.GetMemberByInvitationID(ctx, "non-existent-invitation-id")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "member not found")
|
|
})
|
|
|
|
// Test UpdateMemberByInvitationID
|
|
t.Run("UpdateMemberByInvitationID", func(t *testing.T) {
|
|
updateData := maps.MapStrAny{
|
|
"user_id": inviteeUser, // Now associate with a user
|
|
"status": "active",
|
|
"joined_at": time.Now(),
|
|
"notes": "Invitation accepted",
|
|
}
|
|
|
|
err := testProvider.UpdateMemberByInvitationID(ctx, invitationID, updateData)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify update
|
|
member, err := testProvider.GetMemberByInvitationID(ctx, invitationID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, inviteeUser, member["user_id"])
|
|
assert.Equal(t, "active", member["status"])
|
|
assert.NotNil(t, member["joined_at"])
|
|
|
|
// Test updating sensitive fields (should be ignored except user_id which is allowed)
|
|
sensitiveData := maps.MapStrAny{
|
|
"id": 999,
|
|
"team_id": "new-team",
|
|
"invitation_id": "new-invitation-id",
|
|
}
|
|
|
|
err = testProvider.UpdateMemberByInvitationID(ctx, invitationID, sensitiveData)
|
|
assert.NoError(t, err) // Should not error, just ignore sensitive fields
|
|
|
|
// Verify sensitive fields were not updated
|
|
member, err = testProvider.GetMemberByInvitationID(ctx, invitationID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, invitationID, member["invitation_id"]) // Should remain unchanged
|
|
assert.Equal(t, teamID, member["team_id"]) // Should remain unchanged
|
|
assert.Equal(t, inviteeUser, member["user_id"]) // Should remain as updated value
|
|
})
|
|
|
|
// Test UpdateMemberByInvitationID with non-existent invitation
|
|
t.Run("UpdateMemberByInvitationID_NotFound", func(t *testing.T) {
|
|
updateData := maps.MapStrAny{"notes": "test"}
|
|
err := testProvider.UpdateMemberByInvitationID(ctx, "non-existent-invitation-id", updateData)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "member not found")
|
|
})
|
|
|
|
// Test UpdateMemberByInvitationID with empty data (should not error)
|
|
t.Run("UpdateMemberByInvitationID_EmptyData", func(t *testing.T) {
|
|
err := testProvider.UpdateMemberByInvitationID(ctx, invitationID, maps.MapStrAny{})
|
|
assert.NoError(t, err) // Should not error, just do nothing
|
|
})
|
|
|
|
// Test RemoveMemberByInvitationID (at the end)
|
|
t.Run("RemoveMemberByInvitationID", func(t *testing.T) {
|
|
err := testProvider.RemoveMemberByInvitationID(ctx, invitationID)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify member was removed
|
|
_, err = testProvider.GetMemberByInvitationID(ctx, invitationID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "member not found")
|
|
})
|
|
|
|
// Test RemoveMemberByInvitationID with non-existent invitation
|
|
t.Run("RemoveMemberByInvitationID_NotFound", func(t *testing.T) {
|
|
err := testProvider.RemoveMemberByInvitationID(ctx, "non-existent-invitation-id")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "member not found")
|
|
})
|
|
}
|
|
|
|
func TestCreateMemberInvitationIDGeneration(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
|
|
ownerUser := createTestUser(ctx, t, "owner"+testUUID)
|
|
|
|
// Create test team
|
|
teamMap := maps.MapStrAny{
|
|
"name": "ID Generation Test Team " + testUUID,
|
|
"display_name": "ID Generation Test " + testUUID,
|
|
"description": "A test team for invitation_id generation testing",
|
|
"owner_id": ownerUser,
|
|
"status": "active",
|
|
}
|
|
|
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
|
assert.NoError(t, err)
|
|
|
|
// Test invitation_id generation for pending members
|
|
t.Run("CreateMember_PendingStatus_GeneratesInvitationID", func(t *testing.T) {
|
|
memberData := maps.MapStrAny{
|
|
"team_id": teamID,
|
|
"user_id": nil, // No user_id for pending invitation
|
|
"member_type": "user",
|
|
"role_id": "user",
|
|
"status": "pending",
|
|
"invited_by": ownerUser,
|
|
}
|
|
|
|
memberID, err := testProvider.CreateMember(ctx, memberData)
|
|
assert.NoError(t, err)
|
|
|
|
// Get the created member
|
|
member, err := testProvider.GetMemberByID(ctx, memberID)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify invitation_id was generated
|
|
assert.NotNil(t, member["invitation_id"])
|
|
assert.NotEmpty(t, member["invitation_id"])
|
|
|
|
invitationID := member["invitation_id"].(string)
|
|
t.Logf("Generated invitation_id: %s", invitationID)
|
|
assert.True(t, strings.Contains(invitationID, "inv_"), "invitation_id should contain inv_ prefix, got: "+invitationID)
|
|
assert.True(t, len(invitationID) > 4, "invitation_id should be longer than just the prefix")
|
|
})
|
|
|
|
// Test that active members don't get invitation_id
|
|
t.Run("CreateMember_ActiveStatus_NoInvitationID", func(t *testing.T) {
|
|
activeUser := createTestUser(ctx, t, "active"+testUUID)
|
|
|
|
memberData := maps.MapStrAny{
|
|
"team_id": teamID,
|
|
"user_id": activeUser,
|
|
"member_type": "user",
|
|
"role_id": "user",
|
|
"status": "active",
|
|
}
|
|
|
|
memberID, err := testProvider.CreateMember(ctx, memberData)
|
|
assert.NoError(t, err)
|
|
|
|
// Get the created member
|
|
member, err := testProvider.GetMemberByID(ctx, memberID)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify invitation_id is nil for active members
|
|
assert.Nil(t, member["invitation_id"])
|
|
})
|
|
|
|
// Test explicit invitation_id is preserved
|
|
t.Run("CreateMember_ExplicitInvitationID_Preserved", func(t *testing.T) {
|
|
explicitInvitationID := "inv_explicit_test_" + testUUID
|
|
|
|
memberData := maps.MapStrAny{
|
|
"team_id": teamID,
|
|
"user_id": nil,
|
|
"member_type": "user",
|
|
"role_id": "user",
|
|
"status": "pending",
|
|
"invited_by": ownerUser,
|
|
"invitation_id": explicitInvitationID,
|
|
}
|
|
|
|
memberID, err := testProvider.CreateMember(ctx, memberData)
|
|
assert.NoError(t, err)
|
|
|
|
// Get the created member
|
|
member, err := testProvider.GetMemberByID(ctx, memberID)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify explicit invitation_id was preserved
|
|
assert.Equal(t, explicitInvitationID, member["invitation_id"])
|
|
})
|
|
}
|
|
|
|
// Helper function createTestUser is defined in team_test.go
|