package installer import ( "errors" "net/http" "os" "path/filepath" "runtime" "strings" "testing" "gitgud.io/mike/mpv-manager/pkg/constants" "github.com/stretchr/testify/assert" ) func TestInstallUISafelyRestoresPreviousUIOnFailure(t *testing.T) { configDir := t.TempDir() oldFiles := map[string]string{ filepath.Join(constants.ScriptsDir, "modernz.lua"): "old-modernz", filepath.Join(constants.ScriptsDir, "thumbfast.lua"): "old-thumbfast", filepath.Join(constants.ScriptOptsDir, "modernz.conf"): "old-config", filepath.Join(constants.FontsDir, constants.ModernZFontFile): "old-font", constants.MPVConfigFileName: "osc=no\n", } for relativePath, content := range oldFiles { path := filepath.Join(configDir, relativePath) if err := os.MkdirAll(filepath.Dir(path), constants.DirPermission); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(content), constants.FilePermission); err != nil { t.Fatal(err) } } httpClient := NewMockHTTPClient() httpClient.DoFunc = func(*http.Request) (*http.Response, error) { return nil, errors.New("simulated network failure") } release := ReleaseInfo{} release.UOSC.URL = "https://example.invalid/uosc.zip" inst := NewInstaller(release, configDir, WithHTTPClient(httpClient)) output := make(chan string, 128) runner := NewCommandRunner(output, nil) err := InstallUISafely(inst, runner, configDir, constants.UITypeUOSC) if err == nil { t.Fatal("expected UI installation failure") } for relativePath, want := range oldFiles { content, readErr := os.ReadFile(filepath.Join(configDir, relativePath)) if readErr != nil { t.Fatalf("restored %s: %v", relativePath, readErr) } if string(content) != want { t.Errorf("restored %s = %q, want %q", relativePath, content, want) } } if _, statErr := os.Stat(filepath.Join(configDir, constants.ScriptsDir, "uosc.lua")); !os.IsNotExist(statErr) { t.Fatalf("partial uOSC script survived rollback: %v", statErr) } } // TestGetArchitectureInfo tests architecture info retrieval func TestGetArchitectureInfo(t *testing.T) { mockReleaseInfo := ReleaseInfo{ Windows: struct { X8664 struct{ URL, BLAKE3 string } `json:"x86-64"` X8664v3 struct{ URL, BLAKE3 string } `json:"x86-64-v3"` Aarch64 struct{ URL, BLAKE3 string } `json:"aarch64"` }{ X8664: struct{ URL, BLAKE3 string }{ URL: "https://example.com/mpv-x86_64.7z", BLAKE3: "def456", }, X8664v3: struct{ URL, BLAKE3 string }{ URL: "https://example.com/mpv-x86-64-v3.7z", BLAKE3: "ghi789", }, Aarch64: struct{ URL, BLAKE3 string }{ URL: "https://example.com/mpv-aarch64.7z", BLAKE3: "jkl012", }, }, FFmpeg: struct { X8664 struct{ URL, BLAKE3 string } `json:"x86-64"` X8664v3 struct{ URL, BLAKE3 string } `json:"x86-64-v3"` Aarch64 struct{ URL, BLAKE3 string } `json:"aarch64"` AppVersion string `json:"app_version"` }{ X8664: struct{ URL, BLAKE3 string }{ URL: "https://example.com/ffmpeg-x86_64.7z", BLAKE3: "pqr678", }, X8664v3: struct{ URL, BLAKE3 string }{ URL: "https://example.com/ffmpeg-x86-64-v3.7z", BLAKE3: "stu901", }, Aarch64: struct{ URL, BLAKE3 string }{ URL: "https://example.com/ffmpeg-aarch64.7z", BLAKE3: "vwx234", }, AppVersion: "20260121-402676f13", }, } tests := []struct { name string platformArch string isV3 bool wantArch string wantURL string wantFFmpeg string wantErr bool errMsg string }{ { name: "amd64 architecture", platformArch: constants.ArchAMD64, isV3: false, wantArch: constants.ArchX8664, wantURL: "https://example.com/mpv-x86_64.7z", wantFFmpeg: "https://example.com/ffmpeg-x86_64.7z", wantErr: false, }, { name: "x86_64 architecture", platformArch: constants.ArchX8664, isV3: false, wantArch: constants.ArchX8664, wantURL: "https://example.com/mpv-x86_64.7z", wantFFmpeg: "https://example.com/ffmpeg-x86_64.7z", wantErr: false, }, { name: "amd64 with V3 optimization", platformArch: constants.ArchAMD64, isV3: true, wantArch: constants.ArchX8664V3, wantURL: "https://example.com/mpv-x86-64-v3.7z", wantFFmpeg: "https://example.com/ffmpeg-x86-64-v3.7z", wantErr: false, }, { name: "x86_64 with V3 optimization", platformArch: constants.ArchX8664, isV3: true, wantArch: constants.ArchX8664V3, wantURL: "https://example.com/mpv-x86-64-v3.7z", wantFFmpeg: "https://example.com/ffmpeg-x86-64-v3.7z", wantErr: false, }, { name: "arm64 architecture", platformArch: constants.ArchARM64, isV3: false, wantArch: constants.ArchAarch64, wantURL: "https://example.com/mpv-aarch64.7z", wantFFmpeg: "https://example.com/ffmpeg-aarch64.7z", wantErr: false, }, { name: "unsupported architecture (386/i686 no longer available)", platformArch: constants.Arch386, isV3: false, wantErr: true, errMsg: "unsupported architecture: 386 (i686/32-bit builds are no longer available)", }, { name: "unsupported architecture", platformArch: "unsupported", isV3: false, wantErr: true, errMsg: "unsupported architecture: unsupported (i686/32-bit builds are no longer available)", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := GetArchitectureInfo(tt.platformArch, tt.isV3, mockReleaseInfo) if tt.wantErr { assert.Error(t, err) assert.Equal(t, tt.errMsg, err.Error()) return } assert.NoError(t, err) assert.Equal(t, tt.wantArch, got.Arch) assert.Equal(t, tt.wantURL, got.URL) assert.Equal(t, tt.wantFFmpeg, got.FFmpegURL) }) } } // TestGetMethodDisplayName tests display name retrieval for method IDs func TestGetMethodDisplayName(t *testing.T) { tests := []struct { name string methodID string wantDisplay string }{ { name: "MPV Binary", methodID: constants.MethodMPVBinary, wantDisplay: "MPV", }, { name: "MPV Binary V3", methodID: constants.MethodMPVBinaryV3, wantDisplay: "MPV (AVX2 optimized)", }, { name: "MPC-QT", methodID: constants.MethodMPCQT, wantDisplay: "MPC-QT", }, { name: "MPV App", methodID: constants.MethodMPVApp, wantDisplay: "MPV", }, { name: "MPV Brew", methodID: constants.MethodMPVBrew, wantDisplay: "MPV (CLI only)", }, { name: "IINA", methodID: constants.MethodIINA, wantDisplay: "IINA", }, { name: "MPV Flatpak", methodID: constants.MethodMPVFlatpak, wantDisplay: "MPV", }, { name: "Celluloid Flatpak", methodID: constants.MethodCelluloidFlatpak, wantDisplay: "Celluloid", }, { name: "MPV Package", methodID: constants.MethodMPVPackage, wantDisplay: "MPV", }, { name: "Celluloid Package", methodID: constants.MethodCelluloidPackage, wantDisplay: "Celluloid", }, { name: "Unknown method ID", methodID: "unknown-method", wantDisplay: "unknown-method", }, { name: "Empty method ID", methodID: "", wantDisplay: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := GetMethodDisplayName(tt.methodID) assert.Equal(t, tt.wantDisplay, got) }) } } // TestCommandExists tests command existence checking func TestCommandExists(t *testing.T) { tests := []struct { name string command string want bool }{ { name: "go command", command: "go", want: true, }, { name: "non-existent command", command: "nonexistent-command-12345", want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := CommandExists(tt.command) assert.Equal(t, tt.want, got) }) } } // TestGetMPVConfigDirFromHome tests MPV config directory path construction func TestGetMPVConfigDirFromHome(t *testing.T) { profile := isolateHome(t) xdgConfig := filepath.Join(profile, "custom-xdg") t.Setenv("XDG_CONFIG_HOME", xdgConfig) for _, home := range []string{filepath.Join(profile, "other-home"), ""} { t.Run(home, func(t *testing.T) { var want string switch runtime.GOOS { case "windows": // Windows uses the native per-user mpv installation rather // than interpreting the caller's Unix home path. want = filepath.Join(profile, constants.AppNameMPV, constants.PortableConfigDir) case "linux": want = filepath.Join(xdgConfig, constants.AppNameMPV) default: base := home if base == "" { var err error base, err = filepath.Abs(".") assert.NoError(t, err) } want = filepath.Join(base, ".config", constants.AppNameMPV) } assert.Equal(t, want, GetMPVConfigDirFromHome(home)) }) } } // TestCommandRunnerHelper is a helper to capture output messages type TestCommandRunner struct { outputChan chan string errorChan chan error } func NewTestCommandRunner() *TestCommandRunner { return &TestCommandRunner{ outputChan: make(chan string, 100), errorChan: make(chan error, 10), } } // TestCreateFullBackup tests full backup creation in conf_backups directory func TestCreateFullBackup(t *testing.T) { tests := []struct { name string setupFile bool expectError bool }{ { name: "create full backup", setupFile: true, expectError: false, }, { name: "error for non-existent file", setupFile: false, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tempDir := t.TempDir() configDir := tempDir + "/.config/mpv" testFile := configDir + "/mpv.conf" if tt.setupFile { err := os.MkdirAll(configDir, 0755) assert.NoError(t, err) err = os.WriteFile(testFile, []byte("test config"), 0644) assert.NoError(t, err) } tcr := NewTestCommandRunner() cr := NewCommandRunner(tcr.outputChan, tcr.errorChan) backupPath, err := CreateFullBackup(OSFileSystem{}, testFile, cr) if tt.expectError { assert.Error(t, err) } else { assert.NoError(t, err) assert.FileExists(t, backupPath) assert.Contains(t, backupPath, constants.ConfigBackupsDir) assert.True(t, strings.HasSuffix(backupPath, constants.BackupDirFilePrefix)) } }) } } // TestRestoreBackup tests backup restoration functionality func TestRestoreBackup(t *testing.T) { tests := []struct { name string setupBackup bool setupExisting bool expectError bool }{ { name: "restore from backup", setupBackup: true, setupExisting: false, expectError: false, }, { name: "restore with existing config", setupBackup: true, setupExisting: true, expectError: false, }, { name: "error for non-existent backup", setupBackup: false, setupExisting: false, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Note: RestoreBackup now has path validation for security // We test that non-existent backups return error if tt.name == "error for non-existent backup" { tempDir := t.TempDir() configPath := tempDir + "/config.txt" backupPath := tempDir + "/backup.txt" tcr := NewTestCommandRunner() cr := NewCommandRunner(tcr.outputChan, tcr.errorChan) err := RestoreBackup(OSFileSystem{}, backupPath, configPath, cr) assert.Error(t, err) return } // For restore tests, we need to create proper backup files that pass validation // Since the validation requires backups to be in the real system backup directory, // and have proper filename format, we just verify the error message is correct tempDir := t.TempDir() configPath := tempDir + "/config.txt" backupPath := tempDir + "/backup.txt" if tt.setupBackup { err := os.WriteFile(backupPath, []byte("backup content"), 0644) assert.NoError(t, err) } if tt.setupExisting { err := os.WriteFile(configPath, []byte("existing content"), 0644) assert.NoError(t, err) } tcr := NewTestCommandRunner() cr := NewCommandRunner(tcr.outputChan, tcr.errorChan) err := RestoreBackup(OSFileSystem{}, backupPath, configPath, cr) // Since backup is not in the proper directory, it should fail validation assert.Error(t, err) assert.Contains(t, err.Error(), "invalid backup") }) } } // TestEnsureDirectory tests directory creation functionality func TestEnsureDirectory(t *testing.T) { tests := []struct { name string setupExist bool expectError bool }{ { name: "create new directory", setupExist: false, expectError: false, }, { name: "directory already exists", setupExist: true, expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tempDir := t.TempDir() testDir := tempDir + "/test/nested/dir" if tt.setupExist { err := os.MkdirAll(testDir, 0755) assert.NoError(t, err) } tcr := NewTestCommandRunner() cr := NewCommandRunner(tcr.outputChan, tcr.errorChan) err := EnsureDirectory(testDir, cr) if tt.expectError { assert.Error(t, err) } else { assert.NoError(t, err) assert.DirExists(t, testDir) } }) } } // TestGetPrivilegePrefix tests privilege prefix determination func TestGetPrivilegePrefix(t *testing.T) { tests := []struct { name string expectEmpty bool expectSudo bool }{ { name: "get privilege prefix", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := GetPrivilegePrefix() if os.Geteuid() == 0 { assert.Empty(t, result) } else if CommandExists(constants.CommandSudo) { assert.Equal(t, []string{constants.CommandSudo}, result) } else { assert.Empty(t, result) } }) } } // TestGetArchitectureInfoEmptyURL tests that a missing download URL (e.g. an // empty release manifest after a failed startup fetch) returns a clear error // for every architecture branch func TestGetArchitectureInfoEmptyURL(t *testing.T) { emptyReleaseInfo := ReleaseInfo{} tests := []struct { name string platformArch string isV3 bool }{ {name: "amd64 v3 architecture", platformArch: constants.ArchAMD64, isV3: true}, {name: "amd64 architecture", platformArch: constants.ArchAMD64, isV3: false}, {name: "x86_64 architecture", platformArch: constants.ArchX8664, isV3: false}, {name: "arm64 architecture", platformArch: constants.ArchARM64, isV3: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := GetArchitectureInfo(tt.platformArch, tt.isV3, emptyReleaseInfo) assert.Error(t, err) assert.Contains(t, err.Error(), "release manifest unavailable or missing a download URL") }) } }