Files
2026-06-28 15:00:54 +08:00

486 lines
14 KiB
Go

package chat
import (
"errors"
"strings"
"testing"
"time"
"hfb_sys/backend/internal/model"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func setupQrCodeTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("无法创建测试数据库: %v", err)
}
if err := db.AutoMigrate(
&model.ChatQrCode{},
&model.ChatConversation{},
&model.ChatParticipant{},
&model.ChatMessage{},
&model.ChatQrCodeDeliveryTask{},
&model.ChatSupportGroup{},
&model.ChatSupportGroupMember{},
&model.AdminUser{},
&model.AdminUserRole{},
&model.Role{},
&model.SystemConfig{},
); err != nil {
t.Fatalf("数据库迁移失败: %v", err)
}
return db
}
func TestGetQrCodeOCRConfigReadsSystemConfig(t *testing.T) {
db := setupQrCodeTestDB(t)
repo := NewRepository(db, nil, nil)
rows := []model.SystemConfig{
{Key: qrCodeOCRTokenConfigKey, Value: "test-token"},
{Key: qrCodeOCRJobURLConfigKey, Value: "https://example.test/ocr/jobs"},
{Key: qrCodeOCRModelConfigKey, Value: "PaddleOCR-Test"},
}
if err := db.Create(&rows).Error; err != nil {
t.Fatalf("创建系统配置失败: %v", err)
}
config, err := repo.GetQrCodeOCRConfig(t.Context())
if err != nil {
t.Fatalf("读取 OCR 配置失败: %v", err)
}
if config.Token != "test-token" {
t.Fatalf("token = %q", config.Token)
}
if config.JobURL != "https://example.test/ocr/jobs" {
t.Fatalf("job_url = %q", config.JobURL)
}
if config.Model != "PaddleOCR-Test" {
t.Fatalf("model = %q", config.Model)
}
}
func TestListQrCodesIncludesBoundConversationTitle(t *testing.T) {
db := setupQrCodeTestDB(t)
repo := NewRepository(db, nil, nil)
conversation := model.ChatConversation{
Title: "账号群 L202606180001",
Type: ConversationTypeListingGroup,
Status: "active",
}
if err := db.Create(&conversation).Error; err != nil {
t.Fatalf("创建群聊失败: %v", err)
}
qrcode := model.ChatQrCode{
ImageURL: "/api/files/object?key=qrcode/group.png",
GroupName: "王大锤-鼠鼠跑刀一群",
Status: QrCodeStatusUsed,
ConversationID: &conversation.ID,
CreatedBy: 1,
}
if err := db.Create(&qrcode).Error; err != nil {
t.Fatalf("创建二维码失败: %v", err)
}
items, total, err := repo.ListQrCodes(t.Context(), QrCodeListRequest{Page: 1, Limit: 20})
if err != nil {
t.Fatalf("查询二维码失败: %v", err)
}
if total != 1 || len(items) != 1 {
t.Fatalf("total = %d, len = %d, want 1", total, len(items))
}
if items[0].GroupName != "王大锤-鼠鼠跑刀一群" {
t.Fatalf("群名 = %q", items[0].GroupName)
}
if items[0].BoundConversationTitle != "账号群 L202606180001" {
t.Fatalf("绑定群名 = %q", items[0].BoundConversationTitle)
}
}
func TestListQrCodesFilters(t *testing.T) {
db := setupQrCodeTestDB(t)
repo := NewRepository(db, nil, nil)
now := time.Now()
expiredAt := now.Add(-time.Hour)
validAt := now.Add(7 * 24 * time.Hour)
expiringAt := now.Add(2 * time.Hour)
conversation := model.ChatConversation{
Title: "账号群 绑定测试",
Type: ConversationTypeListingGroup,
Status: "active",
}
if err := db.Create(&conversation).Error; err != nil {
t.Fatalf("创建群聊失败: %v", err)
}
qrcodes := []model.ChatQrCode{
{
ImageURL: "/api/files/object?key=qrcode/unused-a.png",
GroupName: "待用群A",
Status: QrCodeStatusUnused,
CreatedBy: 1,
Note: "六月首批",
ExpiresAt: &validAt,
CreatedAt: now.Add(-48 * time.Hour),
},
{
ImageURL: "/api/files/object?key=qrcode/used-b.png",
GroupName: "已发群B",
Status: QrCodeStatusUsed,
ConversationID: &conversation.ID,
CreatedBy: 1,
Note: "特殊备注",
WecomRenamed: true,
ExpiresAt: &expiredAt,
CreatedAt: now.Add(-2 * time.Hour),
},
{
ImageURL: "/api/files/object?key=qrcode/permanent-c.png",
GroupName: "永久群C",
Status: QrCodeStatusDisabled,
CreatedBy: 1,
Note: "长期备用",
CreatedAt: now.Add(-24 * time.Hour),
},
{
ImageURL: "/api/files/object?key=qrcode/expiring-d.png",
GroupName: "临期群D",
Status: QrCodeStatusUnused,
CreatedBy: 1,
Note: "三天内到期",
ExpiresAt: &expiringAt,
CreatedAt: now.Add(-30 * time.Minute),
},
}
if err := db.Create(&qrcodes).Error; err != nil {
t.Fatalf("创建二维码失败: %v", err)
}
assertListIDs := func(name string, req QrCodeListRequest, want ...uint64) {
t.Helper()
req.Page = 1
req.Limit = 20
items, total, err := repo.ListQrCodes(t.Context(), req)
if err != nil {
t.Fatalf("%s 查询二维码失败: %v", name, err)
}
if total != int64(len(want)) || len(items) != len(want) {
t.Fatalf("%s total = %d, len = %d, want %d", name, total, len(items), len(want))
}
for i, id := range want {
if items[i].ID != id {
t.Fatalf("%s 第 %d 条 ID = %d, want %d", name, i, items[i].ID, id)
}
}
}
assertListIDs("状态", QrCodeListRequest{Status: QrCodeStatusUnused}, qrcodes[3].ID, qrcodes[0].ID)
assertListIDs("关键词群名", QrCodeListRequest{Keyword: "待用群"}, qrcodes[0].ID)
assertListIDs("关键词备注", QrCodeListRequest{Keyword: "特殊备注"}, qrcodes[1].ID)
assertListIDs("关键词绑定群", QrCodeListRequest{Keyword: "绑定测试"}, qrcodes[1].ID)
assertListIDs("企微改名", QrCodeListRequest{WecomRenamed: "true"}, qrcodes[1].ID)
assertListIDs("未绑定", QrCodeListRequest{Bound: "false"}, qrcodes[3].ID, qrcodes[2].ID, qrcodes[0].ID)
assertListIDs("已过期", QrCodeListRequest{ExpireStatus: "expired"}, qrcodes[1].ID)
assertListIDs("永久有效", QrCodeListRequest{ExpireStatus: "permanent"}, qrcodes[2].ID)
assertListIDs("即将过期", QrCodeListRequest{ExpireStatus: "expiring_soon"}, qrcodes[3].ID)
assertListIDs(
"上传时间",
QrCodeListRequest{
CreatedStart: now.Add(-3 * time.Hour).Format(time.RFC3339),
CreatedEnd: now.Add(-90 * time.Minute).Format(time.RFC3339),
},
qrcodes[1].ID,
)
}
func TestParseQrCodeGroupNameCandidates(t *testing.T) {
rawText := strings.Join([]string{
"使用微信或企业微信扫码加入",
"王大锤-鼠鼠跑刀一群",
"这二维码7天内有效",
}, "\n")
candidates := parseQrCodeGroupNameCandidates(rawText)
if len(candidates) == 0 {
t.Fatal("未解析出群名候选")
}
if candidates[0] != "王大锤-鼠鼠跑刀一群" {
t.Fatalf("首个候选 = %q", candidates[0])
}
}
func TestUpdateQrCodeRejectsUsedToUnused(t *testing.T) {
db := setupQrCodeTestDB(t)
repo := NewRepository(db, nil, nil)
now := time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC)
conversationID := uint64(1001)
qrcode := model.ChatQrCode{
ImageURL: "/api/files/object?key=qrcode/used.png",
Status: QrCodeStatusUsed,
ConversationID: &conversationID,
UsedAt: &now,
CreatedBy: 1,
}
if err := db.Create(&qrcode).Error; err != nil {
t.Fatalf("创建二维码失败: %v", err)
}
status := QrCodeStatusUnused
err := repo.UpdateQrCode(t.Context(), qrcode.ID, UpdateQrCodeRequest{Status: &status})
if !errors.Is(err, ErrQrCodeCannotReenable) {
t.Fatalf("error = %v, want ErrQrCodeCannotReenable", err)
}
var saved model.ChatQrCode
if err := db.First(&saved, qrcode.ID).Error; err != nil {
t.Fatalf("查询二维码失败: %v", err)
}
if saved.Status != QrCodeStatusUsed {
t.Fatalf("二维码状态 = %q, want %q", saved.Status, QrCodeStatusUsed)
}
}
func TestUpdateQrCodeRejectsIssuedDisabledToUnused(t *testing.T) {
db := setupQrCodeTestDB(t)
repo := NewRepository(db, nil, nil)
conversationID := uint64(1002)
qrcode := model.ChatQrCode{
ImageURL: "/api/files/object?key=qrcode/disabled.png",
Status: QrCodeStatusDisabled,
ConversationID: &conversationID,
CreatedBy: 1,
}
if err := db.Create(&qrcode).Error; err != nil {
t.Fatalf("创建二维码失败: %v", err)
}
status := QrCodeStatusUnused
err := repo.UpdateQrCode(t.Context(), qrcode.ID, UpdateQrCodeRequest{Status: &status})
if !errors.Is(err, ErrQrCodeCannotReenable) {
t.Fatalf("error = %v, want ErrQrCodeCannotReenable", err)
}
}
func TestUpdateQrCodeAllowsUnusedToDisabled(t *testing.T) {
db := setupQrCodeTestDB(t)
repo := NewRepository(db, nil, nil)
qrcode := model.ChatQrCode{
ImageURL: "/api/files/object?key=qrcode/unused.png",
Status: QrCodeStatusUnused,
CreatedBy: 1,
}
if err := db.Create(&qrcode).Error; err != nil {
t.Fatalf("创建二维码失败: %v", err)
}
status := QrCodeStatusDisabled
if err := repo.UpdateQrCode(t.Context(), qrcode.ID, UpdateQrCodeRequest{Status: &status}); err != nil {
t.Fatalf("更新二维码失败: %v", err)
}
var saved model.ChatQrCode
if err := db.First(&saved, qrcode.ID).Error; err != nil {
t.Fatalf("查询二维码失败: %v", err)
}
if saved.Status != QrCodeStatusDisabled {
t.Fatalf("二维码状态 = %q, want %q", saved.Status, QrCodeStatusDisabled)
}
}
func TestUpdateQrCodeGroupNameAndRenameFlag(t *testing.T) {
db := setupQrCodeTestDB(t)
repo := NewRepository(db, nil, nil)
qrcode := model.ChatQrCode{
ImageURL: "/api/files/object?key=qrcode/unused.png",
Status: QrCodeStatusUnused,
CreatedBy: 1,
}
if err := db.Create(&qrcode).Error; err != nil {
t.Fatalf("创建二维码失败: %v", err)
}
groupName := "王大锤-鼠鼠跑刀一群"
renamed := true
if err := repo.UpdateQrCode(t.Context(), qrcode.ID, UpdateQrCodeRequest{
GroupName: &groupName,
WecomRenamed: &renamed,
}); err != nil {
t.Fatalf("更新二维码失败: %v", err)
}
var saved model.ChatQrCode
if err := db.First(&saved, qrcode.ID).Error; err != nil {
t.Fatalf("查询二维码失败: %v", err)
}
if saved.GroupName != groupName {
t.Fatalf("群名 = %q, want %q", saved.GroupName, groupName)
}
if !saved.WecomRenamed {
t.Fatal("企微改名标记未保存")
}
}
func TestDeleteQrCodeAllowsIssuedQrCode(t *testing.T) {
db := setupQrCodeTestDB(t)
repo := NewRepository(db, nil, nil)
now := time.Date(2026, 6, 19, 17, 8, 0, 0, time.UTC)
conversationID := uint64(1003)
qrcode := model.ChatQrCode{
ImageURL: "/api/files/object?key=qrcode/issued.png",
Status: QrCodeStatusUsed,
ConversationID: &conversationID,
UsedAt: &now,
CreatedBy: 1,
}
if err := db.Create(&qrcode).Error; err != nil {
t.Fatalf("创建二维码失败: %v", err)
}
if err := repo.DeleteQrCode(t.Context(), qrcode.ID); err != nil {
t.Fatalf("删除已发放二维码失败: %v", err)
}
var count int64
if err := db.Model(&model.ChatQrCode{}).Where("id = ?", qrcode.ID).Count(&count).Error; err != nil {
t.Fatalf("统计二维码失败: %v", err)
}
if count != 0 {
t.Fatalf("二维码数量 = %d, want 0", count)
}
}
func TestBatchDeleteQrCodes(t *testing.T) {
db := setupQrCodeTestDB(t)
repo := NewRepository(db, nil, nil)
now := time.Date(2026, 6, 19, 17, 8, 0, 0, time.UTC)
conversationID := uint64(1004)
qrcodes := []model.ChatQrCode{
{
ImageURL: "/api/files/object?key=qrcode/unused.png",
Status: QrCodeStatusUnused,
CreatedBy: 1,
},
{
ImageURL: "/api/files/object?key=qrcode/used.png",
Status: QrCodeStatusUsed,
ConversationID: &conversationID,
UsedAt: &now,
CreatedBy: 1,
},
{
ImageURL: "/api/files/object?key=qrcode/keep.png",
Status: QrCodeStatusDisabled,
CreatedBy: 1,
},
}
if err := db.Create(&qrcodes).Error; err != nil {
t.Fatalf("创建二维码失败: %v", err)
}
deleted, err := repo.BatchDeleteQrCodes(t.Context(), []uint64{qrcodes[0].ID, qrcodes[1].ID})
if err != nil {
t.Fatalf("批量删除二维码失败: %v", err)
}
if deleted != 2 {
t.Fatalf("删除数量 = %d, want 2", deleted)
}
var remaining []model.ChatQrCode
if err := db.Order("id ASC").Find(&remaining).Error; err != nil {
t.Fatalf("查询剩余二维码失败: %v", err)
}
if len(remaining) != 1 || remaining[0].ID != qrcodes[2].ID {
t.Fatalf("剩余二维码 = %+v, want id %d", remaining, qrcodes[2].ID)
}
}
func TestEnsureListingConversationCreatesQrCodeDeliveryTaskWhenStockEmpty(t *testing.T) {
db := setupQrCodeTestDB(t)
listing := model.RentalListing{
ID: 101,
ListingNo: "L202606260001",
OwnerID: 10,
}
if err := db.Transaction(func(tx *gorm.DB) error {
_, err := EnsureListingConversation(tx, listing, 0)
return err
}); err != nil {
t.Fatalf("建发布群失败: %v", err)
}
var task model.ChatQrCodeDeliveryTask
if err := db.First(&task).Error; err != nil {
t.Fatalf("未创建补发任务: %v", err)
}
if task.Status != "pending" {
t.Fatalf("补发任务状态 = %q, want pending", task.Status)
}
}
func TestBatchCreateQrCodeDeliversPendingTask(t *testing.T) {
db := setupQrCodeTestDB(t)
repo := NewRepository(db, nil, nil)
conversation := model.ChatConversation{
Title: "账号群 L202606260002",
Type: ConversationTypeListingGroup,
Status: "active",
}
if err := db.Create(&conversation).Error; err != nil {
t.Fatalf("创建群聊失败: %v", err)
}
task := model.ChatQrCodeDeliveryTask{
ConversationID: conversation.ID,
Status: "pending",
}
if err := db.Create(&task).Error; err != nil {
t.Fatalf("创建补发任务失败: %v", err)
}
_, err := repo.BatchCreateQrCode(t.Context(), 1, BatchCreateQrCodeRequest{
Items: []CreateQrCodeRequest{
{ImageURL: "/api/files/object?key=qrcode/replenish.png", GroupName: "补发群"},
},
})
if err != nil {
t.Fatalf("批量创建二维码失败: %v", err)
}
var savedTask model.ChatQrCodeDeliveryTask
if err := db.First(&savedTask, task.ID).Error; err != nil {
t.Fatalf("查询补发任务失败: %v", err)
}
if savedTask.Status != "sent" {
t.Fatalf("补发任务状态 = %q, want sent", savedTask.Status)
}
if savedTask.QrCodeID == nil {
t.Fatal("补发任务未记录二维码 ID")
}
if savedTask.SentAt == nil {
t.Fatal("补发任务未记录 sent_at")
}
var qrcode model.ChatQrCode
if err := db.First(&qrcode, *savedTask.QrCodeID).Error; err != nil {
t.Fatalf("查询二维码失败: %v", err)
}
if qrcode.Status != QrCodeStatusUsed {
t.Fatalf("二维码状态 = %q, want used", qrcode.Status)
}
if qrcode.ConversationID == nil || *qrcode.ConversationID != conversation.ID {
t.Fatalf("二维码绑定会话 = %v, want %d", qrcode.ConversationID, conversation.ID)
}
var message model.ChatMessage
if err := db.Where("conversation_id = ? AND content_type = ?", conversation.ID, "image").First(&message).Error; err != nil {
t.Fatalf("未生成二维码图片消息: %v", err)
}
}