package releasemanifest import ( "crypto/ed25519" "crypto/rand" "encoding/base64" "encoding/json" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func validPolicyManifest() Manifest { manifest := Manifest{ SchemaVersion: SchemaVersion, Channel: StableChannel, PublishedAt: time.Now().UTC().Format(time.RFC3339), MinimumUpdaterVersion: "1.2.0", Version: "1.3.0", Components: map[string]Component{}, } digest := "blake3:" + strings.Repeat("a", 64) assets := make([]Asset, 0, 6) for _, target := range [][2]string{ {"linux", "amd64"}, {"linux", "arm64"}, {"windows", "amd64"}, {"windows", "arm64"}, {"darwin", "amd64"}, {"darwin", "arm64"}, } { baseline, _ := managerCPUBaseline(target[0], target[1]) asset := Asset{ GOOS: target[0], GOARCH: target[1], CPUBaseline: baseline, Format: ManagerAssetFormat, URL: "https://example.test/" + target[0] + "-" + target[1], BLAKE3: digest, Size: 10, InstallScope: ManagerInstallScope, UpdateStrategy: ManagerUpdateStrategy, ExpectedProduct: "mpv-manager", ExpectedComponent: ManagerPortableComponentID, } assets = append(assets, asset) switch target[0] + "/" + target[1] { case "linux/amd64": manifest.Manager.LinuxAMD64.URL, manifest.Manager.LinuxAMD64.BLAKE3 = asset.URL, asset.BLAKE3 case "linux/arm64": manifest.Manager.LinuxARM64.URL, manifest.Manager.LinuxARM64.BLAKE3 = asset.URL, asset.BLAKE3 case "windows/amd64": manifest.Manager.WinX86_64.URL, manifest.Manager.WinX86_64.BLAKE3 = asset.URL, asset.BLAKE3 case "windows/arm64": manifest.Manager.WinARM64.URL, manifest.Manager.WinARM64.BLAKE3 = asset.URL, asset.BLAKE3 case "darwin/amd64": manifest.Manager.MacosIntel.URL, manifest.Manager.MacosIntel.BLAKE3 = asset.URL, asset.BLAKE3 case "darwin/arm64": manifest.Manager.MacosARM.URL, manifest.Manager.MacosARM.BLAKE3 = asset.URL, asset.BLAKE3 } } manifest.Components[ManagerPortableComponentID] = Component{Version: manifest.Version, Assets: assets} return manifest } func mutateFirstManagerAsset(manifest *Manifest, mutate func(*Asset)) { component := manifest.Components[ManagerPortableComponentID] mutate(&component.Assets[0]) manifest.Components[ManagerPortableComponentID] = component } func signedTestManifest(t *testing.T) (Manifest, ed25519.PublicKey) { t.Helper() publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) manifest := Manifest{ SchemaVersion: SchemaVersion, Channel: StableChannel, MinimumUpdaterVersion: "1.2.0", Version: "1.3.0", Components: map[string]Component{ ManagerPortableComponentID: { Version: "1.3.0", Assets: []Asset{{GOOS: "linux", GOARCH: "amd64", URL: "https://example.test/mpv-manager", BLAKE3: "blake3:test", Size: 10}}, }, }, } require.NoError(t, manifest.Sign("test-key", privateKey)) return manifest, publicKey } func TestManifestSignVerifyAndTamper(t *testing.T) { manifest, publicKey := signedTestManifest(t) require.NoError(t, manifest.Verify(map[string]ed25519.PublicKey{"test-key": publicKey})) manifest.Components[ManagerPortableComponentID] = Component{Version: "9.9.9"} err := manifest.Verify(map[string]ed25519.PublicKey{"test-key": publicKey}) require.Error(t, err) assert.Contains(t, err.Error(), "signature is invalid") } func TestManifestVerifyRejectsTrustAndSchemaFailures(t *testing.T) { manifest, _ := signedTestManifest(t) err := manifest.Verify(nil) require.Error(t, err) assert.Contains(t, err.Error(), "untrusted") manifest.SchemaVersion++ err = manifest.Verify(nil) require.Error(t, err) assert.Contains(t, err.Error(), "unsupported") } func TestManifestValidateEnforcesManagerAssetPolicyAndLegacyEquivalence(t *testing.T) { require.NoError(t, validPolicyManifest().Validate()) for _, test := range []struct { name string mutate func(*Manifest) want string }{ {"format", func(manifest *Manifest) { mutateFirstManagerAsset(manifest, func(asset *Asset) { asset.Format = "zip" }) }, "format"}, {"scope", func(manifest *Manifest) { mutateFirstManagerAsset(manifest, func(asset *Asset) { asset.InstallScope = "system" }) }, "install scope"}, {"strategy", func(manifest *Manifest) { mutateFirstManagerAsset(manifest, func(asset *Asset) { asset.UpdateStrategy = "in-process" }) }, "update strategy"}, {"CPU baseline", func(manifest *Manifest) { mutateFirstManagerAsset(manifest, func(asset *Asset) { asset.CPUBaseline = "x86-64" }) }, "CPU baseline"}, {"native signing", func(manifest *Manifest) { mutateFirstManagerAsset(manifest, func(asset *Asset) { asset.NativeSigning = "authenticode" }) }, "native-signing"}, {"legacy URL", func(manifest *Manifest) { manifest.Manager.LinuxAMD64.URL = "https://example.test/different" }, "legacy manager URL/hash"}, {"legacy hash", func(manifest *Manifest) { manifest.Manager.LinuxAMD64.BLAKE3 = "blake3:" + strings.Repeat("b", 64) }, "legacy manager URL/hash"}, } { t.Run(test.name, func(t *testing.T) { manifest := validPolicyManifest() test.mutate(&manifest) err := manifest.Validate() require.Error(t, err) assert.Contains(t, err.Error(), test.want) }) } } func TestManifestLegacyMPVVersionAlias(t *testing.T) { var manifest Manifest require.NoError(t, json.Unmarshal([]byte(`{"version":"1.2.0","MpvVersion":"0.41.0"}`), &manifest)) assert.Equal(t, "0.41.0", manifest.MpvVersion) err := json.Unmarshal([]byte(`{"mpv-version":"0.41.0","MpvVersion":"0.40.0"}`), &manifest) require.Error(t, err) assert.Contains(t, err.Error(), "conflicting") } func TestManifestMarshalRetainsV12BootstrapFields(t *testing.T) { manifest := Manifest{Version: "1.3.0", MpvVersion: "0.41.0"} manifest.Manager.WinX86_64.URL = "https://example.test/mpv-manager.exe" manifest.Manager.WinX86_64.BLAKE3 = "blake3:" + strings.Repeat("a", 64) data, err := json.Marshal(manifest) require.NoError(t, err) var legacy struct { Version string `json:"version"` MpvVersion string `json:"MpvVersion"` Manager struct { WinX8664 struct { URL string `json:"URL"` BLAKE3 string `json:"BLAKE3"` } `json:"win-x86_64"` } `json:"manager"` } require.NoError(t, json.Unmarshal(data, &legacy)) assert.Equal(t, "1.3.0", legacy.Version) assert.Equal(t, "0.41.0", legacy.MpvVersion) assert.Equal(t, manifest.Manager.WinX86_64.URL, legacy.Manager.WinX8664.URL) assert.Equal(t, manifest.Manager.WinX86_64.BLAKE3, legacy.Manager.WinX8664.BLAKE3) assert.Contains(t, string(data), `"mpv-version":"0.41.0"`) } func TestParseTrustedKeysAndPrivateKey(t *testing.T) { publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) keys, err := ParseTrustedKeys("release-1=" + base64.StdEncoding.EncodeToString(publicKey)) require.NoError(t, err) assert.Equal(t, publicKey, keys["release-1"]) parsed, err := ParsePrivateKey(base64.StdEncoding.EncodeToString(privateKey.Seed())) require.NoError(t, err) assert.Equal(t, privateKey, parsed) } func TestManifestRejectsUnsafeKeyIDs(t *testing.T) { _, privateKey, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) manifest := Manifest{} require.Error(t, manifest.Sign("release,key", privateKey)) _, err = ParseTrustedKeys("release/key=" + base64.StdEncoding.EncodeToString(privateKey.Public().(ed25519.PublicKey))) require.Error(t, err) } func TestSelectAssetRequiresUniquePlatformMatch(t *testing.T) { manifest := Manifest{Components: map[string]Component{ ManagerPortableComponentID: { Version: "1.3.0", Assets: []Asset{ {GOOS: "linux", GOARCH: "amd64", URL: "linux"}, {GOOS: "windows", GOARCH: "amd64", URL: "windows"}, }, }, }} component, asset, err := manifest.SelectAsset(ManagerPortableComponentID, "linux", "amd64") require.NoError(t, err) assert.Equal(t, "1.3.0", component.Version) assert.Equal(t, "linux", asset.URL) _, _, err = manifest.SelectAsset(ManagerPortableComponentID, "darwin", "arm64") require.Error(t, err) } func TestStableManifestRejectsPrerelease(t *testing.T) { manifest := validPolicyManifest() manifest.Version = "1.3.0-rc.1" component := manifest.Components[ManagerPortableComponentID] component.Version = manifest.Version manifest.Components[ManagerPortableComponentID] = component require.ErrorContains(t, manifest.Validate(), "stable channel cannot contain a prerelease") manifest.Channel = ReleaseCandidateChannel require.NoError(t, manifest.Validate()) require.Equal(t, StableChannel, ChannelForVersion("1.3.0+build-with-hyphen")) require.Equal(t, ReleaseCandidateChannel, ChannelForVersion("1.3.0-rc.1+build")) }