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") } }