diff --git a/tests/unit/test_update_command.py b/tests/unit/test_update_command.py index ac951120..f8901278 100644 --- a/tests/unit/test_update_command.py +++ b/tests/unit/test_update_command.py @@ -93,5 +93,201 @@ def test_update_uses_shell_installer_on_unix( mock_chmod.assert_called_once() +class TestUpdatePlatformHelpers(unittest.TestCase): + """Tests for platform-detection helper functions.""" + + def test_is_windows_platform_true_on_win32(self): + with patch.object(update_module.sys, "platform", "win32"): + self.assertTrue(update_module._is_windows_platform()) + + def test_is_windows_platform_false_on_linux(self): + with patch.object(update_module.sys, "platform", "linux"): + self.assertFalse(update_module._is_windows_platform()) + + def test_is_windows_platform_false_on_darwin(self): + with patch.object(update_module.sys, "platform", "darwin"): + self.assertFalse(update_module._is_windows_platform()) + + def test_installer_url_windows(self): + with patch.object(update_module.sys, "platform", "win32"): + url = update_module._get_update_installer_url() + self.assertEqual(url, "https://aka.ms/apm-windows") + + def test_installer_url_unix(self): + with patch.object(update_module.sys, "platform", "linux"): + url = update_module._get_update_installer_url() + self.assertEqual(url, "https://aka.ms/apm-unix") + + def test_installer_suffix_windows(self): + with patch.object(update_module.sys, "platform", "win32"): + suffix = update_module._get_update_installer_suffix() + self.assertEqual(suffix, ".ps1") + + def test_installer_suffix_unix(self): + with patch.object(update_module.sys, "platform", "linux"): + suffix = update_module._get_update_installer_suffix() + self.assertEqual(suffix, ".sh") + + def test_manual_update_command_unix(self): + with patch.object(update_module.sys, "platform", "linux"): + command = update_module._get_manual_update_command() + self.assertIn("aka.ms/apm-unix", command) + self.assertIn("curl", command) + + def test_installer_run_command_unix_bin_sh_exists(self): + with patch.object(update_module.sys, "platform", "linux"), \ + patch.object(update_module.os.path, "exists", return_value=True): + cmd = update_module._get_installer_run_command("/tmp/install.sh") + self.assertEqual(cmd, ["/bin/sh", "/tmp/install.sh"]) + + def test_installer_run_command_unix_fallback_to_sh(self): + with patch.object(update_module.sys, "platform", "linux"), \ + patch.object(update_module.os.path, "exists", return_value=False): + cmd = update_module._get_installer_run_command("/tmp/install.sh") + self.assertEqual(cmd, ["sh", "/tmp/install.sh"]) + + def test_installer_run_command_windows_powershell_not_found(self): + with patch.object(update_module.sys, "platform", "win32"), \ + patch.object(update_module.shutil, "which", return_value=None): + with self.assertRaises(FileNotFoundError): + update_module._get_installer_run_command("/tmp/install.ps1") + + def test_installer_run_command_windows_pwsh_fallback(self): + def _which(name): + return "pwsh.exe" if name == "pwsh" else None + + with patch.object(update_module.sys, "platform", "win32"), \ + patch.object(update_module.shutil, "which", side_effect=_which): + cmd = update_module._get_installer_run_command("/tmp/install.ps1") + self.assertEqual(cmd[0], "pwsh.exe") + self.assertIn("-File", cmd) + + +class TestUpdateCommandLogic(unittest.TestCase): + """Tests for the update click command business logic.""" + + def setUp(self): + self.runner = CliRunner() + + @patch("apm_cli.commands.update.get_version", return_value="unknown") + def test_update_dev_version_warns_and_returns(self, mock_version): + result = self.runner.invoke(cli, ["update"]) + self.assertEqual(result.exit_code, 0) + self.assertIn("development mode", result.output) + + @patch("apm_cli.commands.update.get_version", return_value="unknown") + def test_update_dev_version_check_flag_no_reinstall_hint(self, mock_version): + """When --check is passed with dev version, reinstall hint should be suppressed.""" + result = self.runner.invoke(cli, ["update", "--check"]) + self.assertEqual(result.exit_code, 0) + self.assertIn("development mode", result.output) + self.assertNotIn("reinstall", result.output) + + @patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value=None) + @patch("apm_cli.commands.update.get_version", return_value="1.0.0") + def test_update_cannot_fetch_latest_exits_1(self, mock_version, mock_latest): + result = self.runner.invoke(cli, ["update"]) + self.assertEqual(result.exit_code, 1) + self.assertIn("Unable to fetch latest version", result.output) + + @patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.0.0") + @patch("apm_cli.commands.update.get_version", return_value="1.0.0") + def test_update_already_on_latest(self, mock_version, mock_latest): + result = self.runner.invoke(cli, ["update"]) + self.assertEqual(result.exit_code, 0) + self.assertIn("latest version", result.output) + + @patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.1.0") + @patch("apm_cli.commands.update.get_version", return_value="1.0.0") + def test_update_check_flag_shows_available_no_install(self, mock_version, mock_latest): + result = self.runner.invoke(cli, ["update", "--check"]) + self.assertEqual(result.exit_code, 0) + self.assertIn("1.0.0", result.output) + self.assertIn("1.1.0", result.output) + + @patch("requests.get") + @patch("subprocess.run") + @patch("apm_cli.commands.update.get_version", return_value="1.0.0") + @patch("apm_cli.commands.update.os.chmod") + @patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.1.0") + def test_update_installer_failure_exits_1( + self, mock_latest, mock_chmod, mock_version, mock_run, mock_get + ): + mock_response = Mock() + mock_response.text = "echo install" + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + mock_run.return_value = Mock(returncode=1) + + with patch.object(update_module.sys, "platform", "linux"), \ + patch("apm_cli.commands.update.os.path.exists", return_value=True): + result = self.runner.invoke(cli, ["update"]) + + self.assertEqual(result.exit_code, 1) + self.assertIn("Installation failed", result.output) + + @patch("apm_cli.commands.update.get_version", return_value="1.0.0") + @patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.1.0") + def test_update_requests_not_available_exits_1(self, mock_latest, mock_version): + """When requests library is missing, exit with clear message.""" + import builtins + real_import = builtins.__import__ + + def mock_import(name, *args, **kwargs): + if name == "requests": + raise ImportError("No module named 'requests'") + return real_import(name, *args, **kwargs) + + with patch("builtins.__import__", side_effect=mock_import), \ + patch.object(update_module.sys, "platform", "linux"): + result = self.runner.invoke(cli, ["update"]) + + self.assertEqual(result.exit_code, 1) + self.assertIn("requests", result.output) + + @patch("requests.get") + @patch("apm_cli.commands.update.get_version", return_value="1.0.0") + @patch("apm_cli.commands.update.os.chmod") + @patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.1.0") + def test_update_network_error_exits_1(self, mock_latest, mock_chmod, mock_version, mock_get): + mock_get.side_effect = Exception("Network error") + + with patch.object(update_module.sys, "platform", "linux"): + result = self.runner.invoke(cli, ["update"]) + + self.assertEqual(result.exit_code, 1) + self.assertIn("Update failed", result.output) + + @patch("requests.get") + @patch("subprocess.run") + @patch("apm_cli.commands.update.get_version", return_value="1.0.0") + @patch("apm_cli.commands.update.os.chmod") + @patch("apm_cli.utils.version_checker.get_latest_version_from_github", return_value="1.1.0") + def test_update_temp_file_cleanup_on_success( + self, mock_latest, mock_chmod, mock_version, mock_run, mock_get + ): + """Verify temporary script is deleted after successful install.""" + deleted_paths = [] + original_unlink = update_module.os.unlink + + def tracking_unlink(path): + deleted_paths.append(path) + + mock_response = Mock() + mock_response.text = "echo install" + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + mock_run.return_value = Mock(returncode=0) + + with patch.object(update_module.sys, "platform", "linux"), \ + patch("apm_cli.commands.update.os.path.exists", return_value=True), \ + patch.object(update_module.os, "unlink", side_effect=tracking_unlink): + result = self.runner.invoke(cli, ["update"]) + + self.assertEqual(result.exit_code, 0) + self.assertEqual(len(deleted_paths), 1) + self.assertTrue(deleted_paths[0].endswith(".sh")) + + if __name__ == "__main__": unittest.main() \ No newline at end of file