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.SystemConfig{}); err != nil { t.Fatalf("数据库迁移失败: %v", err) } return db } func TestGetQrCodeOCRConfigReadsSystemConfig(t *testing.T) { db := setupQrCodeTestDB(t) repo := NewRepository(db, 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) 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 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) 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) 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) 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) 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("企微改名标记未保存") } }