317 lines
11 KiB
Go
317 lines
11 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|