* H-16: align takeover with channel AI workflow (#2) * feat(conversations): complete manual AI takeover * fix(conversations): align AI takeover flow with channel AI * fix(conversations): close takeover review gaps --------- Co-authored-by: Rogee <rogee@ipao.vip> * feat(shangwutong): sync customer names back to channel (#3) Co-authored-by: Rogee <rogee@ipao.vip> * fix(shangwutong): close contact sync review gaps (#4) Co-authored-by: Rogee <rogee@ipao.vip> * H-28: harden Shangwutong CID sync (#5) * fix(shangwutong): close contact sync review gaps * fix(shangwutong): harden CID sync boundaries --------- Co-authored-by: Rogee <rogee@ipao.vip> * fix(conversations): sync AI takeover exit in realtime (#6) Co-authored-by: Rogee <rogee@ipao.vip> * test(shangwutong): cover CID rename reliability (#7) Co-authored-by: Rogee <rogee@ipao.vip> * H-43: fix WEB Captain takeover E2E flow (#8) * test(shangwutong): cover CID rename reliability * H-43: fix WEB Captain takeover flow * H-48: preserve compatible provider model * H-49: make Captain takeover atomic * H-50: prevent duplicate widget initialization --------- Co-authored-by: Rogee <rogee@ipao.vip> * H-55: make Captain bindings atomic (#9) Co-authored-by: Rogee <rogee@ipao.vip> * H-60: harden Captain migration rollback and concurrency * chore(agent): baseline — uncommitted work from the local directory * H-335: add safe Captain skills and user deactivation * H-338: close auth and Captain review blockers * H-338: close assignment and session races * H-338: close assignment and websocket invalidation gaps * H-338: enforce assignment write invariants --------- Co-authored-by: Rogee <rogee@ipao.vip>
110 lines
4.6 KiB
Go
110 lines
4.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"github.com/gochat/gochat/internal/auth"
|
|
"github.com/gochat/gochat/internal/config"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/pkg/crypto"
|
|
)
|
|
|
|
func setupAuthServiceTest(t *testing.T) (*AuthService, *gorm.DB, *model.User) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.User{}, &model.AccountUser{}, &model.UserSession{}))
|
|
account := &model.Account{Name: "Auth Service", Status: "active"}
|
|
require.NoError(t, db.Create(account).Error)
|
|
passwordDigest, err := crypto.HashPassword("oldpassword")
|
|
require.NoError(t, err)
|
|
user := &model.User{AccountID: account.ID, Name: "Auth User", Email: "auth-service@example.com", Password: passwordDigest, PasswordDigest: passwordDigest, Provider: "email", Active: true}
|
|
require.NoError(t, db.Create(user).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "administrator"}).Error)
|
|
jwtCfg := &config.JWTConfig{Secret: "auth-service-secret", ExpiryHours: 1, RefreshExpiryHours: 24}
|
|
return NewAuthService(db, auth.NewJWTService(jwtCfg), auth.NewRefreshTokenStore(nil, jwtCfg)), db, user
|
|
}
|
|
|
|
func TestAuthService_ResetPasswordStoresDigestToken(t *testing.T) {
|
|
svc, db, user := setupAuthServiceTest(t)
|
|
|
|
require.NoError(t, svc.ResetPassword(context.Background(), &ResetPasswordInput{Email: " AUTH-SERVICE@example.com "}))
|
|
|
|
var updated model.User
|
|
require.NoError(t, db.First(&updated, user.ID).Error)
|
|
require.NotEmpty(t, updated.ResetPasswordToken)
|
|
require.NotEqual(t, updated.ResetPasswordToken, "AUTH-SERVICE@example.com")
|
|
require.NotNil(t, updated.ResetPasswordSentAt)
|
|
}
|
|
|
|
func TestAuthService_ConfirmResetPasswordUpdatesPasswordAndIssuesTokens(t *testing.T) {
|
|
svc, db, user := setupAuthServiceTest(t)
|
|
rawToken := "reset-token-123"
|
|
sentAt := time.Now().UTC()
|
|
require.NoError(t, db.Model(user).Updates(map[string]interface{}{
|
|
"reset_password_token": digestAuthToken(rawToken),
|
|
"reset_password_sent_at": sentAt,
|
|
}).Error)
|
|
|
|
output, err := svc.ConfirmResetPassword(context.Background(), &ConfirmResetPasswordInput{Token: rawToken, Password: "newpassword", PasswordConfirmation: "newpassword"})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, output.TokenPair.AccessToken)
|
|
|
|
var updated model.User
|
|
require.NoError(t, db.First(&updated, user.ID).Error)
|
|
require.True(t, crypto.CheckPassword("newpassword", updated.PasswordDigest))
|
|
require.Empty(t, updated.ResetPasswordToken)
|
|
require.Nil(t, updated.ResetPasswordSentAt)
|
|
require.NotNil(t, updated.ConfirmedAt)
|
|
}
|
|
|
|
func TestAuthService_ConfirmEmailConfirmsAndIssuesTokens(t *testing.T) {
|
|
svc, db, user := setupAuthServiceTest(t)
|
|
require.NoError(t, db.Model(user).Updates(map[string]interface{}{
|
|
"confirmation_token": "confirm-token-123",
|
|
"confirmed_at": nil,
|
|
}).Error)
|
|
|
|
output, err := svc.ConfirmEmail(context.Background(), &ConfirmEmailInput{Token: "confirm-token-123"})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, output.TokenPair.AccessToken)
|
|
|
|
var updated model.User
|
|
require.NoError(t, db.First(&updated, user.ID).Error)
|
|
require.NotNil(t, updated.ConfirmedAt)
|
|
require.Empty(t, updated.ConfirmationToken)
|
|
}
|
|
|
|
func TestAuthServiceValidateAccessTokenRejectsInactiveUser(t *testing.T) {
|
|
svc, db, user := setupAuthServiceTest(t)
|
|
pair, err := svc.jwtService.GenerateTokenPair(user, user.AccountID, "administrator")
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.Model(user).Update("active", false).Error)
|
|
|
|
_, err = svc.ValidateAccessToken(context.Background(), pair.AccessToken)
|
|
require.ErrorContains(t, err, "inactive")
|
|
}
|
|
|
|
func TestAuthServiceRefreshRejectsTokenLeftInStoreAfterDeactivation(t *testing.T) {
|
|
svc, db, user := setupAuthServiceTest(t)
|
|
output := &LoginOutput{User: user, AccountID: user.AccountID, Role: "administrator"}
|
|
require.NoError(t, svc.TrackChatwootSession(context.Background(), output, "browser", "127.0.0.1", "test"))
|
|
oldRefresh := output.TokenPair.RefreshToken
|
|
require.NoError(t, db.Model(user).Update("active", false).Error)
|
|
require.NoError(t, db.Where("user_id = ?", user.ID).Delete(&model.UserSession{}).Error)
|
|
require.NoError(t, db.Model(user).Update("active", true).Error)
|
|
valid, err := svc.refreshStore.ValidateForClient(context.Background(), user.ID, "browser", oldRefresh)
|
|
require.NoError(t, err)
|
|
require.True(t, valid)
|
|
|
|
_, err = svc.Refresh(context.Background(), &RefreshInput{RefreshToken: oldRefresh})
|
|
require.ErrorIs(t, err, auth.ErrSessionRevoked)
|
|
}
|