This commit is contained in:
2026-07-12 12:20:23 +08:00
parent 7f2d5579ff
commit f923791d39
252 changed files with 9872 additions and 647 deletions
@@ -13,6 +13,7 @@ import (
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/service"
pkgcrypto "github.com/gochat/gochat/pkg/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"gorm.io/driver/sqlite"
@@ -24,7 +25,6 @@ type AgentHandlerTestSuite struct {
suite.Suite
db *gorm.DB
handler *AgentHandler
mailer *fakeProfileConfirmationMailer
account *model.Account
user *model.User
}
@@ -40,8 +40,6 @@ func (s *AgentHandlerTestSuite) SetupSuite() {
agentRepo := repository.NewAgentRepo(db)
svc := service.NewAgentService(agentRepo, db)
s.mailer = &fakeProfileConfirmationMailer{}
svc.SetConfirmationMailer(s.mailer)
s.handler = NewAgentHandler(svc)
s.account = &model.Account{Name: "test-agent-account"}
@@ -61,7 +59,6 @@ func (s *AgentHandlerTestSuite) SetupTest() {
// Don't delete users — we need the inviter user to persist
// Only delete agent users (not the inviter)
s.db.Exec("DELETE FROM users WHERE id != ?", s.user.ID)
s.mailer.Reset()
}
func (s *AgentHandlerTestSuite) TearDownSuite() {
@@ -186,10 +183,20 @@ func (s *AgentHandlerTestSuite) TestCreateAgent() {
assert.NotContains(s.T(), data, "invited_by")
assert.NotContains(s.T(), data, "inviter_id")
assert.NotContains(s.T(), data, "account_user_id")
temporaryPassword, ok := data["temporary_password"].(string)
s.Require().True(ok)
assert.Len(s.T(), temporaryPassword, 16)
assert.Equal(s.T(), "no-store", w.Header().Get("Cache-Control"))
var membership model.AccountUser
s.Require().NoError(s.db.Where("account_id = ? AND user_id = ?", s.account.ID, uint(data["id"].(float64))).First(&membership).Error)
assert.Equal(s.T(), s.user.ID, membership.InvitedBy)
var createdUser model.User
s.Require().NoError(s.db.First(&createdUser, uint(data["id"].(float64))).Error)
assert.NotNil(s.T(), createdUser.ConfirmedAt)
assert.True(s.T(), pkgcrypto.CheckPassword(temporaryPassword, createdUser.PasswordDigest))
assert.True(s.T(), pkgcrypto.CheckPassword(temporaryPassword, createdUser.Password))
}
func (s *AgentHandlerTestSuite) TestCreateAgentChatwootFrontendPayload() {
@@ -218,27 +225,63 @@ func (s *AgentHandlerTestSuite) TestCreateAgentChatwootFrontendPayload() {
assert.NotContains(s.T(), data, "data")
}
func (s *AgentHandlerTestSuite) TestCreateAgentSendsWorkspaceInvitation() {
req := service.CreateAgentRequest{Email: "invite-mail@test.com", Name: "Invite Mail", Role: "agent"}
func (s *AgentHandlerTestSuite) TestCreateAgentDoesNotRequireInvitationEmail() {
req := service.CreateAgentRequest{Email: "direct-login@test.com", Name: "Direct Login", Role: "agent"}
w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID)
s.handler.Create(c)
assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String())
s.Require().Len(s.mailer.calls, 1)
mail := s.mailer.calls[0]
assert.Equal(s.T(), "invitation", mail.Kind)
assert.Equal(s.T(), "invite-mail@test.com", mail.ToEmail)
assert.Equal(s.T(), "You're invited to join test-agent-account", mail.Heading)
assert.Equal(s.T(), "Inviter Admin invited you to join the test-agent-account workspace on Chatwoot.", mail.IntroText)
assert.Equal(s.T(), "Accept invitation", mail.ActionText)
assert.Contains(s.T(), mail.ActionURL, "/app/auth/password/edit?reset_password_token=")
assert.NotEmpty(s.T(), mail.ResetPasswordToken)
var data map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &data))
temporaryPassword, ok := data["temporary_password"].(string)
s.Require().True(ok)
assert.Regexp(s.T(), `[A-Z]`, temporaryPassword)
assert.Regexp(s.T(), `[a-z]`, temporaryPassword)
assert.Regexp(s.T(), `[0-9]`, temporaryPassword)
assert.Regexp(s.T(), `[!@#$%]`, temporaryPassword)
var invited model.User
s.Require().NoError(s.db.Where("email = ?", "invite-mail@test.com").First(&invited).Error)
assert.NotEmpty(s.T(), invited.ResetPasswordToken)
assert.NotEqual(s.T(), mail.ResetPasswordToken, invited.ResetPasswordToken)
assert.NotNil(s.T(), invited.ResetPasswordSentAt)
var created model.User
s.Require().NoError(s.db.Where("email = ?", "direct-login@test.com").First(&created).Error)
assert.Empty(s.T(), created.ResetPasswordToken)
assert.Nil(s.T(), created.ResetPasswordSentAt)
assert.NotNil(s.T(), created.ConfirmedAt)
assert.True(s.T(), pkgcrypto.CheckPassword(temporaryPassword, created.PasswordDigest))
}
func (s *AgentHandlerTestSuite) TestResetPasswordReturnsNewTemporaryPassword() {
req := service.CreateAgentRequest{Email: "reset-agent@test.com", Name: "Reset Agent", Role: "agent"}
createResponse, createContext := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID)
s.handler.Create(createContext)
s.Require().Equal(http.StatusOK, createResponse.Code)
var created map[string]interface{}
s.Require().NoError(json.Unmarshal(createResponse.Body.Bytes(), &created))
agentID := uint(created["id"].(float64))
initialPassword := created["temporary_password"].(string)
w, c := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/1/agents/%d/reset_password", agentID), nil, s.account.ID, s.user.ID)
s.handler.ResetPassword(c)
assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String())
assert.Equal(s.T(), "no-store", w.Header().Get("Cache-Control"))
var data map[string]string
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &data))
newPassword := data["temporary_password"]
assert.Len(s.T(), newPassword, 16)
assert.NotEqual(s.T(), initialPassword, newPassword)
var user model.User
s.Require().NoError(s.db.First(&user, agentID).Error)
assert.True(s.T(), pkgcrypto.CheckPassword(newPassword, user.PasswordDigest))
assert.False(s.T(), pkgcrypto.CheckPassword(initialPassword, user.PasswordDigest))
}
func (s *AgentHandlerTestSuite) TestResetPasswordIsScopedToAccount() {
otherAccount := &model.Account{Name: "other-reset-account"}
s.Require().NoError(s.db.Create(otherAccount).Error)
w, c := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/agents/%d/reset_password", otherAccount.ID, s.user.ID), nil, otherAccount.ID, s.user.ID)
s.handler.ResetPassword(c)
assert.Equal(s.T(), http.StatusNotFound, w.Code, w.Body.String())
}
func (s *AgentHandlerTestSuite) TestCreateAgentDefaultsBlankNameFromEmail() {
@@ -467,7 +510,12 @@ func (s *AgentHandlerTestSuite) TestBulkCreate() {
s.handler.BulkCreate(c)
assert.Equal(s.T(), http.StatusOK, w.Code)
assert.Empty(s.T(), w.Body.String())
var data []map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &data))
s.Require().Len(data, 3)
for _, agent := range data {
assert.NotEmpty(s.T(), agent["temporary_password"])
}
}
func (s *AgentHandlerTestSuite) TestBulkCreateValidation() {
@@ -477,7 +525,7 @@ func (s *AgentHandlerTestSuite) TestBulkCreateValidation() {
w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents/bulk_create", req, s.account.ID, s.user.ID)
s.handler.BulkCreate(c)
assert.Equal(s.T(), http.StatusOK, w.Code)
assert.Empty(s.T(), w.Body.String())
assert.JSONEq(s.T(), `[]`, w.Body.String())
}
func (s *AgentHandlerTestSuite) TestBulkCreateSkipsInvalidEmailsAndClearsOnboardingStep() {
@@ -489,7 +537,10 @@ func (s *AgentHandlerTestSuite) TestBulkCreateSkipsInvalidEmailsAndClearsOnboard
s.handler.BulkCreate(c)
assert.Equal(s.T(), http.StatusOK, w.Code)
assert.Empty(s.T(), w.Body.String())
var data []map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &data))
s.Require().Len(data, 1)
assert.NotEmpty(s.T(), data[0]["temporary_password"])
var validUser model.User
s.Require().NoError(s.db.Where("email = ?", "valid-bulk@test.com").First(&validUser).Error)
@@ -521,7 +572,11 @@ func (s *AgentHandlerTestSuite) TestBulkCreateSkipsDuplicates() {
s.handler.BulkCreate(c2)
assert.Equal(s.T(), http.StatusOK, w2.Code)
assert.Empty(s.T(), w2.Body.String())
var data []map[string]interface{}
s.Require().NoError(json.Unmarshal(w2.Body.Bytes(), &data))
s.Require().Len(data, 1)
assert.Equal(s.T(), "new@test.com", data[0]["email"])
assert.NotEmpty(s.T(), data[0]["temporary_password"])
}
func (s *AgentHandlerTestSuite) TestListAfterCreate() {