智慧水务管理系统 - 精河县供水工程综合管理平台

test_notification_service.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. """
  2. 消息通知模块单元测试
  3. 覆盖短信、邮件、应用内通知发送和调度功能
  4. """
  5. import unittest
  6. from unittest.mock import Mock, patch, MagicMock
  7. from datetime import datetime, timedelta
  8. import json
  9. from src.notification.models import Notification, NotificationTemplate, NotificationSchedule
  10. from src.notification.services import (
  11. SMSService, EmailService, InAppService,
  12. NotificationScheduler, NotificationManager
  13. )
  14. class TestSMSService(unittest.TestCase):
  15. """短信服务测试"""
  16. def setUp(self):
  17. self.sms_service = SMSService()
  18. @patch('src.notification.services.requests')
  19. def test_send_sms(self, mock_requests):
  20. """测试发送短信"""
  21. phone_number = "+8613800138000"
  22. message = "您的水费账单已生成,金额:100.00元,请及时缴纳。"
  23. mock_response = Mock()
  24. mock_response.status_code = 200
  25. mock_response.json.return_value = {
  26. "success": True,
  27. "message_id": "sms_001",
  28. "cost": 0.1
  29. }
  30. mock_requests.post.return_value = mock_response
  31. result = self.sms_service.send_sms(phone_number, message)
  32. self.assertTrue(result["success"])
  33. self.assertEqual(result["message_id"], "sms_001")
  34. self.assertEqual(result["cost"], 0.1)
  35. mock_requests.post.assert_called_once()
  36. def test_phone_number_validation(self):
  37. """测试手机号验证"""
  38. # 有效的手机号
  39. valid_phone = "+8613800138000"
  40. is_valid = self.sms_service.validate_phone_number(valid_phone)
  41. self.assertTrue(is_valid)
  42. # 无效的手机号
  43. invalid_phone = "12345"
  44. is_valid = self.sms_service.validate_phone_number(invalid_phone)
  45. self.assertFalse(is_valid)
  46. # 手机号格式不对
  47. wrong_format = "13800138000" # 缺少国家代码
  48. is_valid = self.sms_service.validate_phone_number(wrong_format)
  49. self.assertFalse(is_valid)
  50. @patch('src.notification.services.requests')
  51. def test_sms_batch_send(self, mock_requests):
  52. """测试批量短信发送"""
  53. recipients = [
  54. {"phone": "+8613800138000", "name": "张三"},
  55. {"phone": "+8613900139000", "name": "李四"},
  56. {"phone": "+8614000140000", "name": "王五"}
  57. ]
  58. message = "尊敬的{name},您的水费账单已生成,请及时查看。"
  59. mock_response = Mock()
  60. mock_response.status_code = 200
  61. mock_response.json.return_value = {
  62. "success": True,
  63. "total_sent": 3,
  64. "failed": [],
  65. "cost": 0.3
  66. }
  67. mock_requests.post.return_value = mock_response
  68. result = self.sms_service.batch_send_sms(recipients, message)
  69. self.assertTrue(result["success"])
  70. self.assertEqual(result["total_sent"], 3)
  71. self.assertEqual(result["cost"], 0.3)
  72. self.assertEqual(len(result["failed"]), 0)
  73. @patch('src.notification.services.requests')
  74. def test_sms_scheduling(self, mock_requests):
  75. """测试短信定时发送"""
  76. phone_number = "+8613800138000"
  77. message = "定时发送的短信"
  78. scheduled_time = datetime.now() + timedelta(hours=1)
  79. mock_response = Mock()
  80. mock_response.status_code = 200
  81. mock_response.json.return_value = {
  82. "success": True,
  83. "schedule_id": "schedule_001",
  84. "scheduled_time": scheduled_time.isoformat()
  85. }
  86. mock_requests.post.return_value = mock_response
  87. result = self.sms_service.schedule_sms(phone_number, message, scheduled_time)
  88. self.assertTrue(result["success"])
  89. self.assertEqual(result["schedule_id"], "schedule_001")
  90. self.assertIsNotNone(result["scheduled_time"])
  91. class TestEmailService(unittest.TestCase):
  92. """邮件服务测试"""
  93. def setUp(self):
  94. self.email_service = EmailService()
  95. @patch('src.notification.services.smtplib')
  96. def test_send_email(self, mock_smtp):
  97. """测试发送邮件"""
  98. email_data = {
  99. "to": "customer@example.com",
  100. "subject": "水费账单通知",
  101. "body": "尊敬的客户,您的6月份水费账单已生成,金额:100.00元。",
  102. "is_html": False
  103. }
  104. mock_server = Mock()
  105. mock_smtp.SMTP.return_value = mock_server
  106. mock_server.sendmail.return_value = {}
  107. result = self.email_service.send_email(email_data)
  108. self.assertTrue(result["success"])
  109. self.assertEqual(result["message_id"], "email_001")
  110. mock_server.sendmail.assert_called_once()
  111. @patch('src.notification.services.smtplib')
  112. def test_send_html_email(self, mock_smtp):
  113. """测试发送HTML邮件"""
  114. email_data = {
  115. "to": "customer@example.com",
  116. "subject": "水费账单通知",
  117. "body": "<html><body><h1>水费账单</h1><p>您的6月份水费账单已生成,金额:100.00元。</p></body></html>",
  118. "is_html": True
  119. }
  120. mock_server = Mock()
  121. mock_smtp.SMTP.return_value = mock_server
  122. mock_server.sendmail.return_value = {}
  123. result = self.email_service.send_email(email_data)
  124. self.assertTrue(result["success"])
  125. self.assertEqual(result["is_html"], True)
  126. def test_email_validation(self):
  127. """测试邮箱验证"""
  128. # 有效的邮箱
  129. valid_email = "customer@example.com"
  130. is_valid = self.email_service.validate_email(valid_email)
  131. self.assertTrue(is_valid)
  132. # 无效的邮箱
  133. invalid_email = "invalid-email"
  134. is_valid = self.email_service.validate_email(invalid_email)
  135. self.assertFalse(is_valid)
  136. @patch('src.notification.services.smtplib')
  137. def test_email_with_attachment(self, mock_smtp):
  138. """测试带附件的邮件"""
  139. email_data = {
  140. "to": "customer@example.com",
  141. "subject": "水费账单详情",
  142. "body": "请查收附件中的账单详情。",
  143. "is_html": False,
  144. "attachments": [
  145. {"filename": "bill.pdf", "content": b"PDF content"},
  146. {"filename": "invoice.xlsx", "content": b"Excel content"}
  147. ]
  148. }
  149. mock_server = Mock()
  150. mock_smtp.SMTP.return_value = mock_server
  151. mock_server.send_message.return_value = {}
  152. result = self.email_service.send_email(email_data)
  153. self.assertTrue(result["success"])
  154. self.assertEqual(len(result["attachments"]), 2)
  155. mock_server.send_message.assert_called_once()
  156. class TestInAppService(unittest.TestCase):
  157. """应用内通知服务测试"""
  158. def setUp(self):
  159. self.inapp_service = InAppService()
  160. @patch('src.notification.services.requests')
  161. def test_send_inapp_notification(self, mock_requests):
  162. """测试发送应用内通知"""
  163. notification_data = {
  164. "user_id": "user_001",
  165. "title": "系统通知",
  166. "message": "您的水费账单已生成",
  167. "type": "BILL_GENERATED",
  168. "priority": "normal"
  169. }
  170. mock_response = Mock()
  171. mock_response.status_code = 200
  172. mock_response.json.return_value = {
  173. "success": True,
  174. "notification_id": "notif_001",
  175. "delivered_at": datetime.now().isoformat()
  176. }
  177. mock_requests.post.return_value = mock_response
  178. result = self.inapp_service.send_notification(notification_data)
  179. self.assertTrue(result["success"])
  180. self.assertEqual(result["notification_id"], "notif_001")
  181. self.assertIsNotNone(result["delivered_at"])
  182. def test_notification_template_rendering(self):
  183. """测试通知模板渲染"""
  184. template = {
  185. "title": "账单通知 - {month}月",
  186. "message": "尊敬的{customer_name},您{month}月份的水费账单已生成,金额:{amount}元。",
  187. "data": {
  188. "month": "6",
  189. "customer_name": "张三",
  190. "amount": "100.00"
  191. }
  192. }
  193. rendered = self.inapp_service.render_template(template)
  194. self.assertEqual(rendered["title"], "账单通知 - 6月")
  195. self.assertIn("张三", rendered["message"])
  196. self.assertIn("100.00", rendered["message"])
  197. @patch('src.notification.services.requests')
  198. def test_notification_batch_send(self, mock_requests):
  199. """测试批量应用内通知"""
  200. notifications = [
  201. {
  202. "user_id": "user_001",
  203. "title": "系统更新",
  204. "message": "系统将于今晚进行维护"
  205. },
  206. {
  207. "user_id": "user_002",
  208. "title": "新功能上线",
  209. "message": "移动端新增缴费功能"
  210. }
  211. ]
  212. mock_response = Mock()
  213. mock_response.status_code = 200
  214. mock_response.json.return_value = {
  215. "success": True,
  216. "total_delivered": 2,
  217. "failed": []
  218. }
  219. mock_requests.post.return_value = mock_response
  220. result = self.inapp_service.batch_send_notifications(notifications)
  221. self.assertTrue(result["success"])
  222. self.assertEqual(result["total_delivered"], 2)
  223. self.assertEqual(len(result["failed"]), 0)
  224. def test_notification_priority_handling(self):
  225. """测试通知优先级处理"""
  226. high_priority_notification = {
  227. "user_id": "user_001",
  228. "title": "紧急通知",
  229. "message": "您的账户余额不足",
  230. "priority": "high"
  231. }
  232. low_priority_notification = {
  233. "user_id": "user_002",
  234. "title": "常规通知",
  235. "message": "系统正常维护通知",
  236. "priority": "low"
  237. }
  238. # 高优先级通知应该立即发送
  239. with patch.object(self.inapp_service, 'send_immediately') as mock_send:
  240. mock_send.return_value = {"success": True}
  241. result = self.inapp_service.handle_notification(high_priority_notification)
  242. mock_send.assert_called_once()
  243. # 低优先级通知可以批量处理
  244. with patch.object(self.inapp_service, 'batch_process') as mock_batch:
  245. mock_batch.return_value = {"success": True}
  246. result = self.inapp_service.handle_notification(low_priority_notification)
  247. mock_batch.assert_called_once()
  248. class TestNotificationScheduler(unittest.TestCase):
  249. """通知调度器测试"""
  250. def setUp(self):
  251. self.scheduler = NotificationScheduler()
  252. @patch('src.notification.services.datetime')
  253. def test_schedule_notification(self, mock_datetime):
  254. """测试通知调度"""
  255. notification_data = {
  256. "user_id": "user_001",
  257. "title": "账单提醒",
  258. "message": "您的水费账单即将到期",
  259. "scheduled_time": "2026-06-20T09:00:00Z"
  260. }
  261. mock_now = Mock()
  262. mock_now.return_value = datetime.now()
  263. mock_datetime.datetime.now.return_value = mock_now.return_value()
  264. schedule_id = self.scheduler.schedule_notification(notification_data)
  265. self.assertIsNotNone(schedule_id)
  266. self.assertIsInstance(schedule_id, str)
  267. def test_get_scheduled_notifications(self):
  268. """测试获取已调度通知"""
  269. # 模拟一些已调度的通知
  270. self.scheduler.schedules = {
  271. "schedule_001": {
  272. "notification_id": "notif_001",
  273. "scheduled_time": "2026-06-17T09:00:00Z",
  274. "status": "pending"
  275. },
  276. "schedule_002": {
  277. "notification_id": "notif_002",
  278. "scheduled_time": "2026-06-18T10:00:00Z",
  279. "status": "pending"
  280. }
  281. }
  282. pending_schedules = self.scheduler.get_pending_schedules()
  283. self.assertEqual(len(pending_schedules), 2)
  284. # 检查是否按时排序
  285. self.assertLess(
  286. datetime.fromisoformat(pending_schedules[0]["scheduled_time"]),
  287. datetime.fromisoformat(pending_schedules[1]["scheduled_time"])
  288. )
  289. def test_reschedule_notification(self):
  290. """测试重新调度通知"""
  291. schedule_id = "schedule_001"
  292. new_time = "2026-06-25T09:00:00Z"
  293. # 先调度一个通知
  294. self.scheduler.schedules = {
  295. schedule_id: {
  296. "notification_id": "notif_001",
  297. "scheduled_time": "2026-06-20T09:00:00Z",
  298. "status": "pending"
  299. }
  300. }
  301. result = self.scheduler.reschedule_notification(schedule_id, new_time)
  302. self.assertTrue(result["success"])
  303. self.assertEqual(self.scheduler.schedules[schedule_id]["scheduled_time"], new_time)
  304. def test_cancel_scheduled_notification(self):
  305. """测试取消调度通知"""
  306. schedule_id = "schedule_001"
  307. # 先调度一个通知
  308. self.scheduler.schedules = {
  309. schedule_id: {
  310. "notification_id": "notif_001",
  311. "scheduled_time": "2026-06-20T09:00:00Z",
  312. "status": "pending"
  313. }
  314. }
  315. result = self.scheduler.cancel_notification(schedule_id)
  316. self.assertTrue(result["success"])
  317. self.assertEqual(self.scheduler.schedules[schedule_id]["status"], "cancelled")
  318. class TestNotificationManager(unittest.TestCase):
  319. """通知管理器测试"""
  320. def setUp(self):
  321. self.manager = NotificationManager()
  322. @patch.object(NotificationManager, 'send_sms')
  323. @patch.object(NotificationManager, 'send_email')
  324. @patch.object(NotificationManager, 'send_inapp')
  325. def test_multi_channel_notification(self, mock_inapp, mock_email, mock_sms):
  326. """测试多渠道通知"""
  327. notification_data = {
  328. "channels": ["sms", "email", "inapp"],
  329. "user_id": "user_001",
  330. "message": "您的账单已生成",
  331. "phone": "+8613800138000",
  332. "email": "customer@example.com"
  333. }
  334. mock_sms.return_value = {"success": True}
  335. mock_email.return_value = {"success": True}
  336. mock_inapp.return_value = {"success": True}
  337. result = self.manager.send_multi_channel_notification(notification_data)
  338. self.assertTrue(result["success"])
  339. self.assertEqual(result["delivered_channels"], 3)
  340. mock_sms.assert_called_once()
  341. mock_email.assert_called_once()
  342. mock_inapp.assert_called_once()
  343. def test_notification_routing(self):
  344. """测试通知路由"""
  345. customer_data = {
  346. "user_id": "user_001",
  347. "name": "张三",
  348. "phone": "+8613800138000",
  349. "email": "customer@example.com",
  350. "preferences": {
  351. "notification_channels": ["sms", "email"],
  352. "notification_types": ["BILL", "REMINDER"]
  353. }
  354. }
  355. notification_type = "BILL"
  356. routed_channels = self.manager.route_notification(customer_data, notification_type)
  357. # 应该返回客户偏好的渠道
  358. self.assertIn("sms", routed_channels)
  359. self.assertIn("email", routed_channels)
  360. self.assertNotIn("inapp", routed_channels) # 客户没有偏好
  361. def test_notification_tracking(self):
  362. """测试通知跟踪"""
  363. notification_id = "notif_001"
  364. # 模拟发送通知
  365. result = self.manager.track_notification_delivery(notification_id, "sms", "delivered")
  366. self.assertTrue(result["success"])
  367. self.assertEqual(result["channel"], "sms")
  368. self.assertEqual(result["status"], "delivered")
  369. # 检查跟踪结果
  370. tracking_info = self.manager.get_notification_tracking(notification_id)
  371. self.assertEqual(tracking_info["total_attempts"], 1)
  372. self.assertEqual(tracking_info["delivered_channels"], ["sms"])
  373. def test_notification_analytics(self):
  374. """测试通知分析"""
  375. # 模拟一些通知数据
  376. notification_data = [
  377. {
  378. "id": "notif_001",
  379. "channel": "sms",
  380. "status": "delivered",
  381. "timestamp": "2026-06-16T10:00:00Z"
  382. },
  383. {
  384. "id": "notif_002",
  385. "channel": "email",
  386. "status": "failed",
  387. "timestamp": "2026-06-16T10:01:00Z"
  388. },
  389. {
  390. "id": "notif_003",
  391. "channel": "sms",
  392. "status": "delivered",
  393. "timestamp": "2026-06-16T10:02:00Z"
  394. }
  395. ]
  396. analytics = self.manager.analyze_notifications(notification_data)
  397. self.assertEqual(analytics["total_sent"], 3)
  398. self.assertEqual(analytics["total_delivered"], 2)
  399. self.assertEqual(analytics["total_failed"], 1)
  400. self.assertEqual(analytics["delivery_rate"], 2/3)
  401. if __name__ == '__main__':
  402. unittest.main()