Add member profile retrieval functionality
- Introduced a new endpoint `GET /teams/:team_id/members/:member_id/profile` to allow users to retrieve member profile information, including display name, bio, avatar, and email. - Implemented the `GinMemberGetProfile` handler to process profile retrieval requests with appropriate authentication and error handling. - Added business logic in `memberGetProfile` to ensure only authenticated users can access their own profile data. - Expanded test cases in `TestMemberProfileGet` to validate various scenarios for profile retrieval, ensuring comprehensive coverage for authentication and data validation.
This commit is contained in:
parent
af02a21f65
commit
bf452ba1d3
3 changed files with 295 additions and 0 deletions
|
|
@ -2038,6 +2038,173 @@ func TestMemberUpdateRobot(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestMemberProfileGet tests the GET /user/teams/:team_id/members/:user_id/profile endpoint
|
||||
func TestMemberProfileGet(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
// Get base URL from server config
|
||||
baseURL := ""
|
||||
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||
baseURL = openapi.Server.Config.BaseURL
|
||||
}
|
||||
|
||||
// Register a test client for OAuth authentication
|
||||
testClient := testutils.RegisterTestClient(t, "Member Profile Get Test Client", []string{"https://localhost/callback"})
|
||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||
|
||||
// Obtain access token with root permission
|
||||
tokenInfo := testutils.ObtainAccessTokenWithRootPermission(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile system:root")
|
||||
|
||||
// Create a test team
|
||||
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Member Profile Get Test Team")
|
||||
teamID := getTeamID(createdTeam)
|
||||
|
||||
// The creator is automatically a member, so we can use their user_id
|
||||
userID := tokenInfo.UserID
|
||||
|
||||
// Update the member profile first to have test data
|
||||
provider := testutils.GetUserProvider(t)
|
||||
ctx := context.Background()
|
||||
updateData := maps.MapStrAny{
|
||||
"display_name": "Test Display Name",
|
||||
"bio": "Test bio description",
|
||||
"avatar": "https://example.com/test-avatar.png",
|
||||
"email": "test-member@example.com",
|
||||
}
|
||||
err := provider.UpdateMember(ctx, teamID, userID, updateData)
|
||||
assert.NoError(t, err, "Should update member profile for testing")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
teamID string
|
||||
userID string
|
||||
headers map[string]string
|
||||
expectCode int
|
||||
expectMsg string
|
||||
validateFn func(*testing.T, map[string]interface{}) // Optional validation function
|
||||
}{
|
||||
{
|
||||
"get profile without authentication",
|
||||
teamID,
|
||||
userID,
|
||||
map[string]string{},
|
||||
401,
|
||||
"should require authentication",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"get own profile successfully",
|
||||
teamID,
|
||||
userID,
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should return own profile successfully",
|
||||
func(t *testing.T, profile map[string]interface{}) {
|
||||
// Verify profile structure
|
||||
assert.Contains(t, profile, "user_id", "Should have user_id")
|
||||
assert.Contains(t, profile, "team_id", "Should have team_id")
|
||||
assert.Contains(t, profile, "display_name", "Should have display_name")
|
||||
assert.Contains(t, profile, "bio", "Should have bio")
|
||||
assert.Contains(t, profile, "avatar", "Should have avatar")
|
||||
assert.Contains(t, profile, "email", "Should have email")
|
||||
|
||||
// Verify values
|
||||
assert.Equal(t, userID, profile["user_id"], "Should have correct user_id")
|
||||
assert.Equal(t, teamID, profile["team_id"], "Should have correct team_id")
|
||||
assert.Equal(t, "Test Display Name", profile["display_name"], "Should have correct display_name")
|
||||
assert.Equal(t, "Test bio description", profile["bio"], "Should have correct bio")
|
||||
assert.Equal(t, "https://example.com/test-avatar.png", profile["avatar"], "Should have correct avatar")
|
||||
assert.Equal(t, "test-member@example.com", profile["email"], "Should have correct email")
|
||||
},
|
||||
},
|
||||
{
|
||||
"get profile from non-existent team",
|
||||
"non-existent-team-id",
|
||||
userID,
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
404,
|
||||
"should return not found for non-existent team",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"get profile for non-existent user",
|
||||
teamID,
|
||||
"non-existent-user-id",
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
404,
|
||||
"should return not found for non-existent user",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"get profile with minimal data",
|
||||
teamID,
|
||||
userID,
|
||||
map[string]string{
|
||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||
},
|
||||
200,
|
||||
"should return profile even with minimal data",
|
||||
func(t *testing.T, profile map[string]interface{}) {
|
||||
// Should always have these fields, even if empty
|
||||
assert.Contains(t, profile, "user_id", "Should have user_id field")
|
||||
assert.Contains(t, profile, "team_id", "Should have team_id field")
|
||||
assert.Contains(t, profile, "display_name", "Should have display_name field")
|
||||
assert.Contains(t, profile, "bio", "Should have bio field")
|
||||
assert.Contains(t, profile, "avatar", "Should have avatar field")
|
||||
assert.Contains(t, profile, "email", "Should have email field")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members/" + tc.userID + "/profile"
|
||||
|
||||
req, err := http.NewRequest("GET", requestURL, nil)
|
||||
assert.NoError(t, err, "Should create HTTP request")
|
||||
|
||||
// Add headers
|
||||
for key, value := range tc.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err, "HTTP request should succeed")
|
||||
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "Should read response body")
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
// Parse response as profile object
|
||||
var profile map[string]interface{}
|
||||
err = json.Unmarshal(body, &profile)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
|
||||
// Run custom validation if provided
|
||||
if tc.validateFn != nil {
|
||||
tc.validateFn(t, profile)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Member profile get test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMemberProfileUpdate tests the PUT /user/teams/:team_id/members/:user_id/profile endpoint
|
||||
func TestMemberProfileUpdate(t *testing.T) {
|
||||
// Initialize test environment
|
||||
|
|
|
|||
|
|
@ -575,6 +575,62 @@ func GinMemberUpdate(c *gin.Context) {
|
|||
response.RespondWithSuccess(c, http.StatusOK, gin.H{"message": "Member updated successfully"})
|
||||
}
|
||||
|
||||
// GinMemberGetProfile handles GET /teams/:team_id/members/:member_id/profile - Get member profile
|
||||
// Note: :member_id in the route actually contains user_id for profile retrieval
|
||||
func GinMemberGetProfile(c *gin.Context) {
|
||||
// Get authorized user info
|
||||
authInfo := authorized.GetInfo(c)
|
||||
if authInfo == nil || authInfo.UserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidClient.Code,
|
||||
ErrorDescription: "User not authenticated",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
teamID := c.Param("id")
|
||||
// For profile retrieval, :member_id parameter contains the user_id (not member_id)
|
||||
memberUserID := c.Param("member_id")
|
||||
if teamID == "" || memberUserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Team ID and User ID are required",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Call business logic
|
||||
profile, err := memberGetProfile(c.Request.Context(), authInfo.UserID, teamID, memberUserID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get member profile: %v", err)
|
||||
// Check error type for appropriate response
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Member not found",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||
} else if strings.Contains(err.Error(), "access denied") {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrAccessDenied.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||
} else {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to get member profile",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondWithSuccess(c, http.StatusOK, profile)
|
||||
}
|
||||
|
||||
// GinMemberUpdateProfile handles PUT /teams/:team_id/members/:member_id/profile - Update member profile
|
||||
// Note: :member_id in the route actually contains user_id for profile updates
|
||||
func GinMemberUpdateProfile(c *gin.Context) {
|
||||
|
|
@ -877,6 +933,38 @@ func ProcessMemberUpdate(process *process.Process) interface{} {
|
|||
}
|
||||
}
|
||||
|
||||
// ProcessMemberGetProfile user.member.profile.get Member profile get processor
|
||||
// Args[0] string: team_id
|
||||
// Args[1] string: user_id (not member_id)
|
||||
// Return: map: Member profile data
|
||||
func ProcessMemberGetProfile(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(2)
|
||||
|
||||
// Get user_id from session
|
||||
requestUserID := GetUserIDFromSession(process)
|
||||
|
||||
teamID := process.ArgsString(0)
|
||||
memberUserID := process.ArgsString(1)
|
||||
|
||||
if teamID == "" || memberUserID == "" {
|
||||
exception.New("team_id and user_id are required", 400).Throw()
|
||||
}
|
||||
|
||||
// Get context
|
||||
ctx := process.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// Call business logic
|
||||
result, err := memberGetProfile(ctx, requestUserID, teamID, memberUserID)
|
||||
if err != nil {
|
||||
exception.New("failed to get member profile: %s", 500, err.Error()).Throw()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ProcessMemberUpdateProfile user.member.profile.update Member profile update processor
|
||||
// Args[0] string: team_id
|
||||
// Args[1] string: user_id (member's user_id)
|
||||
|
|
@ -1256,6 +1344,44 @@ func memberUpdate(ctx context.Context, userID, teamID, memberID string, updateDa
|
|||
return nil
|
||||
}
|
||||
|
||||
// memberGetProfile handles the business logic for getting member profile information
|
||||
func memberGetProfile(ctx context.Context, requestUserID, teamID, memberUserID string) (maps.MapStrAny, error) {
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
||||
}
|
||||
|
||||
// Check if member exists using team_id and user_id
|
||||
member, err := provider.GetMember(ctx, teamID, memberUserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("member not found: %w", err)
|
||||
}
|
||||
|
||||
// Verify member exists
|
||||
if member == nil {
|
||||
return nil, fmt.Errorf("member not found in the specified team")
|
||||
}
|
||||
|
||||
// Check if the requesting user is the member themselves
|
||||
// Only members can view their own profile
|
||||
if memberUserID != requestUserID {
|
||||
return nil, fmt.Errorf("access denied: you can only view your own profile")
|
||||
}
|
||||
|
||||
// Return member profile data (display_name, bio, avatar, email)
|
||||
profileData := maps.MapStrAny{
|
||||
"user_id": memberUserID,
|
||||
"team_id": teamID,
|
||||
"display_name": member["display_name"],
|
||||
"bio": member["bio"],
|
||||
"avatar": member["avatar"],
|
||||
"email": member["email"],
|
||||
}
|
||||
|
||||
return profileData, nil
|
||||
}
|
||||
|
||||
// memberUpdateProfile handles the business logic for updating member profile information
|
||||
func memberUpdateProfile(ctx context.Context, requestUserID, teamID, memberUserID string, req UpdateMemberProfileRequest) error {
|
||||
// Get user provider instance
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ func init() {
|
|||
"member.list": ProcessMemberList,
|
||||
"member.get": ProcessMemberGet,
|
||||
"member.update": ProcessMemberUpdate,
|
||||
"member.profile.get": ProcessMemberGetProfile,
|
||||
"member.profile.update": ProcessMemberUpdateProfile,
|
||||
"member.delete": ProcessMemberDelete,
|
||||
|
||||
|
|
@ -152,6 +153,7 @@ func attachTeam(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
team.GET("/:id/members/check-robot-email", GinMemberCheckRobotEmail) // GET /api/user/teams/:id/members/check-robot-email?robot_email=xxx - Check if robot email exists globally
|
||||
team.POST("/:id/members/robots", GinMemberCreateRobot) // POST /api/user/teams/:id/members/robots - Add robot member
|
||||
team.PUT("/:id/members/robots/:member_id", GinMemberUpdateRobot) // PUT /api/user/teams/:id/members/robots/:member_id - Update robot member
|
||||
team.GET("/:id/members/:member_id/profile", GinMemberGetProfile) // GET /api/user/teams/:id/members/:member_id/profile - Get member profile (display_name, bio, avatar, email)
|
||||
team.PUT("/:id/members/:member_id/profile", GinMemberUpdateProfile) // PUT /api/user/teams/:id/members/:member_id/profile - Update member profile (display_name, bio, avatar, email)
|
||||
team.GET("/:id/members/:member_id", GinMemberGet) // GET /api/user/teams/:id/members/:member_id - Get member details
|
||||
team.PUT("/:id/members/:member_id", GinMemberUpdate) // PUT /api/user/teams/:id/members/:member_id - Update member (admin: role, status)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue