Files
qitongxue-api/internal/cmd/cmd_test.go

122 lines
4.9 KiB
Go
Raw Normal View History

2026-09-29 10:57:01 +08:00
package cmd
import (
"context"
"os"
"testing"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcron"
"github.com/gogf/gf/v2/os/gfile"
"tool-api/internal/logic"
)
// ============================================================================
// 启动期(cmd.Main.Func)两条易静默失效/直接崩的路径,各自固化一个用例。
//
// 背景:这两条都是「本地跑起来才发现」的问题,都不会被业务单测覆盖 ——
// ① cron pattern 写错 → AddSingleton 返回 error 被 WARNING 吞掉,任务静默不注册;
// ② 静态目录不存在 → AddStaticPath 调 Logger().Fatalf → os.Exit(1),进程直接死。
// ============================================================================
// TestCronPatternsAreValid 固化启动期两个定时任务必须注册成功。
//
// GoFrame gcron 的 pattern 是 **6 段**(秒 分 时 日 月 周),不是标准 crontab 的 5 段。
// 本项目曾把「笔记提醒扫描」写成 5 段 "*/1 * * * *":
// AddSingleton 返回 error → 被启动日志里的 WARNING 吞掉 → 服务照常启动、无人察觉,
// 但该任务从未注册、笔记定时提醒功能完全失效。
func TestCronPatternsAreValid(t *testing.T) {
patterns := map[string]string{
"member-order-sweep": cronPatternSweepOrders,
"note-remind-scan": cronPatternScanRemind,
"coupon-lock-release": cronPatternReleaseCoupons,
"order-close-pending": cronPatternCloseOrders,
}
for name, pattern := range patterns {
name, pattern := name, pattern
t.Run(name, func(t *testing.T) {
if _, err := gcron.AddSingleton(
context.Background(), pattern, func(ctx context.Context) {}, name,
); err != nil {
t.Fatalf("cron 任务 %s 注册失败,pattern=%q:%v\n"+
"(gcron 需要 6 段「秒 分 时 日 月 周」;标准 5 段需前置 '#')", name, pattern, err)
}
gcron.Remove(name)
})
}
}
// TestFivePartCronPatternIsRejected 反证上面的用例不是空转:
// 标准 5 段 crontab 写法确实会被 gcron 拒绝。
// 若哪天这条断言失效(gcron 升级后支持了 5 段),说明本文件的假设已过期,需重新核对。
func TestFivePartCronPatternIsRejected(t *testing.T) {
const bad = "*/1 * * * *"
if _, err := gcron.AddSingleton(
context.Background(), bad, func(ctx context.Context) {}, "probe-bad-5part",
); err == nil {
t.Fatalf("5 段 pattern %q 竟被 gcron 接受,本文件假设已失效,请重新核对 gcron 行为", bad)
}
gcron.Remove("probe-bad-5part")
}
// TestQrCodeStaticPathBootOrder 固化「静态目录必须先存在」这一启动约束。
//
// 机理(已读 gf 源码确认,net/ghttp/ghttp_server_config_static.go:93):
//
// if p, err := gfile.Search(path); err != nil {
// s.Logger().Fatalf(ctx, `AddStaticPath failed: %+v`, err)
// }
//
// 是 Fatalf → os.Exit(1),**不是 panic、无法 recover**。
// 而 manifest/qrcode 原先只在「生成小程序码」时才惰性创建 →
// 全新环境首次启动必然崩在注册静态路由这一步(先有鸡先有蛋的死锁)。
// 修法:cmd.go 先 gfile.Mkdir(logic.QrCodeDir) 再 AddStaticPath。
func TestQrCodeStaticPathBootOrder(t *testing.T) {
chdirRepoRoot(t)
// ① 目录不存在时 gfile.Search 会报错(这就是上面 Fatalf 的触发条件)。
// 用 Search 探测而不真的调 AddStaticPath —— 后者会直接 os.Exit(1) 杀掉测试进程。
t.Run("missing_dir_makes_gf_search_fail", func(t *testing.T) {
if _, err := gfile.Search("manifest/__definitely_absent__"); err == nil {
t.Error("gfile.Search 对不存在的目录竟返回成功,本用例假设已失效(gf 行为可能已变更)")
}
})
// ② 预创建目录后,AddStaticPath 可以正常注册(即 cmd.go 的修复路径)。
// AddStaticPath 无返回值,能走到下一行即为通过。
t.Run("precreated_dir_registers_ok", func(t *testing.T) {
if err := gfile.Mkdir(logic.QrCodeDir); err != nil {
t.Fatalf("gfile.Mkdir(%q) 失败:%v", logic.QrCodeDir, err)
}
if !gfile.Exists(logic.QrCodeDir) {
t.Fatalf("Mkdir 之后目录仍不存在:%q", logic.QrCodeDir)
}
g.Server("static-path-probe").AddStaticPath("/qrcode", logic.QrCodeDir)
})
}
// chdirRepoRoot 把工作目录切到仓库根目录。
// go test 的 CWD 是「包目录」(internal/cmd),直接用相对路径会跑偏到 internal/cmd/manifest/qrcode,
// 与被测代码在真实运行时的 CWD(仓库根)不一致。
func chdirRepoRoot(t *testing.T) {
t.Helper()
old, err := os.Getwd()
if err != nil {
t.Fatalf("获取当前工作目录失败:%v", err)
}
t.Cleanup(func() { _ = os.Chdir(old) })
dir := old
for i := 0; i < 8; i++ {
if gfile.Exists(gfile.Join(dir, "go.mod")) {
if err = os.Chdir(dir); err != nil {
t.Fatalf("切换到仓库根目录 %s 失败:%v", dir, err)
}
return
}
dir = gfile.Dir(dir)
}
t.Fatal("向上 8 层未找到 go.mod,无法定位仓库根目录")
}