test_scheduler_schtasks.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. # -*- coding:utf-8 -*-
  2. """
  3. Unit tests for ddns.scheduler.schtasks module
  4. @author: NewFuture
  5. """
  6. import platform
  7. from __init__ import patch, unittest
  8. from ddns.scheduler.schtasks import SchtasksScheduler
  9. from ddns.util.try_run import try_run
  10. class TestSchtasksScheduler(unittest.TestCase):
  11. """Test SchtasksScheduler functionality"""
  12. def setUp(self):
  13. """Set up test fixtures"""
  14. self.scheduler = SchtasksScheduler()
  15. def test_task_name_property(self):
  16. """Test task name property"""
  17. expected_name = "DDNS"
  18. self.assertEqual(self.scheduler.NAME, expected_name)
  19. @patch("ddns.scheduler.schtasks.try_run")
  20. def test_get_status_installed_enabled(self, mock_run_command):
  21. """Test get_status when task is installed and enabled"""
  22. # Mock XML output from schtasks query
  23. xml_output = """<?xml version=\"1.0\" encoding=\"UTF-16\"?>
  24. <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  25. <Settings>
  26. <Enabled>true</Enabled>
  27. </Settings>
  28. <Triggers>
  29. <TimeTrigger>
  30. <Repetition>
  31. <Interval>PT5M</Interval>
  32. </Repetition>
  33. </TimeTrigger>
  34. </Triggers>
  35. <Actions>
  36. <Exec>
  37. <Command>pythonw.exe</Command>
  38. <Arguments>-m ddns</Arguments>
  39. </Exec>
  40. </Actions>
  41. </Task>"""
  42. mock_run_command.return_value = xml_output
  43. status = self.scheduler.get_status()
  44. expected_status = {"scheduler": "schtasks", "installed": True, "enabled": True, "interval": 5}
  45. # Check main fields
  46. self.assertEqual(status["scheduler"], expected_status["scheduler"])
  47. self.assertEqual(status["enabled"], expected_status["enabled"])
  48. self.assertEqual(status["interval"], expected_status["interval"])
  49. @patch("ddns.scheduler.schtasks.try_run")
  50. def test_get_status_not_installed(self, mock_run_command):
  51. """Test get_status when task is not installed"""
  52. # Mock try_run to return None (task not found)
  53. mock_run_command.return_value = None
  54. status = self.scheduler.get_status()
  55. expected_status = {"scheduler": "schtasks", "installed": False}
  56. # Check main fields - only scheduler and installed are included when task not found
  57. self.assertEqual(status["scheduler"], expected_status["scheduler"])
  58. self.assertEqual(status["installed"], expected_status["installed"])
  59. # When task is not installed, enabled and interval are not included in status
  60. @patch("ddns.scheduler.schtasks.try_run")
  61. def test_is_installed_true(self, mock_run_command):
  62. """Test is_installed returns True when task exists"""
  63. mock_run_command.return_value = "TaskName: DDNS\nStatus: Ready"
  64. result = self.scheduler.is_installed()
  65. self.assertTrue(result)
  66. @patch("ddns.scheduler.schtasks.try_run")
  67. def test_is_installed_false(self, mock_run_command):
  68. """Test is_installed returns False when task doesn't exist"""
  69. mock_run_command.return_value = None
  70. result = self.scheduler.is_installed()
  71. self.assertFalse(result)
  72. @patch.object(SchtasksScheduler, "_schtasks")
  73. def test_install_success(self, mock_schtasks):
  74. """Test successful installation"""
  75. mock_schtasks.return_value = True
  76. result = self.scheduler.install(5, {"dns": "debug", "ipv4": ["test.com"]})
  77. self.assertTrue(result)
  78. mock_schtasks.assert_called_once()
  79. @patch.object(SchtasksScheduler, "_schtasks")
  80. def test_uninstall_success(self, mock_schtasks):
  81. """Test successful uninstall"""
  82. mock_schtasks.return_value = True
  83. result = self.scheduler.uninstall()
  84. self.assertTrue(result)
  85. mock_schtasks.assert_called_once()
  86. @patch.object(SchtasksScheduler, "_schtasks")
  87. def test_enable_success(self, mock_schtasks):
  88. """Test successful enable"""
  89. mock_schtasks.return_value = True
  90. result = self.scheduler.enable()
  91. self.assertTrue(result)
  92. mock_schtasks.assert_called_once()
  93. @patch.object(SchtasksScheduler, "_schtasks")
  94. def test_disable_success(self, mock_schtasks):
  95. """Test successful disable"""
  96. mock_schtasks.return_value = True
  97. result = self.scheduler.disable()
  98. self.assertTrue(result)
  99. mock_schtasks.assert_called_once()
  100. def test_build_ddns_command(self):
  101. """Test _build_ddns_command functionality"""
  102. ddns_args = {"dns": "debug", "ipv4": ["test.example.com"], "config": ["config.json"], "ttl": 300}
  103. command = self.scheduler._build_ddns_command(ddns_args)
  104. self.assertIsInstance(command, list)
  105. command_str = " ".join(command)
  106. self.assertIn("python", command_str.lower())
  107. self.assertIn("-m", command)
  108. self.assertIn("ddns", command)
  109. self.assertIn("--dns", command)
  110. self.assertIn("debug", command)
  111. self.assertIn("test.example.com", command)
  112. def test_xml_extraction(self):
  113. """Test _extract_xml functionality"""
  114. xml_text = "<Settings><Enabled>true</Enabled></Settings>"
  115. result = self.scheduler._extract_xml(xml_text, "Enabled")
  116. self.assertEqual(result, "true")
  117. # Test non-existent tag
  118. result = self.scheduler._extract_xml(xml_text, "NonExistent")
  119. self.assertIsNone(result)
  120. @unittest.skipUnless(platform.system().lower() == "windows", "Windows-specific test")
  121. def test_real_schtasks_availability(self):
  122. """Test if schtasks is available on Windows systems"""
  123. if platform.system().lower() == "windows":
  124. # On Windows, test basic status call
  125. status = self.scheduler.get_status()
  126. self.assertIsInstance(status, dict)
  127. self.assertIn("scheduler", status)
  128. self.assertEqual(status["scheduler"], "schtasks")
  129. else:
  130. self.skipTest("Windows-specific test")
  131. @unittest.skipUnless(platform.system().lower() == "windows", "Windows-specific test")
  132. def test_real_schtasks_integration(self):
  133. """Test real schtasks integration with actual system calls"""
  134. if platform.system().lower() != "windows":
  135. self.skipTest("Windows-specific test")
  136. # Test real schtasks query for non-existent task
  137. status = self.scheduler.get_status()
  138. self.assertIsInstance(status, dict)
  139. self.assertEqual(status["scheduler"], "schtasks")
  140. self.assertIsInstance(status["installed"], bool)
  141. # Test schtasks help (safe read-only operation)
  142. # Note: We rely on the try_run function for actual subprocess calls
  143. # since it has proper timeout and error handling
  144. help_result = try_run(["schtasks", "/?"])
  145. if help_result:
  146. self.assertIn("schtasks", help_result.lower())
  147. @unittest.skipUnless(platform.system().lower() == "windows", "Windows-specific test")
  148. def test_real_scheduler_methods_safe(self):
  149. """Test real scheduler methods that don't modify system state"""
  150. if platform.system().lower() != "windows":
  151. self.skipTest("Windows-specific test")
  152. # Test is_installed (safe read-only operation)
  153. installed = self.scheduler.is_installed()
  154. self.assertIsInstance(installed, bool)
  155. # Test build command
  156. ddns_args = {"dns": "debug", "ipv4": ["test.example.com"]}
  157. command = self.scheduler._build_ddns_command(ddns_args)
  158. self.assertIsInstance(command, list)
  159. command_str = " ".join(command)
  160. self.assertIn("python", command_str.lower())
  161. # Test get status (safe read-only operation)
  162. status = self.scheduler.get_status()
  163. # Basic keys should always be present
  164. basic_required_keys = ["scheduler", "installed"]
  165. for key in basic_required_keys:
  166. self.assertIn(key, status)
  167. # If task is installed, additional keys should be present
  168. if status.get("installed", False):
  169. additional_keys = ["enabled", "interval"]
  170. for key in additional_keys:
  171. self.assertIn(key, status)
  172. # Test XML extraction utility
  173. test_xml = "<Settings><Enabled>true</Enabled></Settings>"
  174. enabled = self.scheduler._extract_xml(test_xml, "Enabled")
  175. self.assertEqual(enabled, "true")
  176. # No VBS generation anymore; ensure command building returns a list
  177. ddns_args = {"dns": "debug", "ipv4": ["test.example.com"]}
  178. cmd = self.scheduler._build_ddns_command(ddns_args)
  179. self.assertIsInstance(cmd, list)
  180. # Test enable/disable without actual installation (should handle gracefully)
  181. # These operations will fail if task doesn't exist, but should return boolean
  182. enable_result = self.scheduler.enable()
  183. self.assertIsInstance(enable_result, bool)
  184. disable_result = self.scheduler.disable()
  185. self.assertIsInstance(disable_result, bool)
  186. @unittest.skipUnless(platform.system().lower() == "windows", "Windows-specific integration test")
  187. def test_real_lifecycle_install_enable_disable_uninstall(self):
  188. """
  189. Real-life integration test: Full lifecycle of install → enable → disable → uninstall
  190. This test actually interacts with Windows Task Scheduler
  191. WARNING: This test modifies system state and should only run on test systems
  192. Test phases:
  193. 1. Clean state verification (uninstall if exists)
  194. 2. Install task and verify installation
  195. 3. Disable task and verify disabled state
  196. 4. Enable task and verify enabled state
  197. 5. Uninstall task and verify removal
  198. """
  199. if platform.system().lower() != "windows":
  200. self.skipTest("Windows-specific integration test")
  201. try:
  202. # PHASE 1: Ensure clean state - uninstall if exists
  203. if self.scheduler.is_installed():
  204. self.scheduler.uninstall()
  205. # Verify initial state - task should not exist
  206. initial_status = self.scheduler.get_status()
  207. self.assertEqual(initial_status["scheduler"], "schtasks")
  208. self.assertFalse(initial_status["installed"], "Task should not be installed initially")
  209. # PHASE 2: Install the task
  210. ddns_args = {
  211. "dns": "debug",
  212. "ipv4": ["test-integration.example.com"],
  213. "config": ["config.json"],
  214. "ttl": 300,
  215. }
  216. install_result = self.scheduler.install(interval=10, ddns_args=ddns_args)
  217. self.assertTrue(install_result, "Installation should succeed")
  218. # Verify installation
  219. post_install_status = self.scheduler.get_status()
  220. self.assertTrue(post_install_status["installed"], "Task should be installed after install()")
  221. self.assertTrue(post_install_status["enabled"], "Task should be enabled after install()")
  222. self.assertEqual(post_install_status["interval"], 10, "Task interval should match")
  223. # Command may be pythonw -m ddns ... or compiled exe path; both are acceptable
  224. self.assertTrue(
  225. (post_install_status.get("command") or "").lower().find("python") >= 0
  226. or (post_install_status.get("command") or "").lower().endswith(".exe")
  227. )
  228. # PHASE 3: Test disable functionality
  229. disable_result = self.scheduler.disable()
  230. self.assertTrue(disable_result, "Disable should succeed")
  231. # Verify disabled state
  232. post_disable_status = self.scheduler.get_status()
  233. self.assertTrue(post_disable_status["installed"], "Task should still be installed after disable")
  234. self.assertFalse(post_disable_status["enabled"], "Task should be disabled after disable()")
  235. # PHASE 4: Test enable functionality
  236. enable_result = self.scheduler.enable()
  237. self.assertTrue(enable_result, "Enable should succeed")
  238. # Verify enabled state
  239. post_enable_status = self.scheduler.get_status()
  240. self.assertTrue(post_enable_status["installed"], "Task should still be installed after enable")
  241. self.assertTrue(post_enable_status["enabled"], "Task should be enabled after enable()")
  242. # PHASE 5: Test uninstall functionality
  243. uninstall_result = self.scheduler.uninstall()
  244. self.assertTrue(uninstall_result, "Uninstall should succeed")
  245. # Verify final state - task should be gone
  246. final_status = self.scheduler.get_status()
  247. self.assertFalse(final_status["installed"], "Task should not be installed after uninstall()")
  248. # Double-check with is_installed()
  249. self.assertFalse(self.scheduler.is_installed(), "is_installed() should return False after uninstall")
  250. except Exception as e:
  251. # If test fails, ensure cleanup
  252. try:
  253. if self.scheduler.is_installed():
  254. self.scheduler.uninstall()
  255. except Exception:
  256. pass # Best effort cleanup
  257. raise e
  258. finally:
  259. try:
  260. if self.scheduler.is_installed():
  261. self.scheduler.uninstall()
  262. except Exception:
  263. pass # Best effort cleanup
  264. if __name__ == "__main__":
  265. unittest.main()