* 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>
76 lines
3.5 KiB
Go
76 lines
3.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/gochat/gochat/internal/auth"
|
|
"github.com/gochat/gochat/internal/config"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func TestAgentDeactivationPermanentlyRevokesSessionsAndRefreshTokens(t *testing.T) {
|
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.User{}, &model.AccountUser{}, &model.UserSession{}))
|
|
account := &model.Account{Name: "Account"}
|
|
user := &model.User{Name: "Agent", Email: "agent@example.com", Provider: "email", Active: true}
|
|
require.NoError(t, db.Create(account).Error)
|
|
require.NoError(t, db.Create(user).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "agent"}).Error)
|
|
require.NoError(t, db.Create(&model.UserSession{UserID: user.ID, ClientID: "browser"}).Error)
|
|
|
|
store := auth.NewRefreshTokenStore(nil, &config.JWTConfig{RefreshExpiryHours: 24})
|
|
require.NoError(t, store.StoreForClient(context.Background(), user.ID, "browser", "old-refresh"))
|
|
disconnected := uint(0)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db).WithDeactivation(store, func(userID uint) { disconnected = userID })
|
|
inactive := false
|
|
_, err = svc.Update(context.Background(), user.ID, account.ID, UpdateAgentRequest{Active: &inactive})
|
|
require.NoError(t, err)
|
|
require.Equal(t, user.ID, disconnected)
|
|
var sessions int64
|
|
require.NoError(t, db.Model(&model.UserSession{}).Where("user_id = ?", user.ID).Count(&sessions).Error)
|
|
require.Zero(t, sessions)
|
|
|
|
active := true
|
|
_, err = svc.Update(context.Background(), user.ID, account.ID, UpdateAgentRequest{Active: &active})
|
|
require.NoError(t, err)
|
|
valid, err := store.ValidateForClient(context.Background(), user.ID, "browser", "old-refresh")
|
|
require.NoError(t, err)
|
|
require.False(t, valid)
|
|
}
|
|
|
|
func TestAgentDeactivationRollsBackWhenRefreshRevocationFails(t *testing.T) {
|
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.User{}, &model.AccountUser{}, &model.UserSession{}))
|
|
account := &model.Account{Name: "Account"}
|
|
user := &model.User{Name: "Agent", Email: "rollback@example.com", Provider: "email", Active: true}
|
|
require.NoError(t, db.Create(account).Error)
|
|
require.NoError(t, db.Create(user).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "agent"}).Error)
|
|
require.NoError(t, db.Create(&model.UserSession{UserID: user.ID, ClientID: "browser"}).Error)
|
|
|
|
mr := miniredis.RunT(t)
|
|
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
|
store := auth.NewRefreshTokenStore(rdb, &config.JWTConfig{RefreshExpiryHours: 24})
|
|
require.NoError(t, store.StoreForClient(context.Background(), user.ID, "browser", "old-refresh"))
|
|
require.NoError(t, rdb.Close())
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db).WithDeactivation(store, nil)
|
|
inactive := false
|
|
_, err = svc.Update(context.Background(), user.ID, account.ID, UpdateAgentRequest{Active: &inactive})
|
|
require.Error(t, err)
|
|
|
|
require.NoError(t, db.First(user, user.ID).Error)
|
|
require.True(t, user.Active)
|
|
var sessions int64
|
|
require.NoError(t, db.Model(&model.UserSession{}).Where("user_id = ?", user.ID).Count(&sessions).Error)
|
|
require.EqualValues(t, 1, sessions)
|
|
}
|