Files
gochat/backend/internal/repository/account_user_agent_test.go
T
Rogeeandrogee 6c78820a1f H-338: close H-335 release blockers (#59)
* 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>
2026-08-20 10:21:19 +08:00

499 lines
18 KiB
Go

package repository
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
)
// ---------------------------------------------------------------------------
// AccountUserRepo
// ---------------------------------------------------------------------------
func TestAccountUserRepo_FindByAccountAndUser(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
au := &model.AccountUser{AccountID: 10, UserID: 20, Role: "agent"}
require.NoError(t, db.Create(au).Error)
found, err := repo.FindByAccountAndUser(ctx, 10, 20)
require.NoError(t, err)
require.NotNil(t, found)
assert.Equal(t, au.ID, found.ID)
}
func TestAccountUserRepo_FindByAccountAndUser_NotFound(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
found, err := repo.FindByAccountAndUser(ctx, 9999, 9999)
require.NoError(t, err)
assert.Nil(t, found)
}
func TestAccountUserRepo_FindOnlineAgentsByAccount(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
for id := uint(1); id <= 3; id++ {
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: id}, AccountID: 1, Name: "Agent", Email: fmt.Sprintf("agent-%d@example.com", id), Password: "hash", Active: true}).Error)
}
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 1, Role: "agent", Availability: "online"}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 2, Role: "agent", Availability: "offline"}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 3, Role: "agent", Availability: "online"}).Error)
agents, err := repo.FindOnlineAgentsByAccount(ctx, 1)
require.NoError(t, err)
assert.Len(t, agents, 2)
require.NoError(t, db.Model(&model.User{}).Where("id = ?", 3).Update("active", false).Error)
agents, err = repo.FindOnlineAgentsByAccount(ctx, 1)
require.NoError(t, err)
assert.Len(t, agents, 1)
}
func TestAccountUserRepo_FindByAccount(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 1, Role: "agent"}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 2, Role: "admin"}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 2, UserID: 3, Role: "agent"}).Error)
agents, total, err := repo.FindByAccount(ctx, 1, 0, 10)
require.NoError(t, err)
assert.Equal(t, int64(2), total)
assert.Len(t, agents, 2)
// Pagination
agents, total, err = repo.FindByAccount(ctx, 1, 0, 1)
require.NoError(t, err)
assert.Equal(t, int64(2), total)
assert.Len(t, agents, 1)
}
func TestAccountUserRepo_UpdateAvailability(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 10, Role: "agent", Availability: "offline"}).Error)
require.NoError(t, repo.UpdateAvailability(ctx, 1, 10, "online"))
au, err := repo.FindByAccountAndUser(ctx, 1, 10)
require.NoError(t, err)
assert.Equal(t, "online", au.Availability)
}
func TestAccountUserRepo_IsAdministrator(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: 10}, Email: "admin@example.com", Active: true}).Error)
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: 20}, Email: "agent@example.com", Active: true}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 10, Role: "administrator"}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 20, Role: "agent"}).Error)
isAdmin, err := repo.IsAdministrator(ctx, 1, 10)
require.NoError(t, err)
assert.True(t, isAdmin)
isAdmin, err = repo.IsAdministrator(ctx, 1, 20)
require.NoError(t, err)
assert.False(t, isAdmin)
}
func TestAccountUserRepo_IsAgentOrAdmin(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: 10}, AccountID: 1, Name: "Admin", Email: "admin@example.com", Password: "hash", Active: true}).Error)
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: 20}, AccountID: 1, Name: "Agent", Email: "agent@example.com", Password: "hash", Active: true}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 10, Role: "administrator"}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 20, Role: "agent"}).Error)
isAgentOrAdmin, err := repo.IsAgentOrAdmin(ctx, 1, 10)
require.NoError(t, err)
assert.True(t, isAgentOrAdmin)
isAgentOrAdmin, err = repo.IsAgentOrAdmin(ctx, 1, 20)
require.NoError(t, err)
assert.True(t, isAgentOrAdmin)
require.NoError(t, db.Model(&model.User{}).Where("id = ?", 20).Update("active", false).Error)
isAgentOrAdmin, err = repo.IsAgentOrAdmin(ctx, 1, 20)
require.NoError(t, err)
assert.False(t, isAgentOrAdmin)
isAgentOrAdmin, err = repo.IsAgentOrAdmin(ctx, 1, 9999)
require.NoError(t, err)
assert.False(t, isAgentOrAdmin)
}
func TestAccountUserRepo_FindTeamMembersByAccount(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
require.NoError(t, db.Create(&model.Team{AccountID: 1, Name: "T"}).Error)
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: 10}, AccountID: 1, Name: "Agent 10", Email: "agent-10@example.com", Password: "hash", Active: true}).Error)
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: 20}, AccountID: 1, Name: "Agent 20", Email: "agent-20@example.com", Password: "hash", Active: true}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 10, Role: "agent"}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 20, Role: "agent"}).Error)
require.NoError(t, db.Create(&model.TeamMember{TeamID: 1, UserID: 10}).Error)
require.NoError(t, db.Create(&model.TeamMember{TeamID: 1, UserID: 20}).Error)
members, err := repo.FindTeamMembersByAccount(ctx, 1, 1)
require.NoError(t, err)
assert.Len(t, members, 2)
}
func TestAccountUserRepo_UpdateAutoOffline(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
require.NoError(t, db.Create(&model.AccountUser{AccountID: 1, UserID: 10, Role: "agent", AutoOffline: true}).Error)
require.NoError(t, repo.UpdateAutoOffline(ctx, 1, 10, false))
au, err := repo.FindByAccountAndUser(ctx, 1, 10)
require.NoError(t, err)
assert.False(t, au.AutoOffline)
}
func TestAccountUserRepo_FindByAccountAndUserOrFail(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: 10, Role: "agent"}).Error)
au, err := repo.FindByAccountAndUserOrFail(ctx, account.ID, 10)
require.NoError(t, err)
assert.Equal(t, "agent", au.Role)
assert.NotNil(t, au.Account) // Preloaded
}
func TestAccountUserRepo_FindByAccountAndUserOrFail_NotFound(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
_, err := repo.FindByAccountAndUserOrFail(ctx, 9999, 9999)
assert.ErrorIs(t, err, gorm.ErrRecordNotFound)
}
func TestAccountUserRepo_FindByUserWithAccounts(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
account1 := &model.Account{Name: "A1"}
require.NoError(t, db.Create(account1).Error)
account2 := &model.Account{Name: "A2"}
require.NoError(t, db.Create(account2).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account1.ID, UserID: 10, Role: "agent"}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account2.ID, UserID: 10, Role: "admin"}).Error)
result, err := repo.FindByUserWithAccounts(ctx, 10)
require.NoError(t, err)
assert.Len(t, result, 2)
assert.NotNil(t, result[0].Account) // Preloaded
}
func TestAccountUserRepo_Create(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
au := &model.AccountUser{AccountID: 1, UserID: 10, Role: "agent"}
err := repo.Create(ctx, au)
require.NoError(t, err)
assert.NotZero(t, au.ID)
}
func TestAccountUserRepo_Delete(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
au := &model.AccountUser{AccountID: 1, UserID: 10, Role: "agent"}
require.NoError(t, db.Create(au).Error)
require.NoError(t, repo.Delete(ctx, au.ID))
found, err := repo.FindByAccountAndUser(ctx, 1, 10)
require.NoError(t, err)
assert.Nil(t, found)
}
func TestAccountUserRepo_UpdateRole(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAccountUserRepo(db)
au := &model.AccountUser{AccountID: 1, UserID: 10, Role: "agent"}
require.NoError(t, db.Create(au).Error)
require.NoError(t, repo.UpdateRole(ctx, au.ID, "administrator"))
found, err := repo.FindByAccountAndUser(ctx, 1, 10)
require.NoError(t, err)
assert.Equal(t, "administrator", found.Role)
}
// ---------------------------------------------------------------------------
// AgentRepo
// ---------------------------------------------------------------------------
func TestAgentRepo_ListByAccount(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
user1 := &model.User{Name: "Alice", Email: "alice@e.com", Password: "p", AccountID: account.ID}
require.NoError(t, db.Create(user1).Error)
user2 := &model.User{Name: "Bob", Email: "bob@e.com", Password: "p", AccountID: account.ID}
require.NoError(t, db.Create(user2).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user1.ID, Role: "agent"}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user2.ID, Role: "admin"}).Error)
agents, total, err := repo.ListByAccount(ctx, account.ID, 0, 10)
require.NoError(t, err)
assert.Equal(t, int64(2), total)
assert.Len(t, agents, 2)
// Test pagination
agents, total, err = repo.ListByAccount(ctx, account.ID, 1, 1)
require.NoError(t, err)
assert.Equal(t, int64(2), total)
assert.Len(t, agents, 1)
}
func TestAgentRepo_ListByAccount_Empty(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
agents, total, err := repo.ListByAccount(ctx, 9999, 0, 10)
require.NoError(t, err)
assert.Equal(t, int64(0), total)
assert.Empty(t, agents)
}
func TestAgentRepo_FindAgentByID(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
user := &model.User{Name: "Alice", Email: "alice@e.com", Password: "p", AccountID: account.ID}
require.NoError(t, db.Create(user).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "agent", Availability: "online"}).Error)
agent, err := repo.FindAgentByID(ctx, user.ID, account.ID)
require.NoError(t, err)
assert.Equal(t, user.Name, agent.Name)
assert.Equal(t, "agent", agent.Role)
assert.Equal(t, "online", agent.Availability)
}
func TestAgentRepo_FindAgentByID_NotFound(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
_, err := repo.FindAgentByID(ctx, 9999, 9999)
assert.ErrorIs(t, err, gorm.ErrRecordNotFound)
}
func TestAgentRepo_CreateAgent_NewUser(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
confirmedAt := time.Now()
agent, err := repo.CreateAgent(ctx, account.ID, 1, "John", "john@e.com", "agent", "online", true, 0, "hashedpw", confirmedAt)
require.NoError(t, err)
assert.Equal(t, "John", agent.Name)
assert.Equal(t, "agent", agent.Role)
assert.True(t, agent.IsNewUser)
}
func TestAgentRepo_CreateAgent_ExistingUser(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
// Create user first
user := &model.User{Name: "John", Email: "john@e.com", Password: "p", AccountID: account.ID}
require.NoError(t, db.Create(user).Error)
confirmedAt := time.Now()
agent, err := repo.CreateAgent(ctx, account.ID, 1, "John", "john@e.com", "agent", "online", true, 0, "hashedpw", confirmedAt)
require.NoError(t, err)
assert.False(t, agent.IsNewUser)
}
func TestAgentRepo_CreateAgent_AlreadyMember(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
user := &model.User{Name: "John", Email: "john@e.com", Password: "p", AccountID: account.ID}
require.NoError(t, db.Create(user).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "agent"}).Error)
confirmedAt := time.Now()
_, err := repo.CreateAgent(ctx, account.ID, 1, "John", "john@e.com", "agent", "online", true, 0, "hashedpw", confirmedAt)
assert.ErrorIs(t, err, ErrAlreadyMember)
}
func TestAgentRepo_CreateAgent_NameFromEmail(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
confirmedAt := time.Now()
agent, err := repo.CreateAgent(ctx, account.ID, 1, "", "john.doe@e.com", "agent", "online", true, 0, "hashedpw", confirmedAt)
require.NoError(t, err)
assert.Equal(t, "john.doe", agent.Name)
}
func TestAgentRepo_UpdateAgent(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
user := &model.User{Name: "John", Email: "john@e.com", Password: "p", AccountID: account.ID}
require.NoError(t, db.Create(user).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "agent", Availability: "offline"}).Error)
agent, err := repo.UpdateAgent(ctx, user.ID, account.ID, "Johnny", "admin", "online", true, true, nil, false)
require.NoError(t, err)
assert.Equal(t, "Johnny", agent.Name)
assert.Equal(t, "admin", agent.Role)
assert.Equal(t, "online", agent.Availability)
assert.True(t, agent.AutoOffline)
}
func TestAgentRepo_UpdateAgent_CustomRole(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
user := &model.User{Name: "John", Email: "john@e.com", Password: "p", AccountID: account.ID}
require.NoError(t, db.Create(user).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "agent"}).Error)
roleID := uint(5)
agent, err := repo.UpdateAgent(ctx, user.ID, account.ID, "", "", "", false, false, &roleID, true)
require.NoError(t, err)
assert.Equal(t, roleID, agent.CustomRoleID)
}
func TestAgentRepo_UpdateAgent_SetCustomRoleToZero(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
user := &model.User{Name: "John", Email: "john@e.com", Password: "p", AccountID: account.ID}
require.NoError(t, db.Create(user).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "agent", CustomRoleID: 5}).Error)
agent, err := repo.UpdateAgent(ctx, user.ID, account.ID, "", "", "", false, false, nil, true)
require.NoError(t, err)
assert.Equal(t, uint(0), agent.CustomRoleID)
}
func TestAgentRepo_DeleteAgent(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
user := &model.User{Name: "John", Email: "john@e.com", Password: "p", AccountID: account.ID}
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, repo.DeleteAgent(ctx, user.ID, account.ID))
// AccountUser should be deleted
var count int64
db.Model(&model.AccountUser{}).Where("user_id = ? AND account_id = ?", user.ID, account.ID).Count(&count)
assert.Equal(t, int64(0), count)
// User should also be deleted (no other memberships)
var userCount int64
db.Model(&model.User{}).Where("id = ?", user.ID).Count(&userCount)
assert.Equal(t, int64(0), userCount)
}
func TestAgentRepo_DeleteAgent_NotFound(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
err := repo.DeleteAgent(ctx, 9999, 9999)
assert.ErrorIs(t, err, gorm.ErrRecordNotFound)
}
func TestAgentRepo_CountByAccount(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
repo := NewAgentRepo(db)
account := &model.Account{Name: "A"}
require.NoError(t, db.Create(account).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: 1, Role: "agent"}).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: 2, Role: "agent"}).Error)
count, err := repo.CountByAccount(ctx, account.ID)
require.NoError(t, err)
assert.Equal(t, int64(2), count)
}
func TestIndexOfAt(t *testing.T) {
assert.Equal(t, 4, indexOfAt("test@example.com"))
assert.Equal(t, -1, indexOfAt("noemail"))
}