feat(profile): align chatwoot user serializer
This commit is contained in:
@@ -30,14 +30,15 @@ func (h *ProfileHandler) Get(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.svc.Get(c.Request.Context(), userID)
|
||||
accountID := c.GetUint("account_id")
|
||||
user, err := h.svc.Get(c.Request.Context(), userID, accountID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Get profile for user %d: %v", userID, err)
|
||||
handleServiceError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, user)
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// Update updates the current user's profile.
|
||||
@@ -55,14 +56,15 @@ func (h *ProfileHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user, svcErr := h.svc.Update(c.Request.Context(), userID, req.Profile)
|
||||
accountID := c.GetUint("account_id")
|
||||
user, svcErr := h.svc.Update(c.Request.Context(), userID, accountID, req.Profile)
|
||||
if svcErr != nil {
|
||||
applogger.L().Errorf("Update profile for user %d: %v", userID, svcErr)
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, user)
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// UpdateAvatar updates the current user's avatar.
|
||||
@@ -80,14 +82,15 @@ func (h *ProfileHandler) UpdateAvatar(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user, svcErr := h.svc.UpdateAvatar(c.Request.Context(), userID, req)
|
||||
accountID := c.GetUint("account_id")
|
||||
user, svcErr := h.svc.UpdateAvatar(c.Request.Context(), userID, accountID, req)
|
||||
if svcErr != nil {
|
||||
applogger.L().Errorf("Update avatar for user %d: %v", userID, svcErr)
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, user)
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// SetAvailability updates the user's availability status for a specific account.
|
||||
@@ -106,14 +109,14 @@ func (h *ProfileHandler) SetAvailability(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
au, svcErr := h.svc.SetAvailability(c.Request.Context(), userID, req.Profile)
|
||||
user, svcErr := h.svc.SetAvailability(c.Request.Context(), userID, req.Profile)
|
||||
if svcErr != nil {
|
||||
applogger.L().Errorf("SetAvailability for user %d: %v", userID, svcErr)
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, au)
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// SetAutoOffline updates the user's auto_offline setting for a specific account.
|
||||
@@ -132,14 +135,14 @@ func (h *ProfileHandler) SetAutoOffline(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
au, svcErr := h.svc.SetAutoOffline(c.Request.Context(), userID, req.Profile)
|
||||
user, svcErr := h.svc.SetAutoOffline(c.Request.Context(), userID, req.Profile)
|
||||
if svcErr != nil {
|
||||
applogger.L().Errorf("SetAutoOffline for user %d: %v", userID, svcErr)
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, au)
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// SetActiveAccount sets the user's currently active account.
|
||||
@@ -198,14 +201,15 @@ func (h *ProfileHandler) ResetAccessToken(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user, svcErr := h.svc.ResetAccessToken(c.Request.Context(), userID)
|
||||
accountID := c.GetUint("account_id")
|
||||
user, svcErr := h.svc.ResetAccessToken(c.Request.Context(), userID, accountID)
|
||||
if svcErr != nil {
|
||||
applogger.L().Errorf("ResetAccessToken for user %d: %v", userID, svcErr)
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, user)
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// DeleteAvatar removes the user's avatar.
|
||||
@@ -217,11 +221,12 @@ func (h *ProfileHandler) DeleteAvatar(c *gin.Context) {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated")
|
||||
return
|
||||
}
|
||||
user, err := h.svc.DeleteAvatar(c.Request.Context(), userID)
|
||||
accountID := c.GetUint("account_id")
|
||||
user, err := h.svc.DeleteAvatar(c.Request.Context(), userID, accountID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("DeleteAvatar for user %d: %v", userID, err)
|
||||
handleServiceError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, user)
|
||||
}
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
@@ -54,29 +54,40 @@ func (s *ProfileHandlerTestSuite) SetupSuite() {
|
||||
&model.Account{},
|
||||
&model.User{},
|
||||
&model.AccountUser{},
|
||||
&model.AccessToken{},
|
||||
))
|
||||
s.db = db
|
||||
|
||||
// Create test account
|
||||
account := &model.Account{Name: "TestAccount"}
|
||||
account := &model.Account{Name: "TestAccount", Status: "active", OnboardingStep: "profile"}
|
||||
s.Require().NoError(db.Create(account).Error)
|
||||
s.account = account
|
||||
s.accountID = account.ID
|
||||
|
||||
// Create test user
|
||||
user := &model.User{Name: "ProfileUser", Email: "profile@example.com", AccountID: account.ID}
|
||||
user := &model.User{
|
||||
Name: "ProfileUser",
|
||||
Email: "profile@example.com",
|
||||
AccountID: account.ID,
|
||||
Provider: "email",
|
||||
DisplayName: "Profile Display",
|
||||
MessageSignature: "Regards",
|
||||
PubsubToken: "pubsub-profile-user",
|
||||
}
|
||||
s.Require().NoError(db.Create(user).Error)
|
||||
s.user = user
|
||||
s.userID = user.ID
|
||||
|
||||
// Link user to account
|
||||
accountUser := &model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "administrator"}
|
||||
accountUser := &model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "administrator", Availability: "offline", AutoOffline: true}
|
||||
s.Require().NoError(db.Create(accountUser).Error)
|
||||
s.Require().NoError(db.Create(&model.AccessToken{OwnerType: model.AccessTokenOwnerTypeUser, OwnerID: user.ID, Token: "profile-token-1", TokenPrefix: "profile-", Name: "Personal Access Token"}).Error)
|
||||
|
||||
// Create real repos + service
|
||||
userRepo := repository.NewUserRepo(db)
|
||||
accountUserRepo := repository.NewAccountUserRepo(db)
|
||||
profileSvc := service.NewProfileService(userRepo, accountUserRepo)
|
||||
accessTokenRepo := repository.NewAccessTokenRepo(db)
|
||||
profileSvc := service.NewProfileService(userRepo, accountUserRepo, accessTokenRepo)
|
||||
s.handler = NewProfileHandler(profileSvc)
|
||||
|
||||
// Build router with profile routes and auth middleware
|
||||
@@ -110,11 +121,35 @@ func (s *ProfileHandlerTestSuite) buildRouter() *gin.Engine {
|
||||
func (s *ProfileHandlerTestSuite) SetupTest() {
|
||||
// Reset user to original state before each test
|
||||
s.db.Model(&model.User{}).Where("id = ?", s.userID).Updates(map[string]interface{}{
|
||||
"name": "ProfileUser",
|
||||
"email": "profile@example.com",
|
||||
"avatar_url": "",
|
||||
"available": false,
|
||||
"name": "ProfileUser",
|
||||
"email": "profile@example.com",
|
||||
"avatar_url": "",
|
||||
"available": false,
|
||||
"display_name": "Profile Display",
|
||||
"message_signature": "Regards",
|
||||
"pubsub_token": "pubsub-profile-user",
|
||||
})
|
||||
s.db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", s.accountID, s.userID).Updates(map[string]interface{}{
|
||||
"availability": "offline",
|
||||
"auto_offline": true,
|
||||
})
|
||||
s.db.Unscoped().Where("owner_type = ? AND owner_id = ?", model.AccessTokenOwnerTypeUser, s.userID).Delete(&model.AccessToken{})
|
||||
s.Require().NoError(s.db.Create(&model.AccessToken{OwnerType: model.AccessTokenOwnerTypeUser, OwnerID: s.userID, Token: "profile-token-1", TokenPrefix: "profile-", Name: "Personal Access Token"}).Error)
|
||||
}
|
||||
|
||||
func (s *ProfileHandlerTestSuite) decodeProfileBody(w *httptest.ResponseRecorder) map[string]interface{} {
|
||||
var payload map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
|
||||
return payload
|
||||
}
|
||||
|
||||
func (s *ProfileHandlerTestSuite) firstAccountFromProfile(payload map[string]interface{}) map[string]interface{} {
|
||||
accounts, ok := payload["accounts"].([]interface{})
|
||||
s.Require().True(ok)
|
||||
s.Require().Len(accounts, 1)
|
||||
account, ok := accounts[0].(map[string]interface{})
|
||||
s.Require().True(ok)
|
||||
return account
|
||||
}
|
||||
|
||||
// ===================== Get Profile =====================
|
||||
@@ -126,15 +161,23 @@ func (s *ProfileHandlerTestSuite) TestGet_Success() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp response.APIResponse
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.True(s.T(), resp.Success)
|
||||
|
||||
// Data should contain user info
|
||||
dataMap, ok := resp.Data.(map[string]interface{})
|
||||
assert.True(s.T(), ok)
|
||||
dataMap := s.decodeProfileBody(w)
|
||||
assert.Equal(s.T(), "ProfileUser", dataMap["name"])
|
||||
assert.Equal(s.T(), "profile@example.com", dataMap["email"])
|
||||
assert.Equal(s.T(), "profile-token-1", dataMap["access_token"])
|
||||
assert.Equal(s.T(), "Profile Display", dataMap["available_name"])
|
||||
assert.Equal(s.T(), "Regards", dataMap["message_signature"])
|
||||
assert.Equal(s.T(), "pubsub-profile-user", dataMap["pubsub_token"])
|
||||
assert.Equal(s.T(), "administrator", dataMap["role"])
|
||||
accounts, ok := dataMap["accounts"].([]interface{})
|
||||
assert.True(s.T(), ok)
|
||||
if assert.Len(s.T(), accounts, 1) {
|
||||
account := accounts[0].(map[string]interface{})
|
||||
assert.Equal(s.T(), "TestAccount", account["name"])
|
||||
assert.Equal(s.T(), "offline", account["availability"])
|
||||
assert.Equal(s.T(), "offline", account["availability_status"])
|
||||
assert.Equal(s.T(), true, account["auto_offline"])
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProfileHandlerTestSuite) TestGet_Unauthorized() {
|
||||
@@ -217,12 +260,7 @@ func (s *ProfileHandlerTestSuite) TestUpdate_Success() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp response.APIResponse
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.True(s.T(), resp.Success)
|
||||
|
||||
dataMap, ok := resp.Data.(map[string]interface{})
|
||||
assert.True(s.T(), ok)
|
||||
dataMap := s.decodeProfileBody(w)
|
||||
assert.Equal(s.T(), "UpdatedName", dataMap["name"])
|
||||
}
|
||||
|
||||
@@ -241,9 +279,8 @@ func (s *ProfileHandlerTestSuite) TestUpdate_SuccessWithEmail() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp response.APIResponse
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.True(s.T(), resp.Success)
|
||||
dataMap := s.decodeProfileBody(w)
|
||||
assert.Equal(s.T(), "newemail@example.com", dataMap["email"])
|
||||
}
|
||||
|
||||
func (s *ProfileHandlerTestSuite) TestUpdate_SuccessWithAvailabilityOffline() {
|
||||
@@ -440,12 +477,7 @@ func (s *ProfileHandlerTestSuite) TestUpdateAvatar_Success() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp response.APIResponse
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.True(s.T(), resp.Success)
|
||||
|
||||
dataMap, ok := resp.Data.(map[string]interface{})
|
||||
assert.True(s.T(), ok)
|
||||
dataMap := s.decodeProfileBody(w)
|
||||
assert.Equal(s.T(), "https://cdn.example.com/new-avatar.png", dataMap["avatar_url"])
|
||||
}
|
||||
|
||||
@@ -570,6 +602,64 @@ func (s *ProfileHandlerTestSuite) TestUpdateAvatar_NilService() {
|
||||
// Nil service causes panic — validates catastrophic failure path
|
||||
}
|
||||
|
||||
// ===================== Chatwoot Profile Serializer Fixtures =====================
|
||||
|
||||
func (s *ProfileHandlerTestSuite) TestSetAvailability_ReturnsChatwootUserSerializer() {
|
||||
body := map[string]interface{}{
|
||||
"profile": map[string]interface{}{
|
||||
"account_id": s.accountID,
|
||||
"availability": "online",
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/v1/profile/availability", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
payload := s.decodeProfileBody(w)
|
||||
assert.Equal(s.T(), "ProfileUser", payload["name"])
|
||||
assert.Equal(s.T(), "administrator", payload["role"])
|
||||
account := s.firstAccountFromProfile(payload)
|
||||
assert.Equal(s.T(), "online", account["availability"])
|
||||
assert.Equal(s.T(), "online", account["availability_status"])
|
||||
}
|
||||
|
||||
func (s *ProfileHandlerTestSuite) TestSetAutoOffline_ReturnsChatwootUserSerializer() {
|
||||
body := map[string]interface{}{
|
||||
"profile": map[string]interface{}{
|
||||
"account_id": s.accountID,
|
||||
"auto_offline": false,
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/v1/profile/auto_offline", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
payload := s.decodeProfileBody(w)
|
||||
account := s.firstAccountFromProfile(payload)
|
||||
assert.Equal(s.T(), false, account["auto_offline"])
|
||||
}
|
||||
|
||||
func (s *ProfileHandlerTestSuite) TestResetAccessToken_RegeneratesTokenInChatwootUserSerializer() {
|
||||
req, _ := http.NewRequest("POST", "/api/v1/profile/reset_access_token", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
payload := s.decodeProfileBody(w)
|
||||
token, ok := payload["access_token"].(string)
|
||||
assert.True(s.T(), ok)
|
||||
assert.NotEmpty(s.T(), token)
|
||||
assert.NotEqual(s.T(), "profile-token-1", token)
|
||||
}
|
||||
|
||||
// ===================== Edge Cases =====================
|
||||
|
||||
func (s *ProfileHandlerTestSuite) TestNewProfileHandler() {
|
||||
@@ -591,9 +681,8 @@ func (s *ProfileHandlerTestSuite) TestGet_ViaHeaderUserID() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp response.APIResponse
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.True(s.T(), resp.Success)
|
||||
dataMap := s.decodeProfileBody(w)
|
||||
assert.Equal(s.T(), "ProfileUser", dataMap["name"])
|
||||
}
|
||||
|
||||
func (s *ProfileHandlerTestSuite) TestGet_ZeroUserIDViaHeader() {
|
||||
@@ -644,12 +733,7 @@ func (s *ProfileHandlerTestSuite) TestUpdate_MultipleFieldsAtOnce() {
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp response.APIResponse
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.True(s.T(), resp.Success)
|
||||
|
||||
dataMap, ok := resp.Data.(map[string]interface{})
|
||||
assert.True(s.T(), ok)
|
||||
dataMap := s.decodeProfileBody(w)
|
||||
assert.Equal(s.T(), "MultiUpdate", dataMap["name"])
|
||||
assert.Equal(s.T(), "multi@example.com", dataMap["email"])
|
||||
assert.Equal(s.T(), "https://example.com/multi.png", dataMap["avatar_url"])
|
||||
@@ -682,4 +766,4 @@ func (s *ProfileHandlerTestSuite) TestUpdate_SoftDeletedUser() {
|
||||
// strconvFormatUint helper for X-User-ID header tests
|
||||
func strconvFormatUint(n uint) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user