package version import ( "context" "os" "path/filepath" "testing" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/installer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestIsManagedApp(t *testing.T) { tests := []struct { methodID string want bool }{ {constants.MethodMPVBinary, true}, {constants.MethodMPVBinaryV3, true}, {constants.MethodMPVApp, true}, {constants.MethodMPCQT, true}, {constants.MethodIINA, true}, {constants.MethodMPVFlatpak, false}, {constants.MethodMPVPackage, false}, {constants.MethodMPVBrew, false}, {constants.MethodCelluloidFlatpak, false}, {constants.MethodUOSC, false}, {"", false}, {"something-else", false}, } for _, tt := range tests { t.Run(tt.methodID, func(t *testing.T) { assert.Equal(t, tt.want, isManagedApp(tt.methodID)) }) } } func TestCheckAppForUpdate(t *testing.T) { releaseInfo := &installer.ReleaseInfo{MpvVersion: "0.41.0"} releaseInfo.MPCQT.AppVersion = "23.0" releaseInfo.IINA.AppVersion = "1.4.0" tests := []struct { name string app config.InstalledApp wantNil bool wantUpdateType string wantUpdateVer string wantPkgManager string wantAvailVersion string }{ { name: "git build does not downgrade to its base release", app: config.InstalledApp{AppName: "MPV", InstallMethod: constants.MethodMPVBinary, AppVersion: "v0.41.0-1035-g876ba7b28"}, wantUpdateType: "current-version", wantAvailVersion: "0.41.0", }, { name: "git build still receives a newer release", app: config.InstalledApp{AppName: "MPV", InstallMethod: constants.MethodMPVBinary, AppVersion: "v0.40.0-1035-g876ba7b28"}, wantUpdateType: "update", wantAvailVersion: "0.41.0", }, { name: "mpv binary with update available", app: config.InstalledApp{ ID: "app-1", AppName: "MPV", InstallMethod: constants.MethodMPVBinary, InstallPath: "/opt/mpv", AppVersion: "0.40.0", }, wantUpdateType: "update", wantAvailVersion: "0.41.0", }, { name: "mpv binary v3 already current", app: config.InstalledApp{ ID: "app-2", AppName: "MPV", InstallMethod: constants.MethodMPVBinaryV3, AppVersion: "0.41.0", }, wantUpdateType: "current-version", wantAvailVersion: "0.41.0", }, { name: "mpc-qt with update available", app: config.InstalledApp{ ID: "app-3", AppName: "MPC-QT", InstallMethod: constants.MethodMPCQT, AppVersion: "22.0", }, wantUpdateType: "update", wantAvailVersion: "23.0", }, { name: "iina already current", app: config.InstalledApp{ ID: "app-4", AppName: "IINA", InstallMethod: constants.MethodIINA, AppVersion: "1.4.0", }, wantUpdateType: "current-version", wantAvailVersion: "1.4.0", }, { name: "flatpak app with stored version", app: config.InstalledApp{ ID: "app-5", AppName: "MPV", InstallMethod: constants.MethodMPVFlatpak, AppVersion: "0.40.0", }, wantUpdateType: "package-info", wantPkgManager: "Flatpak", wantAvailVersion: "Check via Flatpak", }, { name: "brew app with stored version", app: config.InstalledApp{ ID: "app-6", AppName: "MPV", InstallMethod: constants.MethodMPVBrew, AppVersion: "0.40.0", }, wantUpdateType: "package-info", wantPkgManager: "Homebrew", wantAvailVersion: "Check via Homebrew", }, { name: "cancelled package probe retains unknown version", app: config.InstalledApp{ ID: "app-7", AppName: "MPV", InstallMethod: constants.MethodMPVPackage, }, wantUpdateType: "package-info", wantPkgManager: "Package Manager", // Cancelled probes keep the app and report unknown availability. wantAvailVersion: "Unknown", }, { name: "unmanaged method returns nil", app: config.InstalledApp{ ID: "app-8", AppName: "MPV", InstallMethod: "mpv-source", }, wantNil: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() item := checkAppForUpdate(ctx, tt.app, releaseInfo) if tt.wantNil { assert.Nil(t, item) return } require.NotNil(t, item) assert.Equal(t, tt.app.ID, item.AppID) assert.Equal(t, tt.app.AppName, item.AppName) assert.Equal(t, tt.app.InstallPath, item.InstallPath) assert.Equal(t, tt.app.InstallMethod, item.MethodID) assert.Equal(t, tt.wantUpdateType, item.UpdateType) assert.Equal(t, tt.wantPkgManager, item.PackageManager) assert.Equal(t, tt.wantAvailVersion, item.AvailableVersion) }) } } func TestGetPackageManagerName(t *testing.T) { tests := []struct { methodID string want string }{ {constants.MethodMPVFlatpak, "Flatpak"}, {constants.MethodCelluloidFlatpak, "Flatpak"}, {constants.MethodMPVPackage, "Package Manager"}, {constants.MethodCelluloidPackage, "Package Manager"}, {constants.MethodMPVBrew, "Homebrew"}, {"unknown-method", "Unknown"}, } for _, tt := range tests { t.Run(tt.methodID, func(t *testing.T) { assert.Equal(t, tt.want, getPackageManagerName(tt.methodID)) }) } } func TestCheckAndCollectBackups(t *testing.T) { t.Run("collects .conf backups with dates", func(t *testing.T) { dir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(dir, "mpv-2026-01-01.conf"), []byte("a"), 0644)) require.NoError(t, os.WriteFile(filepath.Join(dir, "input-backup.conf"), []byte("b"), 0644)) // Non-.conf files and unreadable stats are skipped. require.NoError(t, os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("c"), 0644)) require.NoError(t, os.Mkdir(filepath.Join(dir, "subdir"), 0755)) items := []ConfigBackupItem{} require.NoError(t, checkAndCollectBackups(dir, &items)) require.Len(t, items, 2) names := []string{items[0].BackupName, items[1].BackupName} assert.Contains(t, names, "mpv-2026-01-01") assert.Contains(t, names, "input-backup") for _, item := range items { assert.Equal(t, filepath.Join(dir, item.BackupName+".conf"), item.BackupPath) assert.Regexp(t, `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`, item.BackupDate) } }) t.Run("missing directory returns an error", func(t *testing.T) { items := []ConfigBackupItem{} err := checkAndCollectBackups(filepath.Join(t.TempDir(), "missing"), &items) assert.Error(t, err) assert.Empty(t, items) }) } // TestCheckForAppUpdates exercises the full app-update aggregation including // the config-backed UI version checks and config-backup collection. It runs // against a scratch HOME so the global config singleton never touches real // user state. func TestCheckForAppUpdates(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) t.Setenv("XDG_CONFIG_HOME", home) releaseInfo := &installer.ReleaseInfo{MpvVersion: "0.41.0"} releaseInfo.UOSC.AppVersion = "5.10.0" releaseInfo.ModernZ.AppVersion = "0.4.0" releaseInfo.FFmpeg.AppVersion = "20260801" mpvBinaryApp := config.InstalledApp{ ID: "mpv-app-1", AppName: "MPV", InstallMethod: constants.MethodMPVBinary, InstallPath: "/opt/mpv", AppVersion: "0.40.0", } t.Run("nil release info is treated as an empty manifest", func(t *testing.T) { result := CheckForAppUpdates(nil, nil) require.NotNil(t, result) assert.Empty(t, result.AppsToUpdate) }) t.Run("managed app with update and no UI versions configured", func(t *testing.T) { result := CheckForAppUpdates(releaseInfo, []config.InstalledApp{mpvBinaryApp}) require.Len(t, result.AppsToUpdate, 1) assert.Equal(t, "mpv-app-1", result.AppsToUpdate[0].AppID) assert.Equal(t, "update", result.AppsToUpdate[0].UpdateType) assert.False(t, result.NoEligibleApps) }) t.Run("package-info items alone do not flag NoEligibleApps", func(t *testing.T) { result := CheckForAppUpdates(releaseInfo, []config.InstalledApp{ {ID: "flat-1", AppName: "MPV", InstallMethod: constants.MethodMPVFlatpak, AppVersion: "0.40.0"}, }) // The flatpak entry produces a package-info display item, and // NoEligibleApps only reports true when there is nothing to show at all. assert.Len(t, result.AppsToUpdate, 1) assert.False(t, result.NoEligibleApps) }) t.Run("only unmanaged apps flags NoEligibleApps", func(t *testing.T) { result := CheckForAppUpdates(releaseInfo, []config.InstalledApp{ {ID: "ext-1", AppName: "MPV", InstallMethod: "mpv-source", AppVersion: "0.40.0"}, }) assert.Empty(t, result.AppsToUpdate) assert.True(t, result.NoEligibleApps) }) t.Run("installed UI and ffmpeg versions produce update entries", func(t *testing.T) { require.NoError(t, config.SetUOSCVersion("5.9.0")) require.NoError(t, config.SetModernZVersion("0.3.0")) require.NoError(t, config.SetFFmpegVersion("20250101")) t.Cleanup(func() { config.SetUOSCVersion("") config.SetModernZVersion("") config.SetFFmpegVersion("") }) result := CheckForAppUpdates(releaseInfo, []config.InstalledApp{mpvBinaryApp}) byMethod := map[string]UpdateItem{} for _, item := range result.AppsToUpdate { byMethod[item.MethodID] = item } require.Contains(t, byMethod, constants.MethodUOSC) assert.Equal(t, "5.9.0", byMethod[constants.MethodUOSC].CurrentVersion) assert.Equal(t, "5.10.0", byMethod[constants.MethodUOSC].AvailableVersion) require.Contains(t, byMethod, constants.MethodModernZ) assert.Equal(t, "0.4.0", byMethod[constants.MethodModernZ].AvailableVersion) require.Contains(t, byMethod, constants.MethodFFmpeg) assert.Equal(t, "20260801", byMethod[constants.MethodFFmpeg].AvailableVersion) }) t.Run("current UI and ffmpeg versions produce no update entries", func(t *testing.T) { require.NoError(t, config.SetUOSCVersion("5.10.0")) require.NoError(t, config.SetModernZVersion("0.4.0")) require.NoError(t, config.SetFFmpegVersion("20260801")) t.Cleanup(func() { config.SetUOSCVersion("") config.SetModernZVersion("") config.SetFFmpegVersion("") }) result := CheckForAppUpdates(releaseInfo, []config.InstalledApp{mpvBinaryApp}) for _, item := range result.AppsToUpdate { assert.NotContains(t, []string{constants.MethodUOSC, constants.MethodModernZ, constants.MethodFFmpeg}, item.MethodID) } }) t.Run("config backups are collected from the mpv config dir", func(t *testing.T) { // GetMPVConfigPath resolves under the scratch HOME set above. backupDir := filepath.Join(config.GetMPVConfigPath(), "conf_backups") require.NoError(t, os.MkdirAll(backupDir, 0755)) require.NoError(t, os.WriteFile(filepath.Join(backupDir, "mpv-pre-update.conf"), []byte("x"), 0644)) result := CheckForAppUpdates(releaseInfo, []config.InstalledApp{mpvBinaryApp}) require.Len(t, result.ConfigBackupItems, 1) assert.Equal(t, "mpv-pre-update", result.ConfigBackupItems[0].BackupName) assert.Equal(t, filepath.Join(backupDir, "mpv-pre-update.conf"), result.ConfigBackupItems[0].BackupPath) withoutBackups := CheckForAppUpdatesContext(context.Background(), releaseInfo, []config.InstalledApp{mpvBinaryApp}) require.Empty(t, withoutBackups.ConfigBackupItems, "app-only checks must not enumerate backups") }) }