package version import ( "bytes" "net/http" "os" "path/filepath" "runtime" "testing" "gitgud.io/mike/mpv-manager/pkg/releasemanifest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type roundTripFunc func(*http.Request) (*http.Response, error) func (fn roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return fn(request) } func TestCleanupStaleUpdateFiles(t *testing.T) { writeFile := func(t *testing.T, path, content string) { t.Helper() require.NoError(t, os.WriteFile(path, []byte(content), 0755)) } t.Run("restores missing executable from backup", func(t *testing.T) { dir := t.TempDir() exe := filepath.Join(dir, "mpv-manager") writeFile(t, backupBinaryPath(exe), "previous-version") CleanupStaleUpdateFiles(exe) content, err := os.ReadFile(exe) require.NoError(t, err) assert.Equal(t, "previous-version", string(content)) assert.NoFileExists(t, backupBinaryPath(exe)) }) t.Run("restores backup and removes orphaned staged download", func(t *testing.T) { dir := t.TempDir() exe := filepath.Join(dir, "mpv-manager") writeFile(t, backupBinaryPath(exe), "previous-version") writeFile(t, newBinaryPath(exe), "partial-download") CleanupStaleUpdateFiles(exe) content, err := os.ReadFile(exe) require.NoError(t, err) assert.Equal(t, "previous-version", string(content)) assert.NoFileExists(t, backupBinaryPath(exe)) assert.NoFileExists(t, newBinaryPath(exe)) }) t.Run("removes stale backup when executable exists", func(t *testing.T) { dir := t.TempDir() exe := filepath.Join(dir, "mpv-manager") writeFile(t, exe, "new-version") writeFile(t, backupBinaryPath(exe), "previous-version") CleanupStaleUpdateFiles(exe) content, err := os.ReadFile(exe) require.NoError(t, err) assert.Equal(t, "new-version", string(content), "running executable must not be touched") assert.NoFileExists(t, backupBinaryPath(exe)) }) t.Run("removes orphaned staged download when executable exists", func(t *testing.T) { dir := t.TempDir() exe := filepath.Join(dir, "mpv-manager") writeFile(t, exe, "current-version") writeFile(t, newBinaryPath(exe), "partial-download") CleanupStaleUpdateFiles(exe) assert.NoFileExists(t, newBinaryPath(exe)) content, err := os.ReadFile(exe) require.NoError(t, err) assert.Equal(t, "current-version", string(content)) }) t.Run("leaves a clean install untouched", func(t *testing.T) { dir := t.TempDir() exe := filepath.Join(dir, "mpv-manager") writeFile(t, exe, "current-version") CleanupStaleUpdateFiles(exe) content, err := os.ReadFile(exe) require.NoError(t, err) assert.Equal(t, "current-version", string(content)) }) t.Run("missing executable without backup is a no-op", func(t *testing.T) { dir := t.TempDir() exe := filepath.Join(dir, "mpv-manager") CleanupStaleUpdateFiles(exe) assert.NoFileExists(t, exe) }) t.Run("empty path is a no-op", func(t *testing.T) { CleanupStaleUpdateFiles("") }) } func TestSelectManagerAsset(t *testing.T) { newerRelease := func() *ReleaseInfo { release := &ReleaseInfo{Version: "99.0.0"} release.Components = map[string]releasemanifest.Component{ ComponentID: { Version: "99.0.0", Assets: []releasemanifest.Asset{{ GOOS: "linux", GOARCH: "amd64", URL: "https://example.com/mpv-manager-linux-amd64", BLAKE3: "blake3:abc123", }}, }, } return release } t.Run("known platform with newer version gets update", func(t *testing.T) { result := &VersionCheckResult{} require.NoError(t, selectManagerAsset(result, newerRelease(), "linux", "amd64")) assert.Equal(t, "https://example.com/mpv-manager-linux-amd64", result.URL) assert.Equal(t, "blake3:abc123", result.BLAKE3) assert.True(t, result.UpdateAvailable) }) t.Run("unrecognized platform gets no update even when newer version exists", func(t *testing.T) { result := &VersionCheckResult{} require.Error(t, selectManagerAsset(result, newerRelease(), "plan9", "amd64")) assert.Empty(t, result.URL) assert.Empty(t, result.BLAKE3) assert.False(t, result.UpdateAvailable) }) t.Run("known platform without an asset URL gets no update", func(t *testing.T) { result := &VersionCheckResult{} // windows/arm64 fields are left empty in this fixture require.Error(t, selectManagerAsset(result, newerRelease(), "windows", "arm64")) assert.Empty(t, result.URL) assert.False(t, result.UpdateAvailable) }) t.Run("same version on known platform is not an update", func(t *testing.T) { release := newerRelease() release.Version = CurrentVersion component := release.Components[ComponentID] component.Version = CurrentVersion release.Components[ComponentID] = component result := &VersionCheckResult{} require.NoError(t, selectManagerAsset(result, release, "linux", "amd64")) assert.NotEmpty(t, result.URL) assert.False(t, result.UpdateAvailable) }) } func TestVerifyUpdatedBinary(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("shell-script fixtures are not executable on Windows") } writeScript := func(t *testing.T, body string) string { t.Helper() path := filepath.Join(t.TempDir(), "fake-binary") require.NoError(t, os.WriteFile(path, []byte(body), 0755)) return path } expected := BinaryIdentity{ Product: ProductID, Component: ComponentID, Version: "9.9.9", GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, } t.Run("accepts the exact selected binary", func(t *testing.T) { path := writeScript(t, string(scriptBinaryWithIdentity(0, expected))) assert.NoError(t, verifyUpdatedBinary(path, expected)) }) t.Run("rejects a binary that exits non-zero", func(t *testing.T) { path := writeScript(t, "#!/bin/sh\nexit 1\n") err := verifyUpdatedBinary(path, expected) require.Error(t, err) assert.Contains(t, err.Error(), "--version --json check") }) t.Run("rejects invalid JSON", func(t *testing.T) { path := writeScript(t, "#!/bin/sh\nprintf 'not-json\\n'\n") err := verifyUpdatedBinary(path, expected) require.Error(t, err) assert.Contains(t, err.Error(), "invalid version identity") }) t.Run("bounds identity stdout", func(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("uses POSIX shell fixtures") } path := writeScript(t, "#!/bin/sh\nhead -c 70000 /dev/zero | tr '\\000' x\n") err := verifyUpdatedBinary(path, expected) require.Error(t, err) assert.Contains(t, err.Error(), "identity output exceeded") }) t.Run("bounded writer retains only its configured limit", func(t *testing.T) { var output boundedIdentityOutput payload := bytes.Repeat([]byte("x"), maxIdentityOutputBytes+1024) written, err := output.Write(payload) require.NoError(t, err) assert.Equal(t, len(payload), written) assert.Len(t, output.bytes(), maxIdentityOutputBytes) assert.True(t, output.exceeded) }) for _, mismatch := range []struct { name string field string mutate func(*BinaryIdentity) }{ {name: "wrong product", field: "product", mutate: func(identity *BinaryIdentity) { identity.Product = "other-product" }}, {name: "wrong component", field: "component", mutate: func(identity *BinaryIdentity) { identity.Component = "desktop" }}, {name: "wrong version", field: "version", mutate: func(identity *BinaryIdentity) { identity.Version = "9.9.8" }}, {name: "wrong OS", field: "GOOS", mutate: func(identity *BinaryIdentity) { identity.GOOS = "other-os" }}, {name: "wrong architecture", field: "GOARCH", mutate: func(identity *BinaryIdentity) { identity.GOARCH = "other-arch" }}, } { t.Run(mismatch.name, func(t *testing.T) { actual := expected mismatch.mutate(&actual) path := writeScript(t, string(scriptBinaryWithIdentity(0, actual))) err := verifyUpdatedBinary(path, expected) require.Error(t, err) assert.Contains(t, err.Error(), mismatch.field+" mismatch") }) } t.Run("rejects the update when exec itself is impossible", func(t *testing.T) { // A nonexistent path fails at start. Accepting it would leave an // installation whose replacement was never proven runnable. err := verifyUpdatedBinary(filepath.Join(t.TempDir(), "does-not-exist"), expected) require.Error(t, err) assert.Contains(t, err.Error(), "could not start") }) }