package config import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "software-download-center/utils" "github.com/gin-gonic/gin" ) func TestUpdateInfoContainsOnlyFullInstallerAndMSIX(t *testing.T) { gin.SetMode(gin.TestMode) rootDir := t.TempDir() publicDir := filepath.Join(rootDir, "public") downloadsDir := filepath.Join(publicDir, "downloads") viewsDir := filepath.Join(rootDir, "views") mustMkdirAll(t, downloadsDir) mustMkdirAll(t, viewsDir) writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0.exe", "installer") writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_2.0.7.0_x64.msix", "msix") writeTestFile(t, downloadsDir, "winui.appinstaller", "appinstaller") writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0_Light.exe", "light") writeTestFile(t, filepath.Join(downloadsDir, "incremental"), "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", "incremental") writeTestFile(t, publicDir, "update-info.json", `{ "baselineVersion": "2.0.6.0", "minIncrementalVersion": "2.0.6.0", "lightInstaller": {"fileName":"light.exe"}, "packages": [{"id":"external-tools"}], "incrementals": [{"id":"delta"}], "messages": {"static":"kept"} }`) writeTemplateFiles(t, viewsDir) router := buildTestRouter(t, rootDir) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, httptest.NewRequest("GET", "/update-info.json", nil)) if recorder.Code != http.StatusOK { t.Fatalf("expected update-info 200, got %d: %s", recorder.Code, recorder.Body.String()) } var body map[string]interface{} if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatal(err) } assertAbsent(t, body, "baselineVersion", "minIncrementalVersion", "lightInstaller", "packages", "incrementals", "requiredForFull", "modules") if body["latestVersion"] != "2.0.7.0" { t.Fatalf("expected latestVersion 2.0.7.0, got %#v", body["latestVersion"]) } latest := body["latest"].(map[string]interface{}) assertAbsent(t, latest, "lightInstaller", "incrementals", "packages") fullInstaller := latest["fullInstaller"].(map[string]interface{}) if fullInstaller["fileName"] != "YMhut_Box_WinUI_Setup_2.0.7.0.exe" { t.Fatalf("unexpected full installer: %#v", fullInstaller) } msix := latest["msix"].(map[string]interface{}) if msix["fileName"] != "YMhut_Box_WinUI_2.0.7.0_x64.msix" { t.Fatalf("unexpected msix: %#v", msix) } appInstaller := latest["appInstaller"].(map[string]interface{}) if appInstaller["fileName"] != "winui.appinstaller" { t.Fatalf("unexpected appinstaller: %#v", appInstaller) } text := recorder.Body.String() for _, forbidden := range []string{"_Light.exe", "YMhut_Box_Update_", "external-tools", "baselineVersion", "incrementals"} { if strings.Contains(text, forbidden) { t.Fatalf("update-info leaked removed field/content %q: %s", forbidden, text) } } } func TestRemovedDistributionRoutesReturnGone(t *testing.T) { gin.SetMode(gin.TestMode) rootDir := t.TempDir() publicDir := filepath.Join(rootDir, "public") downloadsDir := filepath.Join(publicDir, "downloads") viewsDir := filepath.Join(rootDir, "views") mustMkdirAll(t, downloadsDir) mustMkdirAll(t, viewsDir) writeTemplateFiles(t, viewsDir) router := buildTestRouter(t, rootDir) for _, route := range []string{ "/modules.json", "/api/modules", "/package-manifest.json", "/incremental-manifest.json", "/api/packages/manifest", "/api/incrementals", "/api/incrementals/manifest", "/api/packages/download/YMhut_Box_Tools_2.0.6.0.zip", "/api/incrementals/download/YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", "/packages/YMhut_Box_Tools_2.0.6.0.zip", "/tool-packages/YMhut_Box_Tools_2.0.6.0.zip", } { recorder := httptest.NewRecorder() router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil)) if recorder.Code != http.StatusGone { t.Fatalf("expected %s to return 410, got %d: %s", route, recorder.Code, recorder.Body.String()) } } } func TestCanonicalUpdateInfoRoutesServeDirectlyAndLegacyRoutesRedirect(t *testing.T) { gin.SetMode(gin.TestMode) rootDir := t.TempDir() publicDir := filepath.Join(rootDir, "public") downloadsDir := filepath.Join(publicDir, "downloads") viewsDir := filepath.Join(rootDir, "views") mustMkdirAll(t, downloadsDir) mustMkdirAll(t, viewsDir) writeTemplateFiles(t, viewsDir) router := buildTestRouter(t, rootDir) for _, route := range []string{"/update-info.json", "/update-info", "/api/update-info"} { recorder := httptest.NewRecorder() router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil)) if recorder.Code != http.StatusOK { t.Fatalf("expected %s to return 200, got %d: %s", route, recorder.Code, recorder.Body.String()) } } manifestRecorder := httptest.NewRecorder() router.ServeHTTP(manifestRecorder, httptest.NewRequest("GET", "/manifest.json", nil)) if manifestRecorder.Code != http.StatusOK { t.Fatalf("expected /manifest.json compatibility response 200, got %d: %s", manifestRecorder.Code, manifestRecorder.Body.String()) } if manifestRecorder.Header().Get("Deprecation") != "true" { t.Fatalf("expected /manifest.json to include Deprecation header") } if manifestRecorder.Header().Get("X-YMhut-Canonical-Manifest") != "/update-info.json" { t.Fatalf("expected /manifest.json to point at /update-info.json") } for _, route := range []string{"/latest-version.json", "/api/releases/latest", "/latest.json"} { recorder := httptest.NewRecorder() router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil)) if recorder.Code != http.StatusMovedPermanently { t.Fatalf("expected %s to redirect with 301, got %d", route, recorder.Code) } if location := recorder.Header().Get("Location"); location != "/update-info.json" { t.Fatalf("expected %s Location /update-info.json, got %q", route, location) } } } func TestDownloadsRouteAllowsOnlySingleInstallerArtifact(t *testing.T) { gin.SetMode(gin.TestMode) rootDir := t.TempDir() publicDir := filepath.Join(rootDir, "public") downloadsDir := filepath.Join(publicDir, "downloads") viewsDir := filepath.Join(rootDir, "views") mustMkdirAll(t, downloadsDir) mustMkdirAll(t, viewsDir) writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0.exe", "installer") writeTestFile(t, downloadsDir, "YMhut_Box_WinUI_Setup_2.0.7.0_Light.exe", "light") writeTestFile(t, filepath.Join(downloadsDir, "incremental"), "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", "incremental") writeTemplateFiles(t, viewsDir) router := buildTestRouter(t, rootDir) okRecorder := httptest.NewRecorder() router.ServeHTTP(okRecorder, httptest.NewRequest("GET", "/downloads/YMhut_Box_WinUI_Setup_2.0.7.0.exe", nil)) if okRecorder.Code != http.StatusOK { t.Fatalf("expected full installer download 200, got %d: %s", okRecorder.Code, okRecorder.Body.String()) } if okRecorder.Body.String() != "installer" { t.Fatalf("unexpected installer body: %q", okRecorder.Body.String()) } for _, route := range []string{ "/downloads/YMhut_Box_WinUI_Setup_2.0.7.0_Light.exe", "/downloads/incremental/YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", "/downloads/..%2Fsecret.exe", "/downloads/subdir/file.exe", } { recorder := httptest.NewRecorder() router.ServeHTTP(recorder, httptest.NewRequest("GET", route, nil)) if recorder.Code != http.StatusForbidden { t.Fatalf("expected %s to be forbidden, got %d: %s", route, recorder.Code, recorder.Body.String()) } } } func TestAdminAPIRequiresAuthentication(t *testing.T) { gin.SetMode(gin.TestMode) rootDir := t.TempDir() publicDir := filepath.Join(rootDir, "public") downloadsDir := filepath.Join(publicDir, "downloads") viewsDir := filepath.Join(rootDir, "views") mustMkdirAll(t, downloadsDir) mustMkdirAll(t, viewsDir) writeTemplateFiles(t, viewsDir) router := buildTestRouter(t, rootDir) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, httptest.NewRequest("GET", "/api/admin/releases/files", nil)) if recorder.Code != http.StatusUnauthorized { t.Fatalf("expected 401 for unauthenticated admin API, got %d: %s", recorder.Code, recorder.Body.String()) } if !strings.Contains(recorder.Body.String(), "UNAUTHORIZED") { t.Fatalf("expected structured unauthorized response, got %s", recorder.Body.String()) } } func TestAdminPageShowsUnauthorizedShellWithoutAuth(t *testing.T) { gin.SetMode(gin.TestMode) rootDir := t.TempDir() publicDir := filepath.Join(rootDir, "public") downloadsDir := filepath.Join(publicDir, "downloads") viewsDir := filepath.Join(rootDir, "views") mustMkdirAll(t, downloadsDir) mustMkdirAll(t, viewsDir) writeTemplateFiles(t, viewsDir) router := buildTestRouter(t, rootDir) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, httptest.NewRequest("GET", "/admin/", nil)) if recorder.Code != http.StatusOK { t.Fatalf("expected admin shell 200, got %d: %s", recorder.Code, recorder.Body.String()) } if !strings.Contains(recorder.Body.String(), "未授权") { t.Fatalf("expected admin shell fallback to show unauthorized copy, got %s", recorder.Body.String()) } } func TestComparePackageFileNamesSupportsNewAndLegacyInstallerNames(t *testing.T) { if comparePackageFileNames("YMhut_Box_WinUI_Setup_2.0.7.0.exe", "YMhut_Box_Setup_2.0.6.0.exe") <= 0 { t.Fatal("expected WinUI 2.0.7.0 installer to be newer than legacy 2.0.6.0 installer") } if comparePackageFileNames("YMhut_Box_Setup_2.0.7.0.exe", "YMhut_Box_WinUI_Setup_2.0.7.0.exe") == 0 { t.Fatal("expected stable tie-breaker for same-version installer names") } } func TestProductsInfoRecognizesMSIXAndSkipsIncrementalSubdirectory(t *testing.T) { dir := t.TempDir() writeTestFile(t, dir, "YMhut_Box_WinUI_2.0.7.0_x64.msix", "msix") writeTestFile(t, filepath.Join(dir, "incremental"), "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip", "incremental") products := utils.GetProductsInfo(dir, utils.NewLogger()) if len(products) == 0 { t.Fatalf("expected MSIX package to be detected") } for _, releases := range products { for _, release := range releases { if release.FileName == "YMhut_Box_Update_2.0.6.0_to_2.0.7.0.zip" { t.Fatalf("incremental package from subdirectory should not be listed as product: %#v", products) } } } } func buildTestRouter(t *testing.T, rootDir string) *gin.Engine { t.Helper() oldWorkingDir, err := os.Getwd() if err != nil { t.Fatal(err) } if err := os.Chdir(rootDir); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Chdir(oldWorkingDir) }) router := gin.New() RegisterRoutes(router, utils.NewLogger()) return router } func writeTemplateFiles(t *testing.T, dir string) { t.Helper() writeTestFile(t, dir, "index.html", "{{.pageTitle}}") writeTestFile(t, dir, "404.html", "{{.title}}") writeTestFile(t, dir, "500.html", "{{.title}}") } func writeTestFile(t *testing.T, dir string, name string, content string) { t.Helper() if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil { t.Fatalf("write %s: %v", name, err) } } func mustMkdirAll(t *testing.T, dir string) { t.Helper() if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } } func assertAbsent(t *testing.T, body map[string]interface{}, keys ...string) { t.Helper() for _, key := range keys { if _, ok := body[key]; ok { t.Fatalf("expected field %s to be absent in %#v", key, body) } } }