# -*- coding:utf-8 -*- """ Unit tests for ddns.scheduler.schtasks module @author: NewFuture """ import platform from __init__ import patch, unittest from ddns.scheduler.schtasks import SchtasksScheduler from ddns.util.try_run import try_run class TestSchtasksScheduler(unittest.TestCase): """Test SchtasksScheduler functionality""" def setUp(self): """Set up test fixtures""" self.scheduler = SchtasksScheduler() def test_task_name_property(self): """Test task name property""" expected_name = "DDNS" self.assertEqual(self.scheduler.NAME, expected_name) @patch("ddns.scheduler.schtasks.try_run") def test_get_status_installed_enabled(self, mock_run_command): """Test get_status when task is installed and enabled""" # Mock XML output from schtasks query xml_output = """ true PT5M pythonw.exe -m ddns """ mock_run_command.return_value = xml_output status = self.scheduler.get_status() expected_status = {"scheduler": "schtasks", "installed": True, "enabled": True, "interval": 5} # Check main fields self.assertEqual(status["scheduler"], expected_status["scheduler"]) self.assertEqual(status["enabled"], expected_status["enabled"]) self.assertEqual(status["interval"], expected_status["interval"]) @patch("ddns.scheduler.schtasks.try_run") def test_get_status_not_installed(self, mock_run_command): """Test get_status when task is not installed""" # Mock try_run to return None (task not found) mock_run_command.return_value = None status = self.scheduler.get_status() expected_status = {"scheduler": "schtasks", "installed": False} # Check main fields - only scheduler and installed are included when task not found self.assertEqual(status["scheduler"], expected_status["scheduler"]) self.assertEqual(status["installed"], expected_status["installed"]) # When task is not installed, enabled and interval are not included in status @patch("ddns.scheduler.schtasks.try_run") def test_is_installed_true(self, mock_run_command): """Test is_installed returns True when task exists""" mock_run_command.return_value = "TaskName: DDNS\nStatus: Ready" result = self.scheduler.is_installed() self.assertTrue(result) @patch("ddns.scheduler.schtasks.try_run") def test_is_installed_false(self, mock_run_command): """Test is_installed returns False when task doesn't exist""" mock_run_command.return_value = None result = self.scheduler.is_installed() self.assertFalse(result) @patch.object(SchtasksScheduler, "_schtasks") def test_install_success(self, mock_schtasks): """Test successful installation""" mock_schtasks.return_value = True result = self.scheduler.install(5, {"dns": "debug", "ipv4": ["test.com"]}) self.assertTrue(result) mock_schtasks.assert_called_once() @patch.object(SchtasksScheduler, "_schtasks") def test_uninstall_success(self, mock_schtasks): """Test successful uninstall""" mock_schtasks.return_value = True result = self.scheduler.uninstall() self.assertTrue(result) mock_schtasks.assert_called_once() @patch.object(SchtasksScheduler, "_schtasks") def test_enable_success(self, mock_schtasks): """Test successful enable""" mock_schtasks.return_value = True result = self.scheduler.enable() self.assertTrue(result) mock_schtasks.assert_called_once() @patch.object(SchtasksScheduler, "_schtasks") def test_disable_success(self, mock_schtasks): """Test successful disable""" mock_schtasks.return_value = True result = self.scheduler.disable() self.assertTrue(result) mock_schtasks.assert_called_once() def test_build_ddns_command(self): """Test _build_ddns_command functionality""" ddns_args = {"dns": "debug", "ipv4": ["test.example.com"], "config": ["config.json"], "ttl": 300} command = self.scheduler._build_ddns_command(ddns_args) self.assertIsInstance(command, list) command_str = " ".join(command) self.assertIn("python", command_str.lower()) self.assertIn("-m", command) self.assertIn("ddns", command) self.assertIn("--dns", command) self.assertIn("debug", command) self.assertIn("test.example.com", command) def test_xml_extraction(self): """Test _extract_xml functionality""" xml_text = "true" result = self.scheduler._extract_xml(xml_text, "Enabled") self.assertEqual(result, "true") # Test non-existent tag result = self.scheduler._extract_xml(xml_text, "NonExistent") self.assertIsNone(result) @unittest.skipUnless(platform.system().lower() == "windows", "Windows-specific test") def test_real_schtasks_availability(self): """Test if schtasks is available on Windows systems""" if platform.system().lower() == "windows": # On Windows, test basic status call status = self.scheduler.get_status() self.assertIsInstance(status, dict) self.assertIn("scheduler", status) self.assertEqual(status["scheduler"], "schtasks") else: self.skipTest("Windows-specific test") @unittest.skipUnless(platform.system().lower() == "windows", "Windows-specific test") def test_real_schtasks_integration(self): """Test real schtasks integration with actual system calls""" if platform.system().lower() != "windows": self.skipTest("Windows-specific test") # Test real schtasks query for non-existent task status = self.scheduler.get_status() self.assertIsInstance(status, dict) self.assertEqual(status["scheduler"], "schtasks") self.assertIsInstance(status["installed"], bool) # Test schtasks help (safe read-only operation) # Note: We rely on the try_run function for actual subprocess calls # since it has proper timeout and error handling help_result = try_run(["schtasks", "/?"]) if help_result: self.assertIn("schtasks", help_result.lower()) @unittest.skipUnless(platform.system().lower() == "windows", "Windows-specific test") def test_real_scheduler_methods_safe(self): """Test real scheduler methods that don't modify system state""" if platform.system().lower() != "windows": self.skipTest("Windows-specific test") # Test is_installed (safe read-only operation) installed = self.scheduler.is_installed() self.assertIsInstance(installed, bool) # Test build command ddns_args = {"dns": "debug", "ipv4": ["test.example.com"]} command = self.scheduler._build_ddns_command(ddns_args) self.assertIsInstance(command, list) command_str = " ".join(command) self.assertIn("python", command_str.lower()) # Test get status (safe read-only operation) status = self.scheduler.get_status() # Basic keys should always be present basic_required_keys = ["scheduler", "installed"] for key in basic_required_keys: self.assertIn(key, status) # If task is installed, additional keys should be present if status.get("installed", False): additional_keys = ["enabled", "interval"] for key in additional_keys: self.assertIn(key, status) # Test XML extraction utility test_xml = "true" enabled = self.scheduler._extract_xml(test_xml, "Enabled") self.assertEqual(enabled, "true") # No VBS generation anymore; ensure command building returns a list ddns_args = {"dns": "debug", "ipv4": ["test.example.com"]} cmd = self.scheduler._build_ddns_command(ddns_args) self.assertIsInstance(cmd, list) # Test enable/disable without actual installation (should handle gracefully) # These operations will fail if task doesn't exist, but should return boolean enable_result = self.scheduler.enable() self.assertIsInstance(enable_result, bool) disable_result = self.scheduler.disable() self.assertIsInstance(disable_result, bool) @unittest.skipUnless(platform.system().lower() == "windows", "Windows-specific integration test") def test_real_lifecycle_install_enable_disable_uninstall(self): """ Real-life integration test: Full lifecycle of install → enable → disable → uninstall This test actually interacts with Windows Task Scheduler WARNING: This test modifies system state and should only run on test systems Test phases: 1. Clean state verification (uninstall if exists) 2. Install task and verify installation 3. Disable task and verify disabled state 4. Enable task and verify enabled state 5. Uninstall task and verify removal """ if platform.system().lower() != "windows": self.skipTest("Windows-specific integration test") try: # PHASE 1: Ensure clean state - uninstall if exists if self.scheduler.is_installed(): self.scheduler.uninstall() # Verify initial state - task should not exist initial_status = self.scheduler.get_status() self.assertEqual(initial_status["scheduler"], "schtasks") self.assertFalse(initial_status["installed"], "Task should not be installed initially") # PHASE 2: Install the task ddns_args = { "dns": "debug", "ipv4": ["test-integration.example.com"], "config": ["config.json"], "ttl": 300, } install_result = self.scheduler.install(interval=10, ddns_args=ddns_args) self.assertTrue(install_result, "Installation should succeed") # Verify installation post_install_status = self.scheduler.get_status() self.assertTrue(post_install_status["installed"], "Task should be installed after install()") self.assertTrue(post_install_status["enabled"], "Task should be enabled after install()") self.assertEqual(post_install_status["interval"], 10, "Task interval should match") # Command may be pythonw -m ddns ... or compiled exe path; both are acceptable self.assertTrue( (post_install_status.get("command") or "").lower().find("python") >= 0 or (post_install_status.get("command") or "").lower().endswith(".exe") ) # PHASE 3: Test disable functionality disable_result = self.scheduler.disable() self.assertTrue(disable_result, "Disable should succeed") # Verify disabled state post_disable_status = self.scheduler.get_status() self.assertTrue(post_disable_status["installed"], "Task should still be installed after disable") self.assertFalse(post_disable_status["enabled"], "Task should be disabled after disable()") # PHASE 4: Test enable functionality enable_result = self.scheduler.enable() self.assertTrue(enable_result, "Enable should succeed") # Verify enabled state post_enable_status = self.scheduler.get_status() self.assertTrue(post_enable_status["installed"], "Task should still be installed after enable") self.assertTrue(post_enable_status["enabled"], "Task should be enabled after enable()") # PHASE 5: Test uninstall functionality uninstall_result = self.scheduler.uninstall() self.assertTrue(uninstall_result, "Uninstall should succeed") # Verify final state - task should be gone final_status = self.scheduler.get_status() self.assertFalse(final_status["installed"], "Task should not be installed after uninstall()") # Double-check with is_installed() self.assertFalse(self.scheduler.is_installed(), "is_installed() should return False after uninstall") except Exception as e: # If test fails, ensure cleanup try: if self.scheduler.is_installed(): self.scheduler.uninstall() except Exception: pass # Best effort cleanup raise e finally: try: if self.scheduler.is_installed(): self.scheduler.uninstall() except Exception: pass # Best effort cleanup if __name__ == "__main__": unittest.main()