Files
qitongxue-api/internal/cmd/cmd_test.go
2026-09-29 10:57:01 +08:00

122 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,无法定位仓库根目录")
}