package config import ( "encoding/json" "errors" "os" "path/filepath" "testing" "gitgud.io/mike/mpv-manager/pkg/constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupInstalledAppIdentityTest(t *testing.T) string { t.Helper() resetGlobalConfig() homeDir := t.TempDir() configHome := filepath.Join(homeDir, ".config") isolateConfigHome(t, homeDir) t.Setenv("XDG_CONFIG_HOME", configHome) t.Cleanup(resetGlobalConfig) return filepath.Join(testConfigDir(homeDir), constants.ConfigFileName) } func TestInstalledAppIdentityMigrationPreservesDistinctMethods(t *testing.T) { configPath := setupInstalledAppIdentityTest(t) require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0755)) raw := `{ "installed_apps": [ {"app_name":"MPV","app_type":"flatpak","install_method":"mpv-flatpak","install_path":"/var/lib/flatpak/app","managed":true}, {"app_name":"MPV","app_type":"package-manager","install_method":"mpv-package","install_path":"apt","managed":true}, {"app_name":"MPV","app_type":"exe-binary","install_method":"mpv-binary","install_path":"/opt/mpv","managed":true}, {"app_name":"Celluloid","app_type":"flatpak","install_method":"celluloid-flatpak","install_path":"/var/lib/flatpak/app","managed":false} ] }` require.NoError(t, os.WriteFile(configPath, []byte(raw), 0644)) loaded, err := Load() require.NoError(t, err) require.Len(t, loaded.InstalledApps, 4) ids := make(map[string]struct{}, len(loaded.InstalledApps)) for _, app := range loaded.InstalledApps { require.NotEmpty(t, app.ID) ids[app.ID] = struct{}{} } assert.Len(t, ids, 4) persistedData, err := os.ReadFile(configPath) require.NoError(t, err) var persisted Config require.NoError(t, json.Unmarshal(persistedData, &persisted)) for _, app := range persisted.InstalledApps { assert.NotEmpty(t, app.ID, "migration must persist every generated ID") } } func TestAddInstalledAppCoexistsAndReplacesOnlySameIdentity(t *testing.T) { setupInstalledAppIdentityTest(t) apps := []InstalledApp{ {AppName: "MPV", InstallMethod: "mpv-flatpak", InstallPath: "/var/lib/flatpak/app", AppVersion: "1"}, {AppName: "MPV", InstallMethod: "mpv-package", InstallPath: "apt", AppVersion: "1"}, {AppName: "MPV", InstallMethod: "mpv-binary", InstallPath: "/opt/mpv-a", AppVersion: "1"}, {AppName: "MPV", InstallMethod: "mpv-binary", InstallPath: "/opt/mpv-b", AppVersion: "1"}, {AppName: "Celluloid", InstallMethod: "celluloid-package", InstallPath: "apt", AppVersion: "1"}, } for _, app := range apps { require.NoError(t, AddInstalledApp(app)) } require.Len(t, GetInstalledApps(), 5) // Lexically equivalent paths are the same identity and update in place. require.NoError(t, AddInstalledApp(InstalledApp{ AppName: "MPV", InstallMethod: "mpv-binary", InstallPath: "/opt/unused/../mpv-a", AppVersion: "2", })) stored := GetInstalledApps() require.Len(t, stored, 5) resolved, err := ResolveInstalledApp("", "MPV", "mpv-binary", "/opt/mpv-a") require.NoError(t, err) assert.Equal(t, "2", resolved.AppVersion) assert.Equal(t, "/opt/unused/../mpv-a", resolved.InstallPath) } func TestStableIDTargetsRemovalAndUpdates(t *testing.T) { setupInstalledAppIdentityTest(t) require.NoError(t, AddInstalledApp(InstalledApp{ AppName: "MPV", InstallMethod: "mpv-flatpak", InstallPath: "/flatpak", Managed: false, })) require.NoError(t, AddInstalledApp(InstalledApp{ AppName: "MPV", InstallMethod: "mpv-package", InstallPath: "apt", Managed: true, })) apps := GetInstalledApps() require.Len(t, apps, 2) flatpak, err := ResolveInstalledApp("", "MPV", "mpv-flatpak", "/flatpak") require.NoError(t, err) packageApp, err := ResolveInstalledApp("", "MPV", "mpv-package", "apt") require.NoError(t, err) require.NoError(t, UpdateInstalledAppUITypeByID(flatpak.ID, "uosc")) updated, err := ResolveInstalledApp(flatpak.ID, "", "", "") require.NoError(t, err) assert.False(t, updated.Managed) assert.Equal(t, "uosc", updated.UIType) require.NoError(t, UpdateInstalledAppManagedByID(flatpak.ID, true, "modernz")) updated, err = ResolveInstalledApp(flatpak.ID, "", "", "") require.NoError(t, err) assert.True(t, updated.Managed) assert.Equal(t, "modernz", updated.UIType) require.ErrorIs(t, RemoveInstalledApp("MPV"), ErrInstalledAppAmbiguous) assert.Len(t, GetInstalledApps(), 2, "ambiguous legacy removal must not mutate config") require.NoError(t, RemoveInstalledAppByID(packageApp.ID)) remaining := GetInstalledApps() require.Len(t, remaining, 1) assert.Equal(t, flatpak.ID, remaining[0].ID) assert.ErrorIs(t, RemoveInstalledAppByID(packageApp.ID), ErrInstalledAppNotFound) } func TestUpdateInstalledAppVersionByIDTargetsOneSameMethodInstall(t *testing.T) { setupInstalledAppIdentityTest(t) require.NoError(t, AddInstalledApp(InstalledApp{ AppName: "MPV", InstallMethod: "mpv-binary", InstallPath: "/opt/mpv-a", AppVersion: "1", })) require.NoError(t, AddInstalledApp(InstalledApp{ AppName: "MPV", InstallMethod: "mpv-binary", InstallPath: "/opt/mpv-b", AppVersion: "1", })) first, err := ResolveInstalledApp("", "MPV", "mpv-binary", "/opt/mpv-a") require.NoError(t, err) second, err := ResolveInstalledApp("", "MPV", "mpv-binary", "/opt/mpv-b") require.NoError(t, err) updated, err := UpdateInstalledAppVersionByID(second.ID, "2") require.NoError(t, err) assert.Equal(t, second.ID, updated.ID) assert.Equal(t, "2", updated.AppVersion) firstAfter, err := ResolveInstalledApp(first.ID, "", "", "") require.NoError(t, err) secondAfter, err := ResolveInstalledApp(second.ID, "", "", "") require.NoError(t, err) assert.Equal(t, "1", firstAfter.AppVersion) assert.Equal(t, "2", secondAfter.AppVersion) } func TestLegacyManagedMigrationUsesRecordNotMethod(t *testing.T) { configPath := setupInstalledAppIdentityTest(t) require.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0755)) raw := `{ "installed_apps": [ {"app_name":"MPV","install_method":"mpv-binary","install_path":"/opt/a","managed":false}, {"app_name":"MPV","install_method":"mpv-binary","install_path":"/opt/b"} ] }` require.NoError(t, os.WriteFile(configPath, []byte(raw), 0644)) loaded, err := Load() require.NoError(t, err) require.Len(t, loaded.InstalledApps, 2) assert.False(t, loaded.InstalledApps[0].Managed, "an explicit false value must be preserved") assert.True(t, loaded.InstalledApps[1].Managed, "a missing legacy field must migrate to true") } func TestResolveInstalledAppRejectsAmbiguousLegacyFields(t *testing.T) { setupInstalledAppIdentityTest(t) require.NoError(t, AddInstalledApp(InstalledApp{AppName: "MPV", InstallMethod: "mpv-binary", InstallPath: "/opt/a"})) require.NoError(t, AddInstalledApp(InstalledApp{AppName: "MPV", InstallMethod: "mpv-binary", InstallPath: "/opt/b"})) _, err := ResolveInstalledApp("", "MPV", "mpv-binary", "") assert.True(t, errors.Is(err, ErrInstalledAppAmbiguous)) } func TestInstalledAppIDNormalizesWindowsPaths(t *testing.T) { first := installedAppID(InstalledApp{InstallMethod: "mpv-binary", InstallPath: `C:\Users\Test\mpv\..\MPV`}) second := installedAppID(InstalledApp{InstallMethod: "mpv-binary", InstallPath: `c:/users/test/mpv`}) assert.Equal(t, first, second) } func TestAbsenceObservationCannotRemoveChangedOrDifferentApp(t *testing.T) { setupInstalledAppIdentityTest(t) _, err := Load() require.NoError(t, err) require.NoError(t, AddInstalledApp(InstalledApp{AppName: "MPV", InstallMethod: constants.MethodMPVBinary, InstallPath: filepath.Join(t.TempDir(), "one")})) require.NoError(t, AddInstalledApp(InstalledApp{AppName: "MPV", InstallMethod: constants.MethodMPVBinary, InstallPath: filepath.Join(t.TempDir(), "two")})) observed := GetInstalledApps()[0] require.NoError(t, UpdateInstalledAppManagedByID(observed.ID, true, "modernz")) removed, err := RemoveInstalledAppIfUnchanged(observed) require.NoError(t, err) assert.False(t, removed, "adoption after the probe must win over stale absence") require.Len(t, GetInstalledApps(), 2) fresh := GetInstalledApps()[0] removed, err = RemoveInstalledAppIfUnchanged(fresh) require.NoError(t, err) assert.True(t, removed) remaining := GetInstalledApps() require.Len(t, remaining, 1) assert.NotEqual(t, observed.ID, remaining[0].ID) assert.Equal(t, observed.InstallMethod, remaining[0].InstallMethod) }