package releases import ( "errors" "net/http/httptest" "os" "path/filepath" "strings" "testing" "ymhut-box/server/unified-management/internal/config" "ymhut-box/server/unified-management/internal/db" ) func TestCompareVersion(t *testing.T) { cases := []struct { a string b string want int }{ {"2.0.6.31", "2.0.6.2", 1}, {"2.0.10", "2.0.9", 1}, {"2.0.6.2", "2.0.6.31", -1}, {"2.0.6", "2.0.6.0", 0}, } for _, tc := range cases { if got := compareVersion(tc.a, tc.b); got != tc.want { t.Fatalf("compareVersion(%q, %q) = %d, want %d", tc.a, tc.b, got, tc.want) } } } func TestDetectPackageMetadata(t *testing.T) { platform, arch := detectPlatform("YMhutBox_2.0.6.31_x64.msix") if platform != "windows" || arch != "x64" { t.Fatalf("detectPlatform returned %s/%s", platform, arch) } if version := detectVersion("YMhut_Box_WinUI_Setup_2.0.6.31.exe"); version != "2.0.6.31" { t.Fatalf("detectVersion returned %q", version) } } func TestScanPackagesUsesCDNAndCachesHashes(t *testing.T) { dir := t.TempDir() name := "YMhut_Box_WinUI_Setup_2.0.7_x64.exe" if err := os.WriteFile(filepath.Join(dir, name), []byte("stable package"), 0o640); err != nil { t.Fatal(err) } service := NewService(&config.Config{ DownloadsDir: dir, BaseURL: "https://update.ymhut.cn", CDNBaseURL: "https://cdn.ymhut.cn/", }, nil) req := httptest.NewRequest("GET", "https://origin.example/api/client/releases", nil) first := service.ScanPackages(req) second := service.ScanPackages(req) if len(first) != 1 || first[0].URL != "https://cdn.ymhut.cn/downloads/"+name { t.Fatalf("unexpected CDN package: %#v", first) } if len(second) != 1 || first[0].SHA256 == "" || second[0].SHA256 != first[0].SHA256 { t.Fatalf("cached hashes differ: first=%#v second=%#v", first, second) } if len(service.hashes) != 1 { t.Fatalf("expected one cached hash, got %d", len(service.hashes)) } } func TestSaveUploadedPackageWritesFileAndUpdatesManifest(t *testing.T) { dir := t.TempDir() cfg := &config.Config{ BaseDir: dir, StorageDir: filepath.Join(dir, "storage"), DataDir: filepath.Join(dir, "data"), UpdatePublicDir: filepath.Join(dir, "data", "update", "public"), DownloadsDir: filepath.Join(dir, "data", "update", "public", "downloads"), BaseURL: "https://update.ymhut.cn", Database: config.DatabaseConfig{ Provider: "sqlite", SQLitePath: filepath.Join(dir, "storage", "unified.sqlite"), HealthIntervalSec: 30, }, } store, err := db.Open(cfg) if err != nil { t.Fatal(err) } defer store.Close() service := NewService(cfg, store) req := httptest.NewRequest("POST", "https://update.ymhut.cn/api/admin/releases/packages", nil) pkg, err := service.SaveUploadedPackage(req, strings.NewReader("package bytes"), UploadOptions{ FileName: "YMhut_Box_WinUI_Setup_2.0.6.31.exe", UpdateManifest: true, }, "admin") if err != nil { t.Fatal(err) } if pkg.Version != "2.0.6.31" || pkg.SHA256 == "" || pkg.Size == 0 { t.Fatalf("unexpected package metadata: %#v", pkg) } if _, err := os.Stat(filepath.Join(cfg.DownloadsDir, pkg.FileName)); err != nil { t.Fatal(err) } manifest := readJSON(filepath.Join(cfg.UpdatePublicDir, "update-info.json")) if manifest["download_url"] != pkg.URL || manifest["package_sha256"] != pkg.SHA256 { t.Fatalf("manifest not updated: %#v", manifest) } } func TestSaveUploadedPackageRejectsUnsafeName(t *testing.T) { dir := t.TempDir() cfg := &config.Config{ BaseDir: dir, StorageDir: filepath.Join(dir, "storage"), DataDir: filepath.Join(dir, "data"), UpdatePublicDir: filepath.Join(dir, "data", "update", "public"), DownloadsDir: filepath.Join(dir, "data", "update", "public", "downloads"), Database: config.DatabaseConfig{ Provider: "sqlite", SQLitePath: filepath.Join(dir, "storage", "unified.sqlite"), HealthIntervalSec: 30, }, } store, err := db.Open(cfg) if err != nil { t.Fatal(err) } defer store.Close() service := NewService(cfg, store) _, err = service.SaveUploadedPackage(httptest.NewRequest("POST", "/", nil), strings.NewReader("x"), UploadOptions{FileName: "../evil.exe"}, "admin") if err == nil { t.Fatal("expected unsafe filename to be rejected") } } func TestSavePreparedPackageReplacesExistingFile(t *testing.T) { service, cfg, cleanup := newPreparedPackageTestService(t) defer cleanup() name := "YMhut_Box_WinUI_Setup_2.0.8_x64.exe" target := filepath.Join(cfg.DownloadsDir, name) if err := os.MkdirAll(cfg.DownloadsDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(target, []byte("old package"), 0o640); err != nil { t.Fatal(err) } temp := filepath.Join(cfg.DownloadsDir, ".upload-new") if err := os.WriteFile(temp, []byte("new package"), 0o640); err != nil { t.Fatal(err) } _, err := service.SavePreparedPackage( httptest.NewRequest("POST", "https://update.ymhut.cn/api/admin/releases/packages", nil), UploadedPackageFile{TempPath: temp, Size: int64(len("new package")), SHA256: "abc123"}, UploadOptions{FileName: name}, "admin") if err != nil { t.Fatal(err) } data, err := os.ReadFile(target) if err != nil { t.Fatal(err) } if string(data) != "new package" { t.Fatalf("target contains %q, want new package", data) } backups, err := filepath.Glob(filepath.Join(cfg.DownloadsDir, "."+name+".backup-*")) if err != nil { t.Fatal(err) } if len(backups) != 0 { t.Fatalf("successful replacement left backups: %v", backups) } } func TestSavePreparedPackageRestoresExistingFileWhenManifestFails(t *testing.T) { service, cfg, cleanup := newPreparedPackageTestService(t) defer cleanup() name := "YMhut_Box_WinUI_Setup_2.0.8_x64.exe" if err := os.MkdirAll(cfg.DownloadsDir, 0o755); err != nil { t.Fatal(err) } target := filepath.Join(cfg.DownloadsDir, name) if err := os.WriteFile(target, []byte("old package"), 0o640); err != nil { t.Fatal(err) } temp := filepath.Join(cfg.DownloadsDir, ".upload-new") if err := os.WriteFile(temp, []byte("new package"), 0o640); err != nil { t.Fatal(err) } blocked := filepath.Join(cfg.BaseDir, "manifest-blocked") if err := os.WriteFile(blocked, []byte("not a directory"), 0o640); err != nil { t.Fatal(err) } cfg.UpdatePublicDir = blocked _, err := service.SavePreparedPackage( httptest.NewRequest("POST", "https://update.ymhut.cn/api/admin/releases/packages", nil), UploadedPackageFile{TempPath: temp, Size: int64(len("new package")), SHA256: "abc123"}, UploadOptions{FileName: name, UpdateManifest: true}, "admin") if !errors.Is(err, ErrUploadedPackageManifestFailed) { t.Fatalf("got %v, want manifest failure", err) } data, readErr := os.ReadFile(target) if readErr != nil { t.Fatal(readErr) } if string(data) != "old package" { t.Fatalf("rollback restored %q, want old package", data) } } func newPreparedPackageTestService(t *testing.T) (*Service, *config.Config, func()) { t.Helper() dir := t.TempDir() cfg := &config.Config{ BaseDir: dir, StorageDir: filepath.Join(dir, "storage"), DataDir: filepath.Join(dir, "data"), UpdatePublicDir: filepath.Join(dir, "data", "update", "public"), DownloadsDir: filepath.Join(dir, "data", "update", "public", "downloads"), BaseURL: "https://update.ymhut.cn", Database: config.DatabaseConfig{ Provider: "sqlite", SQLitePath: filepath.Join(dir, "storage", "unified.sqlite"), HealthIntervalSec: 30, }, } store, err := db.Open(cfg) if err != nil { t.Fatal(err) } return NewService(cfg, store), cfg, func() { _ = store.Close() } }