topfans/backend/pkg/queue/consts/consts_test.go
zerosaturation e1326acaf9 fix(backend): service stability — bcrypt off-txn / login anti-enum / MQ stub / aiChat / event reliability / gateway aggregate (batch 3)
- 3.1 bcrypt 移出事务 (Register): repository.HashPassword 前移到 db.Transaction 之前,消除连接池占用。
- 3.2 Login 消除用户枚举 + 限流 + timing 抹平: pkg/errors 加 ErrInvalidCredential
  /ErrTooManyLoginAttempts; 用户不存在/密码错/密码空 三路径统一返回同一错误;
  mobile 5次/ip 20次 per 15min 限流 (Redis, fail-open 降级); user-not-found 走
  dummy bcrypt 抹平 ~100ms 时序差,完全消除枚举侧信道;空密码分支已核实无时序 leak。
- 3.3 MQ streams adapter 停用 → stub: 0 业务调用方, 新 stub EventProducer.Publish no-op;
  pkg/mq/mq.go Init 不再装配 streams; 全仓 grep 验证 11 处硬编码
  'gallery'/'default' 集中到 pkg/queue/consts (值不变, 仅消漂移)。
- 3.5 JWT 密钥治理: pkg/jwt MustInit fail-fast + atomic.Value (见上一个 commit 293c7b1)。
- 3.6 aiChat 健壮性: SaveContext 用 persona.ID(非 req.PersonaId); Redis/memory 错误
  记 WARN 不静默; Dify err 映射稳定用户文案,原始 err 仅服务端日志。
- 3.7 statistic.Client 重构: TrackEvent 改 buffered channel (cap 1024) + dispatchLoop
  worker; 失败 ERROR 日志带字段; drop 记 WARN; Close 可重复调用。
- 3.8 网关聚合: StarCache (60s TTL, singleflight) 替换 5+ 处 GetFanIdentities 链式调用;
  DeleteAccount 改网关直调 userService.DeleteAccount(避免改 hand-written triple.go
  风险,见报告 §5 proto 风险复盘); 铸造双写改异步 channel+consumer (3 retry)。
- 大量单测: 各子项 TDD (RED→GREEN), 关键并发 race_test (50 goroutine)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 18:50:40 +08:00

110 lines
3.6 KiB
Go

package consts
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
// TestQueueNames_HaveExpectedValues 锁死字面量值 — 改名 = 丢消息(regression guard)。
func TestQueueNames_HaveExpectedValues(t *testing.T) {
if QueueGallery != "gallery" {
t.Fatalf("QueueGallery changed to %q — 改名会丢消息!", QueueGallery)
}
if QueueDefault != "default" {
t.Fatalf("QueueDefault changed to %q — 改名会丢消息!", QueueDefault)
}
}
// TestServiceFilesUseConstants 验证 galleryService / taskService 已用 consts 引用字面量,
// 不再硬编码 "gallery"/"default"。此测试是字面量集中化的回归保护。
//
// 允许保留字面量的位置:
// - consts.go 本文件(定义)
// - 注释 / doc 字符串
// - pkg/mq/asynq 的默认值("default" 作为 asynq client 端的语义保留)
func TestServiceFilesUseConstants(t *testing.T) {
// 项目根 = 后端目录
_, thisFile, _, _ := runtime.Caller(0)
root := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(thisFile))))
// thisFile = .../pkg/queue/consts/consts_test.go
// 走 4 层: consts -> queue -> pkg -> backend
targets := []string{
filepath.Join(root, "services", "galleryService", "main.go"),
filepath.Join(root, "services", "galleryService", "mq", "producer.go"),
filepath.Join(root, "services", "galleryService", "mq", "consumer.go"),
filepath.Join(root, "services", "taskService", "main.go"),
filepath.Join(root, "services", "taskService", "mq", "consumer.go"),
}
for _, p := range targets {
body, err := os.ReadFile(p)
if err != nil {
t.Fatalf("read %s: %v", p, err)
}
text := string(body)
// 抽掉所有 // / /* */ 注释,再扫字面量
stripped := stripComments(text)
// 找 `Queue: "gallery"` / `Queue: "default"` / `Queues: map[...]string{...}` 等
// 实际只关心:
// - "gallery" 出现在 Queue 上下文字面量位置(Queue: "gallery" 或 "gallery": N)
// - "default" 同上
assertNoQueueLiteral(t, p, stripped, `"gallery"`, "QueueGallery")
assertNoQueueLiteral(t, p, stripped, `"default"`, "QueueDefault")
}
}
func assertNoQueueLiteral(t *testing.T, file, body, literal, constName string) {
t.Helper()
// 行号定位以便排查
for i, line := range rangeLines(body) {
// 跳过 consts 导入行(import 块里就是 consts. 引用,不算字面量)
// 但我们看的是 stripped 文本,import 也还在;不影响,只看 Queue/Queues 上下文
if !strings.Contains(line, literal) {
continue
}
// 判定: 此行是否在 Queue: / Queues: 上下文里
isQueueContext := strings.Contains(line, "Queue:") ||
strings.Contains(line, "Queues:") ||
strings.Contains(line, "Queue =") ||
strings.Contains(line, `Queues =`)
if isQueueContext {
t.Errorf("%s:%d 仍硬编码 %s — 应改为 consts.%s\n line: %s",
file, i+1, literal, constName, strings.TrimSpace(line))
}
}
}
// stripComments 删掉 // / /* */ 注释。简化版:用 // 拆行处理 // 注释,/ *...* / 整段删。
func stripComments(s string) string {
var b strings.Builder
// 先删 /* ... */
for {
start := strings.Index(s, "/*")
if start < 0 {
break
}
end := strings.Index(s[start:], "*/")
if end < 0 {
s = s[:start]
break
}
s = s[:start] + s[start+end+2:]
}
// 再按行删 // 注释
for _, line := range strings.Split(s, "\n") {
if i := strings.Index(line, "//"); i >= 0 {
line = line[:i]
}
b.WriteString(line)
b.WriteByte('\n')
}
return b.String()
}
// rangeLines 把字符串按行返回(不分配 index/element 配对,只给测试用)。
func rangeLines(s string) []string { return strings.Split(s, "\n") }