test.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. #!/usr/bin/env python3
  2. """
  3. Test script for Emergency Access Server
  4. Validates system configuration, file access, notifications, and endpoints
  5. """
  6. import os
  7. import sys
  8. import json
  9. import time
  10. import yaml
  11. import tempfile
  12. import subprocess
  13. from typing import Dict, List, Tuple, Any
  14. from config import Config
  15. class EmergencyAccessTester:
  16. def __init__(self, config_path: str = None):
  17. """Initialize tester with configuration"""
  18. self.config = Config(config_path)
  19. self.test_results = []
  20. self.server_process = None
  21. def log_test(self, test_name: str, success: bool, message: str = ""):
  22. """Log test result"""
  23. status = "PASS" if success else "FAIL"
  24. result = {
  25. 'test': test_name,
  26. 'status': status,
  27. 'message': message,
  28. 'timestamp': time.time()
  29. }
  30. self.test_results.append(result)
  31. color = '\033[92m' if success else '\033[91m' # Green or Red
  32. reset = '\033[0m'
  33. print(f"{color}[{status}]{reset} {test_name}: {message}")
  34. return success
  35. def test_config_loading(self) -> bool:
  36. """Test configuration file loading"""
  37. try:
  38. # Test all required config properties
  39. self.config.server_host
  40. self.config.server_port
  41. self.config.key_route
  42. self.config.health_route
  43. self.config.key_file_path
  44. self.config.dummy_file_path
  45. self.config.ntfy_backends_key
  46. self.config.ntfy_backends_health
  47. return self.log_test("Config Loading", True, "All config properties accessible")
  48. except Exception as e:
  49. return self.log_test("Config Loading", False, f"Error: {str(e)}")
  50. def test_file_access(self) -> bool:
  51. """Test access to key and dummy files"""
  52. success = True
  53. # Test key file
  54. try:
  55. if not os.path.exists(self.config.key_file_path):
  56. self.log_test("Key File Exists", False, f"File not found: {self.config.key_file_path}")
  57. success = False
  58. else:
  59. with open(self.config.key_file_path, 'r') as f:
  60. content = f.read().strip()
  61. if not content:
  62. self.log_test("Key File Content", False, "Key file is empty")
  63. success = False
  64. else:
  65. self.log_test("Key File Access", True, f"Key file readable, length: {len(content)}")
  66. except Exception as e:
  67. self.log_test("Key File Access", False, f"Error: {str(e)}")
  68. success = False
  69. # Test dummy file
  70. try:
  71. if not os.path.exists(self.config.dummy_file_path):
  72. self.log_test("Dummy File Exists", False, f"File not found: {self.config.dummy_file_path}")
  73. success = False
  74. else:
  75. with open(self.config.dummy_file_path, 'r') as f:
  76. content = f.read().strip()
  77. if not content:
  78. self.log_test("Dummy File Content", False, "Dummy file is empty")
  79. success = False
  80. else:
  81. self.log_test("Dummy File Access", True, f"Dummy file readable, length: {len(content)}")
  82. except Exception as e:
  83. self.log_test("Dummy File Access", False, f"Error: {str(e)}")
  84. success = False
  85. return success
  86. def test_ntfy_connectivity(self) -> bool:
  87. """Test ntfy backend connectivity"""
  88. success = True
  89. # Test importing ntfy
  90. try:
  91. import ntfy
  92. self.log_test("ntfy Import", True, "dschep/ntfy package available")
  93. except ImportError:
  94. self.log_test("ntfy Import", False, "dschep/ntfy package not installed")
  95. return False
  96. # Test key notification backends
  97. for backend in self.config.ntfy_backends_key:
  98. try:
  99. # Just verify the backend name is valid (assumes global ntfy config is set up)
  100. # We don't actually send notifications during testing
  101. if backend and isinstance(backend, str) and len(backend.strip()) > 0:
  102. self.log_test(f"Key Backend: {backend}", True, "Backend name valid (using global ntfy config)")
  103. else:
  104. self.log_test(f"Key Backend: {backend}", False, "Invalid backend name")
  105. success = False
  106. except Exception as e:
  107. self.log_test(f"Key Backend: {backend}", False, f"Error: {str(e)}")
  108. success = False
  109. # Test health notification backends
  110. for backend in self.config.ntfy_backends_health:
  111. try:
  112. # Similar validation for health backends
  113. if backend and isinstance(backend, str) and len(backend.strip()) > 0:
  114. self.log_test(f"Health Backend: {backend}", True, "Backend name valid (using global ntfy config)")
  115. else:
  116. self.log_test(f"Health Backend: {backend}", False, "Invalid backend name")
  117. success = False
  118. except Exception as e:
  119. self.log_test(f"Health Backend: {backend}", False, f"Error: {str(e)}")
  120. success = False
  121. return success
  122. def start_test_server(self) -> bool:
  123. """Start the server for endpoint testing"""
  124. try:
  125. import subprocess
  126. import time
  127. # Start server in background
  128. cmd = [sys.executable, "main.py"]
  129. env = os.environ.copy()
  130. env['EMERGENCY_CONFIG'] = self.config.config_path
  131. self.server_process = subprocess.Popen(
  132. cmd,
  133. stdout=subprocess.PIPE,
  134. stderr=subprocess.PIPE,
  135. env=env
  136. )
  137. # Wait for server to start
  138. time.sleep(3)
  139. # Check if server is running
  140. if self.server_process.poll() is None:
  141. return self.log_test("Server Start", True, "Server started successfully")
  142. else:
  143. stdout, stderr = self.server_process.communicate()
  144. error_msg = stderr.decode() if stderr else "Unknown error"
  145. return self.log_test("Server Start", False, f"Server failed to start: {error_msg}")
  146. except Exception as e:
  147. return self.log_test("Server Start", False, f"Error: {str(e)}")
  148. def stop_test_server(self):
  149. """Stop the test server"""
  150. if self.server_process and self.server_process.poll() is None:
  151. self.server_process.terminate()
  152. try:
  153. self.server_process.wait(timeout=5)
  154. except subprocess.TimeoutExpired:
  155. self.server_process.kill()
  156. self.server_process.wait()
  157. self.log_test("Server Stop", True, "Server stopped successfully")
  158. def test_endpoints(self) -> bool:
  159. """Test server endpoints"""
  160. success = True
  161. base_url = f"http://{self.config.server_host}:{self.config.server_port}"
  162. # Test health endpoint
  163. try:
  164. import requests
  165. response = requests.get(
  166. f"{base_url}{self.config.health_route}",
  167. timeout=30
  168. )
  169. if response.status_code == 200:
  170. data = response.json()
  171. if data.get('status') == 'ok':
  172. self.log_test("Health Endpoint", True, f"Response: {response.status_code}")
  173. else:
  174. self.log_test("Health Endpoint", False, f"Invalid response: {data}")
  175. success = False
  176. else:
  177. self.log_test("Health Endpoint", False, f"HTTP {response.status_code}")
  178. success = False
  179. except Exception as e:
  180. self.log_test("Health Endpoint", False, f"Error: {str(e)}")
  181. success = False
  182. # Test key endpoint
  183. try:
  184. import requests
  185. response = requests.get(
  186. f"{base_url}{self.config.key_route}",
  187. timeout=30
  188. )
  189. if response.status_code == 200:
  190. data = response.json()
  191. if data.get('success') and 'key_part' in data:
  192. self.log_test("Key Endpoint", True, f"Key retrieved successfully")
  193. else:
  194. self.log_test("Key Endpoint", False, f"Invalid response: {data}")
  195. success = False
  196. else:
  197. self.log_test("Key Endpoint", False, f"HTTP {response.status_code}")
  198. success = False
  199. except Exception as e:
  200. self.log_test("Key Endpoint", False, f"Error: {str(e)}")
  201. success = False
  202. # Test 404 handling
  203. try:
  204. import requests
  205. response = requests.get(f"{base_url}/nonexistent-path", timeout=10)
  206. if response.status_code == 404:
  207. self.log_test("404 Handling", True, "Correctly returns 404 for invalid paths")
  208. else:
  209. self.log_test("404 Handling", False, f"Expected 404, got {response.status_code}")
  210. success = False
  211. except Exception as e:
  212. self.log_test("404 Handling", False, f"Error: {str(e)}")
  213. success = False
  214. return success
  215. def test_fail_safe_behavior(self) -> bool:
  216. """Test fail-safe behavior with invalid backends"""
  217. success = True
  218. # Create temporary config with invalid backends
  219. try:
  220. with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
  221. invalid_config = {
  222. "server": {
  223. "host": self.config.server_host,
  224. "port": self.config.server_port + 1 # Different port
  225. },
  226. "routes": {
  227. "key_route": "/test-key",
  228. "health_route": "/test-health"
  229. },
  230. "files": {
  231. "key_file": self.config.key_file_path,
  232. "dummy_file": self.config.dummy_file_path
  233. },
  234. "notifications": {
  235. "key_backends": ["invalid_backend_test"],
  236. "health_backends": ["invalid_backend_test"],
  237. "key_message": "Test message",
  238. "health_message": "Test health message"
  239. }
  240. }
  241. json.dump(invalid_config, f)
  242. temp_config_path = f.name
  243. # Test with invalid config
  244. test_config = Config(temp_config_path)
  245. # Start server with invalid config
  246. cmd = [sys.executable, "main.py"]
  247. env = os.environ.copy()
  248. env['EMERGENCY_CONFIG'] = temp_config_path
  249. test_process = subprocess.Popen(
  250. cmd,
  251. stdout=subprocess.PIPE,
  252. stderr=subprocess.PIPE,
  253. env=env
  254. )
  255. time.sleep(3)
  256. if test_process.poll() is None:
  257. # Try to access endpoints - should fail due to notification failures
  258. base_url = f"http://{test_config.server_host}:{test_config.server_port}"
  259. try:
  260. import requests
  261. response = requests.get(f"{base_url}/test-key", timeout=15)
  262. if response.status_code == 500:
  263. self.log_test("Fail-Safe Key", True, "Correctly blocks access when notifications fail")
  264. else:
  265. self.log_test("Fail-Safe Key", False, f"Expected 500, got {response.status_code}")
  266. success = False
  267. except Exception as e:
  268. self.log_test("Fail-Safe Key", False, f"Error testing fail-safe: {str(e)}")
  269. success = False
  270. # Clean up
  271. test_process.terminate()
  272. try:
  273. test_process.wait(timeout=5)
  274. except subprocess.TimeoutExpired:
  275. test_process.kill()
  276. else:
  277. self.log_test("Fail-Safe Test", False, "Test server failed to start")
  278. success = False
  279. # Clean up temp file
  280. os.unlink(temp_config_path)
  281. except Exception as e:
  282. self.log_test("Fail-Safe Test", False, f"Error: {str(e)}")
  283. success = False
  284. return success
  285. def run_all_tests(self) -> bool:
  286. """Run all tests and return overall success"""
  287. print("=" * 60)
  288. print("Emergency Access Server Test Suite")
  289. print("=" * 60)
  290. overall_success = True
  291. # Configuration tests
  292. print("\n--- Configuration Tests ---")
  293. overall_success &= self.test_config_loading()
  294. # File system tests
  295. print("\n--- File System Tests ---")
  296. overall_success &= self.test_file_access()
  297. # Network tests
  298. print("\n--- Network Tests ---")
  299. overall_success &= self.test_ntfy_connectivity()
  300. # Server tests
  301. print("\n--- Server Tests ---")
  302. if self.start_test_server():
  303. time.sleep(2) # Give server time to fully start
  304. overall_success &= self.test_endpoints()
  305. self.stop_test_server()
  306. else:
  307. overall_success = False
  308. # Fail-safe tests
  309. print("\n--- Fail-Safe Tests ---")
  310. overall_success &= self.test_fail_safe_behavior()
  311. # Print summary
  312. self.print_summary()
  313. return overall_success
  314. def print_summary(self):
  315. """Print test summary"""
  316. print("\n" + "=" * 60)
  317. print("TEST SUMMARY")
  318. print("=" * 60)
  319. passed = sum(1 for r in self.test_results if r['status'] == 'PASS')
  320. failed = sum(1 for r in self.test_results if r['status'] == 'FAIL')
  321. total = len(self.test_results)
  322. print(f"Total Tests: {total}")
  323. print(f"Passed: {passed}")
  324. print(f"Failed: {failed}")
  325. if failed > 0:
  326. print("\nFAILED TESTS:")
  327. for result in self.test_results:
  328. if result['status'] == 'FAIL':
  329. print(f" - {result['test']}: {result['message']}")
  330. overall_status = "PASS" if failed == 0 else "FAIL"
  331. color = '\033[92m' if failed == 0 else '\033[91m'
  332. reset = '\033[0m'
  333. print(f"\n{color}Overall Status: {overall_status}{reset}")
  334. def main():
  335. """Main function"""
  336. import argparse
  337. parser = argparse.ArgumentParser(description='Test Emergency Access Server')
  338. parser.add_argument('--config', help='Configuration file path')
  339. parser.add_argument('--quick', action='store_true', help='Run quick tests only (skip server startup)')
  340. args = parser.parse_args()
  341. try:
  342. tester = EmergencyAccessTester(args.config)
  343. if args.quick:
  344. # Quick tests only
  345. success = True
  346. success &= tester.test_config_loading()
  347. success &= tester.test_file_access()
  348. success &= tester.test_ntfy_connectivity()
  349. tester.print_summary()
  350. else:
  351. # Full test suite
  352. success = tester.run_all_tests()
  353. sys.exit(0 if success else 1)
  354. except KeyboardInterrupt:
  355. print("\nTest interrupted by user")
  356. sys.exit(1)
  357. except Exception as e:
  358. print(f"Test suite failed: {str(e)}")
  359. sys.exit(1)
  360. if __name__ == '__main__':
  361. main()