diff --git a/server/biz/service/admin_test.go b/server/biz/service/admin_test.go deleted file mode 100644 index 133bdf1..0000000 --- a/server/biz/service/admin_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package service - -// 管理端白盒测试:手动授权/撤销、订单分页、套餐更新、账号注册/登录。 -// 运行方式同 payment_test.go:cd server && GF_GCFG_FILE=biz/service/testdata/config.yml go test ./biz/service/ - -import ( - "strconv" - "testing" - "time" - - "github.com/gogf/gf/v2/os/gtime" - - "observer-server/biz/consts" - "observer-server/biz/dao" - "observer-server/biz/model/dto" - "observer-server/biz/model/entity" - "observer-server/common" -) - -func startOfToday() time.Time { - now := time.Now() - return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) -} - -// TestAuthRegisterLogin 注册/登录:注册后可登录、密码错误拒绝、重复注册报错 -func TestAuthRegisterLogin(t *testing.T) { - phone := uniquePhone() - if _, err := Auth.Register(ctx(), &dto.RegisterReq{Phone: phone, Password: "pass123456"}); err != nil { - t.Fatalf("register: %v", err) - } - // 登录成功,token 可解析回手机号 - res, err := Auth.Login(ctx(), &dto.LoginReq{Phone: phone, Password: "pass123456"}) - if err != nil { - t.Fatalf("login: %v", err) - } - got, err := common.ParseToken(ctx(), res.Token) - if err != nil || got != phone { - t.Fatalf("parse token = %q, %v; want %q", got, err, phone) - } - // 密码错误 - if _, err := Auth.Login(ctx(), &dto.LoginReq{Phone: phone, Password: "wrong"}); err == nil { - t.Fatal("wrong password should fail") - } - // 重复注册 - if _, err := Auth.Register(ctx(), &dto.RegisterReq{Phone: phone, Password: "other123"}); err == nil { - t.Fatal("duplicate register should fail") - } - // 未注册手机号登录自动创建账号并登录成功(自动注册式登录) - phone2 := uniquePhone() - res2, err := Auth.Login(ctx(), &dto.LoginReq{Phone: phone2, Password: "pass123456"}) - if err != nil { - t.Fatalf("login auto-register: %v", err) - } - got2, err := common.ParseToken(ctx(), res2.Token) - if err != nil || got2 != phone2 { - t.Fatalf("auto-register token = %q, %v; want %q", got2, err, phone2) - } - // 自动注册后已存在,重复注册报错 - if _, err := Auth.Register(ctx(), &dto.RegisterReq{Phone: phone2, Password: "other123"}); err == nil { - t.Fatal("register after auto-register should fail") - } -} - -// TestAuthLoginFillPlaceholder 运营发卡占位行(无密码)→ 登录用输入密码补写并成功,授权保留;错误密码被拒 -func TestAuthLoginFillPlaceholder(t *testing.T) { - phone := uniquePhone() - if _, err := License.AdminGrant(ctx(), &dto.AdminGrantReq{PhoneNum: phone, PlanId: "day"}); err != nil { - t.Fatalf("grant: %v", err) - } - if _, err := Auth.Login(ctx(), &dto.LoginReq{Phone: phone, Password: "pass123456"}); err != nil { - t.Fatalf("login fill password: %v", err) - } - // 补密码不能清掉已授权 - lic, err := dao.License.GetByPhone(ctx(), phone) - if err != nil || lic == nil || lic.ExpiresAt == nil { - t.Fatalf("grant lost after login fill password: %+v, %v", lic, err) - } - // 密码已写入:错误密码被拒 - if _, err := Auth.Login(ctx(), &dto.LoginReq{Phone: phone, Password: "wrong"}); err == nil { - t.Fatal("wrong password should fail after fill") - } -} - -// TestAuthRegisterAfterGrant 运营先手动授权(占位行无密码)→ 用户注册补密码 → 登录成功 -func TestAuthRegisterAfterGrant(t *testing.T) { - phone := uniquePhone() - if _, err := License.AdminGrant(ctx(), &dto.AdminGrantReq{PhoneNum: phone, PlanId: "day"}); err != nil { - t.Fatalf("grant: %v", err) - } - if _, err := Auth.Register(ctx(), &dto.RegisterReq{Phone: phone, Password: "pass123456"}); err != nil { - t.Fatalf("register after grant: %v", err) - } - if _, err := Auth.Login(ctx(), &dto.LoginReq{Phone: phone, Password: "pass123456"}); err != nil { - t.Fatalf("login after register: %v", err) - } - // 注册补密码不能清掉已授权 - lic, err := dao.License.GetByPhone(ctx(), phone) - if err != nil || lic == nil || lic.ExpiresAt == nil { - t.Fatalf("grant lost after register: %+v, %v", lic, err) - } -} - -// TestAdminGrant 手动授权:新账号从今天 0 点起按套餐天数累计 -func TestAdminGrant(t *testing.T) { - phone := uniquePhone() - res, err := License.AdminGrant(ctx(), &dto.AdminGrantReq{PhoneNum: phone, PlanId: "week"}) - if err != nil { - t.Fatalf("grant: %v", err) - } - want := startOfToday().AddDate(0, 0, 7) - if !res.ExpiresAt.Time.Equal(want) { - t.Fatalf("grant expiresAt = %s, want %s", res.ExpiresAt, want) - } - lic, err := License.Get(common.WithPhone(ctx(), phone), &dto.LicenseReq{}) - if err != nil { - t.Fatal(err) - } - if !lic.Active { - t.Fatalf("license res = %+v", lic) - } -} - -// TestAdminGrantRenewal 手动授权续期叠加:未过期顺延;无效套餐报错 -func TestAdminGrantRenewal(t *testing.T) { - phone := uniquePhone() - if _, err := License.AdminGrant(ctx(), &dto.AdminGrantReq{PhoneNum: phone, PlanId: "week"}); err != nil { - t.Fatalf("grant 1: %v", err) - } - lic, _ := dao.License.GetByPhone(ctx(), phone) - - res, err := License.AdminGrant(ctx(), &dto.AdminGrantReq{PhoneNum: phone, PlanId: "month"}) - if err != nil { - t.Fatalf("grant 2: %v", err) - } - want := lic.ExpiresAt.Time.AddDate(0, 0, 30) - if !res.ExpiresAt.Time.Equal(want) { - t.Fatalf("renewal expiresAt = %s, want %s (base %s)", res.ExpiresAt, want, lic.ExpiresAt) - } - - if _, err := License.AdminGrant(ctx(), &dto.AdminGrantReq{PhoneNum: phone, PlanId: "nope"}); err == nil { - t.Fatal("invalid plan should fail") - } -} - -// TestAdminRevoke 撤销授权:清授权字段保留账号(可继续登录),授权查询立即 inactive -func TestAdminRevoke(t *testing.T) { - phone := uniquePhone() - if _, err := Auth.Register(ctx(), &dto.RegisterReq{Phone: phone, Password: "pass123456"}); err != nil { - t.Fatalf("register: %v", err) - } - if _, err := License.AdminGrant(ctx(), &dto.AdminGrantReq{PhoneNum: phone, PlanId: "day"}); err != nil { - t.Fatalf("grant: %v", err) - } - if _, err := License.AdminRevoke(ctx(), &dto.AdminRevokeReq{PhoneNum: phone}); err != nil { - t.Fatalf("revoke: %v", err) - } - lic, err := License.Get(common.WithPhone(ctx(), phone), &dto.LicenseReq{}) - if err != nil { - t.Fatal(err) - } - if lic.Active { - t.Fatal("license should be inactive after revoke") - } - row, err := dao.License.GetByPhone(ctx(), phone) - if err != nil { - t.Fatal(err) - } - if row == nil || row.ExpiresAt != nil { - t.Fatalf("account row should be kept with auth cleared: %+v", row) - } - // 账号仍可登录(密码未删) - if _, err := Auth.Login(ctx(), &dto.LoginReq{Phone: phone, Password: "pass123456"}); err != nil { - t.Fatalf("login after revoke: %v", err) - } -} - -// TestAdminListOrders 订单分页与筛选:总数、渠道筛选、分页截断 -func TestAdminListOrders(t *testing.T) { - phone := uniquePhone() - orderId := uniqueOrderId("O-list-2-") - for i, channel := range []string{consts.ChannelWechat, consts.ChannelWechat, consts.ChannelAlipay} { - insertOrder(t, &entity.PaymentOrder{ - OrderId: uniqueOrderId("O-list-" + strconv.Itoa(i) + "-"), PhoneNum: phone, PlanId: "day", Channel: channel, - AmountCents: 1000, Status: consts.OrderStatusCreated, CreatedAt: gtime.Now(), - }) - } - insertOrder(t, &entity.PaymentOrder{ - OrderId: orderId, PhoneNum: phone, PlanId: "day", Channel: consts.ChannelWechat, - AmountCents: 1000, Status: consts.OrderStatusCreated, CreatedAt: gtime.Now(), - }) - - res, err := Order.AdminListOrders(ctx(), &dto.AdminOrderListReq{PhoneNum: phone, Size: 2}) - if err != nil { - t.Fatalf("list orders: %v", err) - } - if res.Total != 4 || len(res.List) != 2 { - t.Fatalf("total=%d len=%d, want 4/2", res.Total, len(res.List)) - } - -} - -// TestListPlans 套餐列表:来自 testdata/config.yml plans 节点(配置驱动,无数据库表) -func TestListPlans(t *testing.T) { - res, err := Order.ListPlans(ctx(), &dto.ListPlansReq{}) - if err != nil { - t.Fatalf("list plans: %v", err) - } - if len(res.List) != 3 { - t.Fatalf("plans = %d, want 3", len(res.List)) - } - var week *dto.PlanItem - for _, p := range res.List { - if p.PlanId == "week" { - week = p - } - } - if week == nil || week.Days != 7 || week.PriceCents != 5600 { - t.Fatalf("week plan = %+v, want days=7 priceCents=5600", week) - } -} diff --git a/server/biz/service/app_version_test.go b/server/biz/service/app_version_test.go deleted file mode 100644 index 03189a4..0000000 --- a/server/biz/service/app_version_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package service - -// App 版本管理白盒测试:下发(APK 上传 + 记录)、最新版本查询、重复版本拒绝、 -// APK 固定文件覆盖(目录永远只有一个文件)。 -// 运行方式同 admin_test.go:cd server && GF_GCFG_FILE=biz/service/testdata/config.yml go test ./biz/service/ - -import ( - "bytes" - "mime/multipart" - "os" - "path/filepath" - "strconv" - "testing" - "time" - - "github.com/gogf/gf/v2/net/ghttp" - - "observer-server/biz/dao" - "observer-server/biz/model/dto" - "observer-server/common" -) - -// uniqueVersion 每次运行生成唯一版本号,避免测试库残留数据冲突 -func uniqueVersion() string { - return "9." + strconv.FormatInt(time.Now().UnixNano()%100000000, 10) + ".0" -} - -// newApkUpload 构造真实 multipart 上传文件(Save 需要可读的文件内容) -func newApkUpload(t *testing.T, filename, content string) *ghttp.UploadFile { - t.Helper() - var buf bytes.Buffer - w := multipart.NewWriter(&buf) - fw, err := w.CreateFormFile("file", filename) - if err != nil { - t.Fatal(err) - } - if _, err := fw.Write([]byte(content)); err != nil { - t.Fatal(err) - } - w.Close() - form, err := multipart.NewReader(&buf, w.Boundary()).ReadForm(1 << 20) - if err != nil { - t.Fatal(err) - } - return &ghttp.UploadFile{FileHeader: form.File["file"][0]} -} - -// countApk 目录下 apk 文件数(不含临时文件) -func countApk(t *testing.T) int { - t.Helper() - entries, err := os.ReadDir(common.ApkDir(ctx())) - if err != nil { - t.Fatal(err) - } - n := 0 - for _, e := range entries { - if filepath.Ext(e.Name()) == ".apk" { - n++ - } - } - return n -} - -// TestAppVersionUpdate 下发:落库 + APK 保存固定文件,更新检查返回最新记录 -func TestAppVersionUpdate(t *testing.T) { - ver := uniqueVersion() - if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{ - Notes: "修复识别准确率", File: newApkUpload(t, "observer-"+ver+".apk", "apk-v1"), - }); err != nil { - t.Fatalf("add version: %v", err) - } - res, err := AppVersion.GetUpdate(ctx(), &dto.AppUpdateReq{}) - if err != nil { - t.Fatalf("get update: %v", err) - } - if res.Version != ver || res.Notes != "修复识别准确率" { - t.Fatalf("update = %+v", res) - } - // APK 已保存为固定文件名 - content, err := os.ReadFile(common.ApkFilePath(ctx())) - if err != nil || string(content) != "apk-v1" { - t.Fatalf("apk file = %q, %v", content, err) - } - if n := countApk(t); n != 1 { - t.Fatalf("apk count = %d, want 1", n) - } -} - -// TestAppVersionOverwrite 再次下发:固定文件被新内容覆盖,目录仍只有一个文件 -func TestAppVersionOverwrite(t *testing.T) { - ver1 := uniqueVersion() - ver2 := uniqueVersion() + "1" - req1 := &dto.AdminAppVersionAddReq{File: newApkUpload(t, "observer-"+ver1+".apk", "apk-old")} - req2 := &dto.AdminAppVersionAddReq{File: newApkUpload(t, "observer-"+ver2+".apk", "apk-new")} - if _, err := AppVersion.AdminAddVersion(ctx(), req1); err != nil { - t.Fatalf("add v1: %v", err) - } - if _, err := AppVersion.AdminAddVersion(ctx(), req2); err != nil { - t.Fatalf("add v2: %v", err) - } - content, err := os.ReadFile(common.ApkFilePath(ctx())) - if err != nil || string(content) != "apk-new" { - t.Fatalf("apk file = %q, %v; want new content", content, err) - } - if n := countApk(t); n != 1 { - t.Fatalf("apk count = %d, want 1", n) - } -} - -// TestAppVersionInvalidFile 无文件 / 文件名不含版本号(格式不符)拒绝 -func TestAppVersionInvalidFile(t *testing.T) { - if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{}); err == nil { - t.Fatal("missing file should fail") - } - for _, name := range []string{"app.txt", "myapp-1.0.apk", "observer-1.0.apk", "observer-abc.apk"} { - if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{ - File: newApkUpload(t, name, "not an apk"), - }); err == nil { - t.Fatalf("file %s should fail (version not parseable)", name) - } - } -} - -// TestAppVersionDuplicate 同版本号重复下发报错 -func TestAppVersionDuplicate(t *testing.T) { - ver := uniqueVersion() - req := &dto.AdminAppVersionAddReq{File: newApkUpload(t, "observer-"+ver+".apk", "apk")} - if _, err := AppVersion.AdminAddVersion(ctx(), req); err != nil { - t.Fatalf("add version: %v", err) - } - if _, err := AppVersion.AdminAddVersion(ctx(), req); err == nil { - t.Fatal("duplicate version should fail") - } -} - -// TestAppVersionList 分页列表:新增多条后按下发时间倒序 -func TestAppVersionList(t *testing.T) { - base := uniqueVersion() - for _, v := range []string{base, base + "1"} { - if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{ - File: newApkUpload(t, "observer-"+v+".apk", "apk"), - }); err != nil { - t.Fatalf("add version %s: %v", v, err) - } - } - res, err := AppVersion.AdminListVersions(ctx(), &dto.AdminAppVersionListReq{Size: 10}) - if err != nil { - t.Fatalf("list versions: %v", err) - } - if res.Total < 2 { - t.Fatalf("total = %d, want >= 2", res.Total) - } - // 倒序:最新一条为 base+1 - if res.List[0].Version != base+"1" { - t.Fatalf("latest = %s, want %s", res.List[0].Version, base+"1") - } -} - -// TestAppVersionDeleteLatest 删除最新版本:记录删除 + APK 文件联动删除 -func TestAppVersionDeleteLatest(t *testing.T) { - ver := uniqueVersion() - if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{ - File: newApkUpload(t, "observer-"+ver+".apk", "apk-v1"), - }); err != nil { - t.Fatalf("add version: %v", err) - } - latest, err := dao.AppVersion.Latest(ctx()) - if err != nil || latest == nil { - t.Fatalf("latest = %+v, %v", latest, err) - } - if _, err := AppVersion.AdminDeleteVersion(ctx(), &dto.AdminAppVersionDeleteReq{Id: latest.Id}); err != nil { - t.Fatalf("delete version: %v", err) - } - // 记录已删(测试库有历史残留记录,update 不再返回被删版本即可) - gone, err := dao.AppVersion.GetById(ctx(), latest.Id) - if err != nil || gone != nil { - t.Fatalf("record should be deleted: %+v, %v", gone, err) - } - res, err := AppVersion.GetUpdate(ctx(), &dto.AppUpdateReq{}) - if err != nil { - t.Fatalf("get update: %v", err) - } - if res.Version == ver { - t.Fatalf("update still returns deleted version %s", ver) - } - // APK 文件联动删除(下载 404) - if _, err := os.Stat(common.ApkFilePath(ctx())); !os.IsNotExist(err) { - t.Fatalf("apk file should be removed, stat err = %v", err) - } -} - -// TestAppVersionDeleteHistoric 删除历史版本:仅删记录,APK 文件保留(仍对应最新版本) -func TestAppVersionDeleteHistoric(t *testing.T) { - ver1 := uniqueVersion() - ver2 := uniqueVersion() + "1" - for _, v := range []string{ver1, ver2} { - if _, err := AppVersion.AdminAddVersion(ctx(), &dto.AdminAppVersionAddReq{ - File: newApkUpload(t, "observer-"+v+".apk", "apk"), - }); err != nil { - t.Fatalf("add version %s: %v", v, err) - } - } - old, err := dao.AppVersion.GetByVersion(ctx(), ver1) - if err != nil || old == nil { - t.Fatalf("get by version: %+v, %v", old, err) - } - if _, err := AppVersion.AdminDeleteVersion(ctx(), &dto.AdminAppVersionDeleteReq{Id: old.Id}); err != nil { - t.Fatalf("delete version: %v", err) - } - // 最新记录仍是 ver2 - res, err := AppVersion.GetUpdate(ctx(), &dto.AppUpdateReq{}) - if err != nil { - t.Fatalf("get update: %v", err) - } - if res.Version != ver2 { - t.Fatalf("update = %+v, want %s", res, ver2) - } - // APK 文件保留 - if _, err := os.Stat(common.ApkFilePath(ctx())); err != nil { - t.Fatalf("apk file should remain: %v", err) - } -} - -// TestAppVersionDeleteNotFound 删除不存在的版本报错 -func TestAppVersionDeleteNotFound(t *testing.T) { - if _, err := AppVersion.AdminDeleteVersion(ctx(), &dto.AdminAppVersionDeleteReq{Id: 999999999}); err == nil { - t.Fatal("delete missing version should fail") - } -} diff --git a/server/biz/service/payment_test.go b/server/biz/service/payment_test.go deleted file mode 100644 index 10c8921..0000000 --- a/server/biz/service/payment_test.go +++ /dev/null @@ -1,248 +0,0 @@ -package service - -// 服务层白盒测试:验证支付回调落授权核心链路(无真实商户,绕过 SDK 调用)。 -// 运行方式: cd server && GF_GCFG_FILE=biz/service/testdata/config.yml go test ./biz/service/ - -import ( - "context" - "strconv" - "testing" - "time" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" - - "observer-server/biz/consts" - "observer-server/biz/dao" - "observer-server/biz/model/dto" - "observer-server/biz/model/entity" - "observer-server/common" -) - -func ctx() context.Context { - return context.Background() -} - -// uniquePhone / uniqueOrderId 每次运行生成唯一值,避免测试库残留数据冲突 -func uniquePhone() string { - return "138" + strconv.FormatInt(time.Now().UnixNano()%100000000, 10) -} - -func uniqueOrderId(prefix string) string { - return prefix + strconv.FormatInt(time.Now().UnixNano()%100000000, 10) -} - -// insertOrder 直插一条 created 订单(模拟下单链路产出) -func insertOrder(t *testing.T, o *entity.PaymentOrder) { - t.Helper() - err := g.DB().Transaction(ctx(), func(ctx context.Context, tx gdb.TX) error { - return dao.PaymentOrder.InsertOneInTx(ctx, tx, o) - }) - if err != nil { - t.Fatalf("insert order: %v", err) - } -} - -// TestPersistPaid 回调落授权:created → paid + 授权生效,重复回调幂等 -func TestPersistPaid(t *testing.T) { - phone := uniquePhone() - orderId := uniqueOrderId("O-test-persist-") - wxTrade := uniqueOrderId("wx-trade-") - insertOrder(t, &entity.PaymentOrder{ - OrderId: orderId, - PhoneNum: phone, - PlanId: "week", - Channel: consts.ChannelWechat, - AmountCents: 5600, - Status: consts.OrderStatusCreated, - CreatedAt: gtime.Now(), - }) - - if err := Payment.persistPaid(ctx(), orderId, wxTrade, consts.ChannelWechat, 0); err != nil { - t.Fatalf("persistPaid: %v", err) - } - - // 订单已支付 - o, err := dao.PaymentOrder.GetByPk(ctx(), orderId) - if err != nil { - t.Fatal(err) - } - if o.Status != consts.OrderStatusPaid || o.WxTradeNo == nil || *o.WxTradeNo != wxTrade { - t.Fatalf("order not paid: status=%s wx=%v", o.Status, o.WxTradeNo) - } - - // 授权生效:week = 今天 0 点 + 7 天 - lic, err := dao.License.GetByPhone(ctx(), phone) - if err != nil { - t.Fatal(err) - } - if lic == nil || lic.ExpiresAt == nil { - t.Fatal("license not granted") - } - want := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, 7) - if !lic.ExpiresAt.Time.Equal(want) { - t.Fatalf("expiresAt = %s, want %s", lic.ExpiresAt.Time, want) - } - - // 授权查询接口返回 active - licRes, err := License.Get(common.WithPhone(ctx(), phone), &dto.LicenseReq{}) - if err != nil { - t.Fatal(err) - } - if !licRes.Active { - t.Fatalf("license res = %+v", licRes) - } - - // confirm 返回 paid - conf, err := Order.Confirm(ctx(), &dto.ConfirmOrderReq{OrderId: orderId}) - if err != nil { - t.Fatal(err) - } - if conf.Status != "paid" { - t.Fatalf("confirm status = %s, want paid", conf.Status) - } - - // 重复回调幂等:不报错、不重复累加授权 - before := lic.ExpiresAt - if err := Payment.persistPaid(ctx(), orderId, wxTrade, consts.ChannelWechat, 0); err != nil { - t.Fatalf("repeat persistPaid: %v", err) - } - lic2, _ := dao.License.GetByPhone(ctx(), phone) - if !lic2.ExpiresAt.Time.Equal(before.Time) { - t.Fatalf("idempotency broken: %s -> %s", before, lic2.ExpiresAt) - } -} - -// TestPersistPaidRenewal 续费叠加:未过期顺延(base=现有到期),已过期从当前时间起算 -func TestPersistPaidRenewal(t *testing.T) { - phone := uniquePhone() - order1 := uniqueOrderId("O-test-renew-1-") - insertOrder(t, &entity.PaymentOrder{ - OrderId: order1, - PhoneNum: phone, - PlanId: "week", - Channel: consts.ChannelAlipay, - AmountCents: 5600, - Status: consts.OrderStatusCreated, - CreatedAt: gtime.Now(), - }) - if err := Payment.persistPaid(ctx(), order1, uniqueOrderId("ali-trade-"), consts.ChannelAlipay, 5600); err != nil { - t.Fatalf("persistPaid 1: %v", err) - } - lic, _ := dao.License.GetByPhone(ctx(), phone) - - // 第二次购买 month:base = 现有到期(未过期),30 天叠加 - order2 := uniqueOrderId("O-test-renew-2-") - insertOrder(t, &entity.PaymentOrder{ - OrderId: order2, - PhoneNum: phone, - PlanId: "month", - Channel: consts.ChannelWechat, - AmountCents: 18000, - Status: consts.OrderStatusCreated, - CreatedAt: gtime.Now(), - }) - if err := Payment.persistPaid(ctx(), order2, uniqueOrderId("wx-trade-"), consts.ChannelWechat, 0); err != nil { - t.Fatalf("persistPaid 2: %v", err) - } - lic2, _ := dao.License.GetByPhone(ctx(), phone) - want := lic.ExpiresAt.Time.AddDate(0, 0, 30) - if !lic2.ExpiresAt.Time.Equal(want) { - t.Fatalf("renewal expiresAt = %s, want %s (base %s)", lic2.ExpiresAt, want, lic.ExpiresAt) - } - // 购买的套餐记录在 payment_order,license 只留到期时间(此处验证第二次下单的订单快照) - o2, _ := dao.PaymentOrder.GetByPk(ctx(), order2) - if o2 == nil || o2.PlanId != "month" { - t.Fatalf("order plan snapshot = %v, want month", o2) - } - - // 支付宝回调金额不匹配 → 拒绝 - order3 := uniqueOrderId("O-test-renew-3-") - insertOrder(t, &entity.PaymentOrder{ - OrderId: order3, - PhoneNum: uniquePhone(), - PlanId: "day", - Channel: consts.ChannelAlipay, - AmountCents: 1000, - Status: consts.OrderStatusCreated, - CreatedAt: gtime.Now(), - }) - if err := Payment.persistPaid(ctx(), order3, uniqueOrderId("ali-trade-"), consts.ChannelAlipay, 999); err == nil { - t.Fatal("amount mismatch should fail") - } -} - -// TestCreateSequence 订单创建串行序列:惰性关闭超时订单 + 2 小时内复用 created 订单 -func TestCreateSequence(t *testing.T) { - phone := uniquePhone() - orderStale := uniqueOrderId("O-test-stale-") - orderRecent := uniqueOrderId("O-test-recent-") - // 3 小时前的 stale 订单 + 1 分钟前的有效订单 - now := gtime.Now() - insertOrder(t, &entity.PaymentOrder{ - OrderId: orderStale, PhoneNum: phone, PlanId: "day", Channel: consts.ChannelWechat, - AmountCents: 1000, Status: consts.OrderStatusCreated, - CreatedAt: gtime.New(now.Time.Add(-3 * time.Hour)), - }) - insertOrder(t, &entity.PaymentOrder{ - OrderId: orderRecent, PhoneNum: phone, PlanId: "day", Channel: consts.ChannelWechat, - AmountCents: 1000, Status: consts.OrderStatusCreated, - CreatedAt: gtime.New(now.Time.Add(-1 * time.Minute)), - }) - - // 复现 Create 的串行事务序列 - var recent *entity.PaymentOrder - err := common.Serial().Submit(ctx(), func() error { - return g.DB().Transaction(ctx(), func(ctx context.Context, tx gdb.TX) error { - window := gtime.New(now.Time.Add(-consts.PendingOrderWindowSeconds * time.Second)) - if err := dao.PaymentOrder.CloseStaleCreatedInTx(ctx, tx, phone, window); err != nil { - return err - } - var err error - recent, err = dao.PaymentOrder.GetRecentCreatedInTx(ctx, tx, phone, window) - return err - }) - }) - if err != nil { - t.Fatal(err) - } - - stale, _ := dao.PaymentOrder.GetByPk(ctx(), orderStale) - if stale.Status != consts.OrderStatusClosed { - t.Fatalf("stale order status = %s, want closed", stale.Status) - } - if recent == nil || recent.OrderId != orderRecent { - t.Fatalf("recent = %+v, want %s", recent, orderRecent) - } -} - -// TestTradeNoNullable 未支付订单 trade_no 必须落 NULL:UNIQUE 约束下多个 created 订单(同渠道不同账号)可共存 -func TestTradeNoNullable(t *testing.T) { - for _, channel := range []string{consts.ChannelAlipay, consts.ChannelAlipay, consts.ChannelWechat, consts.ChannelWechat} { - insertOrder(t, &entity.PaymentOrder{ - OrderId: uniqueOrderId("O-test-trade-"), PhoneNum: uniquePhone(), PlanId: "day", Channel: channel, - AmountCents: 1000, Status: consts.OrderStatusCreated, CreatedAt: gtime.Now(), - }) - } -} - -// TestCalcExpiresAt 自然日语义:day 当天 24 点失效、week/month 自生效日起累计 -func TestCalcExpiresAt(t *testing.T) { - base := time.Date(2026, 8, 22, 15, 30, 0, 0, time.Local) - start := time.Date(2026, 8, 22, 0, 0, 0, 0, time.Local) - cases := []struct { - days int - want time.Time - }{ - {1, start.AddDate(0, 0, 1)}, // 当天 24:00 - {7, start.AddDate(0, 0, 7)}, // 第 7 天 24:00 - {30, start.AddDate(0, 0, 30)}, - } - for _, c := range cases { - got := License.CalcExpiresAt(base, c.days) - if !got.Time.Equal(c.want) { - t.Fatalf("CalcExpiresAt(%d) = %s, want %s", c.days, got, c.want) - } - } -} diff --git a/server/biz/service/testdata/.gitignore b/server/biz/service/testdata/.gitignore deleted file mode 100644 index 1befdd6..0000000 --- a/server/biz/service/testdata/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# 测试运行时产物(APK 上传目录),不入库 -workspace/ diff --git a/server/biz/service/testdata/config.yml b/server/biz/service/testdata/config.yml deleted file mode 100644 index 3c62274..0000000 --- a/server/biz/service/testdata/config.yml +++ /dev/null @@ -1,33 +0,0 @@ -# 服务层白盒测试专用配置(独立临时库,不提交运行数据) -# 运行方式: cd server && GF_GCFG_FILE=biz/service/testdata/config.yml go test ./biz/service/ -server: - address: ":0" - -database: - default: - type: sqlite - link: "sqlite::@file(testdata/observer_test.db)" - cache: - ttl: 30 - -auth: - secret: "test-auth-secret" - tokenTtl: 2592000 - -admin: - token: "test-admin-token" - -plans: - - id: day - days: 1 - price_cents: 1000 - - id: week - days: 7 - price_cents: 5600 - - id: month - days: 30 - price_cents: 18000 - -# 测试 APK 目录(testdata 下,避免污染仓库根) -app: - apkDir: "./testdata/workspace" diff --git a/server/data/observer.db b/server/data/observer.db index 2e485d5..35c7705 100644 Binary files a/server/data/observer.db and b/server/data/observer.db differ