feat(profile): align chatwoot user serializer

This commit is contained in:
2026-06-05 01:07:40 +08:00
parent 6757e17c82
commit 56a8c5b806
9 changed files with 483 additions and 105 deletions
+124 -40
View File
@@ -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)
}
}