feat(profile): send confirmation invitations
This commit is contained in:
@@ -24,6 +24,7 @@ type AgentHandlerTestSuite struct {
|
||||
suite.Suite
|
||||
db *gorm.DB
|
||||
handler *AgentHandler
|
||||
mailer *fakeProfileConfirmationMailer
|
||||
account *model.Account
|
||||
user *model.User
|
||||
}
|
||||
@@ -34,11 +35,13 @@ func (s *AgentHandlerTestSuite) SetupSuite() {
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
s.Require().NoError(db.AutoMigrate(&model.Account{}, &model.User{}, &model.AccountUser{}))
|
||||
s.Require().NoError(db.AutoMigrate(&model.Account{}, &model.User{}, &model.AccountUser{}, &model.InstallationConfig{}))
|
||||
s.db = db
|
||||
|
||||
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"}
|
||||
@@ -58,6 +61,7 @@ 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() {
|
||||
@@ -188,6 +192,29 @@ func (s *AgentHandlerTestSuite) TestCreateAgent() {
|
||||
assert.Equal(s.T(), s.user.ID, membership.InvitedBy)
|
||||
}
|
||||
|
||||
func (s *AgentHandlerTestSuite) TestCreateAgentSendsWorkspaceInvitation() {
|
||||
req := service.CreateAgentRequest{Email: "invite-mail@test.com", Name: "Invite Mail", 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 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)
|
||||
}
|
||||
|
||||
func (s *AgentHandlerTestSuite) TestCreateAgentDefaultsBlankNameFromEmail() {
|
||||
req := map[string]interface{}{
|
||||
"agent": map[string]interface{}{
|
||||
|
||||
@@ -2,6 +2,7 @@ package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -34,6 +36,7 @@ type ProfileHandlerTestSuite struct {
|
||||
router *gin.Engine
|
||||
handler *ProfileHandler
|
||||
db *gorm.DB
|
||||
mailer *fakeProfileConfirmationMailer
|
||||
user *model.User
|
||||
account *model.Account
|
||||
|
||||
@@ -100,6 +103,8 @@ func (s *ProfileHandlerTestSuite) SetupSuite() {
|
||||
accessTokenRepo := repository.NewAccessTokenRepo(db)
|
||||
installationConfigRepo := repository.NewInstallationConfigRepo(db)
|
||||
profileSvc := service.NewProfileService(userRepo, accountUserRepo, accessTokenRepo, installationConfigRepo)
|
||||
s.mailer = &fakeProfileConfirmationMailer{}
|
||||
profileSvc.SetConfirmationMailer(s.mailer)
|
||||
s.handler = NewProfileHandler(profileSvc)
|
||||
|
||||
// Build router with profile routes and auth middleware
|
||||
@@ -135,26 +140,49 @@ func (s *ProfileHandlerTestSuite) SetupTest() {
|
||||
s.Require().NoError(err)
|
||||
// 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",
|
||||
"password": passwordDigest,
|
||||
"password_digest": passwordDigest,
|
||||
"avatar_url": "",
|
||||
"available": false,
|
||||
"display_name": "Profile Display",
|
||||
"message_signature": "Regards",
|
||||
"pubsub_token": "pubsub-profile-user",
|
||||
"name": "ProfileUser",
|
||||
"email": "profile@example.com",
|
||||
"password": passwordDigest,
|
||||
"password_digest": passwordDigest,
|
||||
"avatar_url": "",
|
||||
"available": false,
|
||||
"display_name": "Profile Display",
|
||||
"message_signature": "Regards",
|
||||
"pubsub_token": "pubsub-profile-user",
|
||||
"confirmation_token": "",
|
||||
"unconfirmed_email": "",
|
||||
})
|
||||
s.db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", s.accountID, s.userID).Updates(map[string]interface{}{
|
||||
s.db.Model(&model.User{}).Where("id = ?", s.userID).UpdateColumns(map[string]interface{}{
|
||||
"confirmed_at": nil,
|
||||
"confirmation_sent_at": nil,
|
||||
"reset_password_token": "",
|
||||
"reset_password_sent_at": nil,
|
||||
})
|
||||
s.db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", s.accountID, s.userID).UpdateColumns(map[string]interface{}{
|
||||
"role": "administrator",
|
||||
"custom_role_id": 0,
|
||||
"availability": "offline",
|
||||
"auto_offline": true,
|
||||
"inviter_id": 0,
|
||||
})
|
||||
s.db.Unscoped().Where("account_id = ?", s.accountID).Delete(&model.CustomRole{})
|
||||
s.db.Unscoped().Where("owner_type = ? AND owner_id = ?", model.AccessTokenOwnerTypeUser, s.userID).Delete(&model.AccessToken{})
|
||||
s.db.Unscoped().Where("name = ?", "CHATWOOT_INBOX_HMAC_KEY").Delete(&model.InstallationConfig{})
|
||||
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)
|
||||
s.mailer.Reset()
|
||||
}
|
||||
|
||||
type fakeProfileConfirmationMailer struct {
|
||||
calls []service.ProfileConfirmationMailRequest
|
||||
}
|
||||
|
||||
func (m *fakeProfileConfirmationMailer) SendConfirmationInstructions(_ context.Context, req service.ProfileConfirmationMailRequest) error {
|
||||
m.calls = append(m.calls, req)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeProfileConfirmationMailer) Reset() {
|
||||
m.calls = nil
|
||||
}
|
||||
|
||||
func (s *ProfileHandlerTestSuite) decodeProfileBody(w *httptest.ResponseRecorder) map[string]interface{} {
|
||||
@@ -724,6 +752,70 @@ func (s *ProfileHandlerTestSuite) TestResetAccessToken_RegeneratesTokenInChatwoo
|
||||
assert.NotEqual(s.T(), "profile-token-1", token)
|
||||
}
|
||||
|
||||
func (s *ProfileHandlerTestSuite) TestResendConfirmation_DoesNotSendForConfirmedUser() {
|
||||
now := time.Now().UTC()
|
||||
s.Require().NoError(s.db.Model(&model.User{}).Where("id = ?", s.userID).Update("confirmed_at", now).Error)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/v1/profile/resend_confirmation", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusNoContent, w.Code)
|
||||
assert.Empty(s.T(), s.mailer.calls)
|
||||
}
|
||||
|
||||
func (s *ProfileHandlerTestSuite) TestResendConfirmation_SendsConfirmationInstructions() {
|
||||
req, _ := http.NewRequest("POST", "/api/v1/profile/resend_confirmation", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusNoContent, w.Code)
|
||||
s.Require().Len(s.mailer.calls, 1)
|
||||
mail := s.mailer.calls[0]
|
||||
assert.Equal(s.T(), "confirmation", mail.Kind)
|
||||
assert.Equal(s.T(), "profile@example.com", mail.ToEmail)
|
||||
assert.Equal(s.T(), "Confirm your email to get started", mail.Heading)
|
||||
assert.Equal(s.T(), "Confirm my account", mail.ActionText)
|
||||
assert.Contains(s.T(), mail.ActionURL, "/app/auth/confirmation?confirmation_token=")
|
||||
assert.NotEmpty(s.T(), mail.ConfirmationToken)
|
||||
assert.Empty(s.T(), mail.ResetPasswordToken)
|
||||
|
||||
var user model.User
|
||||
s.Require().NoError(s.db.First(&user, s.userID).Error)
|
||||
assert.Equal(s.T(), mail.ConfirmationToken, user.ConfirmationToken)
|
||||
assert.NotNil(s.T(), user.ConfirmationSentAt)
|
||||
}
|
||||
|
||||
func (s *ProfileHandlerTestSuite) TestResendConfirmation_SendsWorkspaceInvitationForInvitedUser() {
|
||||
inviter := &model.User{Name: "Inviter Admin", Email: "inviter-profile@example.com", Provider: "email", Active: true}
|
||||
s.Require().NoError(s.db.Create(inviter).Error)
|
||||
s.Require().NoError(s.db.Model(&model.AccountUser{}).
|
||||
Where("account_id = ? AND user_id = ?", s.accountID, s.userID).
|
||||
Update("inviter_id", inviter.ID).Error)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/v1/profile/resend_confirmation", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusNoContent, w.Code)
|
||||
s.Require().Len(s.mailer.calls, 1)
|
||||
mail := s.mailer.calls[0]
|
||||
assert.Equal(s.T(), "invitation", mail.Kind)
|
||||
assert.Equal(s.T(), "Workspace invitation", mail.Eyebrow)
|
||||
assert.Equal(s.T(), "You're invited to join TestAccount", mail.Heading)
|
||||
assert.Equal(s.T(), "Inviter Admin invited you to join the TestAccount 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.Empty(s.T(), mail.ConfirmationToken)
|
||||
assert.NotEmpty(s.T(), mail.ResetPasswordToken)
|
||||
|
||||
var user model.User
|
||||
s.Require().NoError(s.db.First(&user, s.userID).Error)
|
||||
assert.NotEmpty(s.T(), user.ResetPasswordToken)
|
||||
assert.NotEqual(s.T(), mail.ResetPasswordToken, user.ResetPasswordToken)
|
||||
assert.NotNil(s.T(), user.ResetPasswordSentAt)
|
||||
}
|
||||
|
||||
// ===================== Edge Cases =====================
|
||||
|
||||
func (s *ProfileHandlerTestSuite) TestNewProfileHandler() {
|
||||
|
||||
Reference in New Issue
Block a user