package version import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "runtime" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // fullTestManifest is a release manifest with a newer manager version and // assets for every supported platform. const fullTestManifest = `{ "version": "9.9.9", "date": "2026-08-01", "MpvVersion": "0.41.0", "uosc": {"app_version": "5.10.0"}, "modernz": {"app_version": "0.4.0"}, "ffmpeg": {"app_version": "20260801"}, "mpc-qt": {"app_version": "23.0"}, "iina": {"app_version": "1.4.0"}, "manager": { "linux-amd64": {"URL": "https://example.com/mpv-manager-linux-amd64", "BLAKE3": "blake3:linuxamd64"}, "linux-arm64": {"URL": "https://example.com/mpv-manager-linux-arm64", "BLAKE3": "blake3:linuxarm64"}, "win-x86_64": {"URL": "https://example.com/mpv-manager-win-x86_64.exe", "BLAKE3": "blake3:winx64"}, "win-arm64": {"URL": "https://example.com/mpv-manager-win-arm64.exe", "BLAKE3": "blake3:winarm64"}, "macos-intel": {"URL": "https://example.com/mpv-manager-macos-intel", "BLAKE3": "blake3:macintel"}, "macos-arm": {"URL": "https://example.com/mpv-manager-macos-arm", "BLAKE3": "blake3:macarm"} } }` func manifestServer(t *testing.T, status int, body string) *httptest.Server { t.Helper() if status == http.StatusOK && body != "" && json.Valid([]byte(body)) { var candidate ReleaseInfo if err := json.Unmarshal([]byte(body), &candidate); err == nil && candidate.Version != "" { body = signedManifestJSON(t, body) } } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if body != "" { w.Header().Set("Content-Type", "application/json") } w.WriteHeader(status) if body != "" { fmt.Fprint(w, body) } })) t.Cleanup(server.Close) return server } func TestCheckForUpdateFrom(t *testing.T) { t.Run("newer version with platform asset offers update", func(t *testing.T) { server := manifestServer(t, http.StatusOK, fullTestManifest) result := checkForUpdateFrom(context.Background(), server.URL) require.Nil(t, result.Error) assert.Equal(t, CurrentVersion, result.CurrentVersion) assert.Equal(t, "9.9.9", result.LatestVersion) assert.Equal(t, "0.41.0", result.MpvVersion) assert.Equal(t, "5.10.0", result.UOSCLatestVersion) assert.Equal(t, "0.4.0", result.ModernZLatestVersion) assert.Equal(t, "20260801", result.FFmpegLatestVersion) assert.Equal(t, "23.0", result.MPQTLLatestVersion) assert.Equal(t, "1.4.0", result.INALatestVersion) assert.True(t, result.UpdateAvailable) assert.NotEmpty(t, result.URL) assert.NotEmpty(t, result.BLAKE3) }) t.Run("same version does not offer update", func(t *testing.T) { server := manifestServer(t, http.StatusOK, `{"version": "`+CurrentVersion+`", "manager": {"linux-amd64": {"URL": "https://example.com/x", "BLAKE3": "blake3:x"}, "linux-arm64": {"URL": "https://example.com/x", "BLAKE3": "blake3:x"}, "win-x86_64": {"URL": "https://example.com/x", "BLAKE3": "blake3:x"}, "win-arm64": {"URL": "https://example.com/x", "BLAKE3": "blake3:x"}, "macos-intel": {"URL": "https://example.com/x", "BLAKE3": "blake3:x"}, "macos-arm": {"URL": "https://example.com/x", "BLAKE3": "blake3:x"}}}`) result := checkForUpdateFrom(context.Background(), server.URL) require.Nil(t, result.Error) assert.Equal(t, CurrentVersion, result.LatestVersion) assert.False(t, result.UpdateAvailable) }) t.Run("unsigned manifest is rejected", func(t *testing.T) { previous := ManifestPublicKeys ManifestPublicKeys = "" t.Cleanup(func() { ManifestPublicKeys = previous }) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, `{"version":"9.9.9"}`) })) t.Cleanup(server.Close) result := checkForUpdateFrom(context.Background(), server.URL) require.Error(t, result.Error) assert.Contains(t, result.Error.Error(), "no trusted release manifest keys") assert.False(t, result.UpdateAvailable) }) t.Run("self-update disabled hides update but keeps latest version", func(t *testing.T) { previous := SelfUpdateDisabled SelfUpdateDisabled = "true" t.Cleanup(func() { SelfUpdateDisabled = previous }) server := manifestServer(t, http.StatusOK, fullTestManifest) result := checkForUpdateFrom(context.Background(), server.URL) require.Nil(t, result.Error) assert.Equal(t, "9.9.9", result.LatestVersion) assert.False(t, result.UpdateAvailable) assert.Empty(t, result.URL) }) t.Run("non-200 status surfaces an error", func(t *testing.T) { server := manifestServer(t, http.StatusBadGateway, "") result := checkForUpdateFrom(context.Background(), server.URL) require.NotNil(t, result.Error) assert.Contains(t, result.Error.Error(), "HTTP 502") assert.False(t, result.UpdateAvailable) }) t.Run("malformed JSON surfaces a parse error", func(t *testing.T) { server := manifestServer(t, http.StatusOK, `{not json`) result := checkForUpdateFrom(context.Background(), server.URL) require.NotNil(t, result.Error) assert.Contains(t, result.Error.Error(), "decode release manifest") }) t.Run("unreachable server surfaces a network error", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) url := server.URL server.Close() result := checkForUpdateFrom(context.Background(), url) require.NotNil(t, result.Error) assert.Contains(t, result.Error.Error(), "failed to check for updates") assert.Equal(t, CurrentVersion, result.CurrentVersion) }) } // scriptBinary is a runnable stand-in for a downloaded update on Unix-like // systems. Successful fixtures report the exact identity expected by the // transactional updater tests. func scriptBinary(exitCode int) []byte { return scriptBinaryWithIdentity(exitCode, BinaryIdentity{ Product: ProductID, Component: ComponentID, Version: "9.9.9", GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, }) } func scriptBinaryWithIdentity(exitCode int, identity BinaryIdentity) []byte { payload, err := json.Marshal(identity) if err != nil { panic(err) } return []byte(fmt.Sprintf("#!/bin/sh\nprintf '%%s\\n' '%s'\nexit %d\n", payload, exitCode)) }