test_app.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import pytest
  2. import sys
  3. import os
  4. from unittest.mock import MagicMock, patch
  5. from flask import json
  6. from flask_socketio import SocketIOTestClient
  7. # 添加项目根目录到Python路径
  8. sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  9. from app import app, socketio, serial_client, mqtt_client
  10. class TestApp:
  11. def setup_method(self):
  12. """每个测试方法执行前的设置"""
  13. # 创建Flask测试客户端
  14. self.client = app.test_client()
  15. app.config['TESTING'] = True
  16. # 创建SocketIO测试客户端
  17. self.socketio_client = SocketIOTestClient(app, socketio)
  18. # 模拟串口客户端和MQTT客户端
  19. self.mock_serial = patch('app.serial_client').start()
  20. self.mock_mqtt = patch('app.mqtt_client').start()
  21. # 模拟初始状态 (get_status 返回 dict)
  22. self.mock_serial.get_status.return_value = {'connected': False, 'config': None, 'has_error': False}
  23. self.mock_serial.current_config = None
  24. self.mock_mqtt.get_status.return_value = {'connected': False, 'broker': None}
  25. self.mock_mqtt.config = None
  26. def teardown_method(self):
  27. """每个测试方法执行后的清理"""
  28. # 停止所有模拟
  29. patch.stopall()
  30. # 断开SocketIO连接
  31. self.socketio_client.disconnect()
  32. def test_serial_connect_api(self):
  33. """测试串口连接API"""
  34. # 模拟连接成功
  35. self.mock_serial.connect.return_value = (True, "串口连接成功")
  36. # 发送POST请求
  37. response = self.client.post('/api/serial/connect',
  38. data=json.dumps({
  39. 'port': 'COM1',
  40. 'baudrate': 9600
  41. }),
  42. content_type='application/json')
  43. # 验证响应
  44. assert response.status_code == 200
  45. data = json.loads(response.data)
  46. assert data['success'] is True
  47. assert data['message'] == "串口连接成功"
  48. # 验证调用 (使用关键字参数)
  49. self.mock_serial.connect.assert_called_once_with(
  50. 'COM1', baudrate=9600, timeout=0.1, bytesize=8, parity='N', stopbits=1
  51. )
  52. def test_serial_disconnect_api(self):
  53. """测试串口断开连接API"""
  54. # 模拟断开返回 (success, message)
  55. self.mock_serial.disconnect.return_value = (True, "已断开连接")
  56. # 发送POST请求
  57. response = self.client.post('/api/serial/disconnect')
  58. # 验证响应
  59. assert response.status_code == 200
  60. data = json.loads(response.data)
  61. assert data['success'] is True
  62. assert data['message'] == "已断开连接"
  63. # 验证调用
  64. self.mock_serial.disconnect.assert_called_once()
  65. def test_serial_status_api(self):
  66. """测试获取串口状态API"""
  67. # 模拟串口已连接 (get_status 返回 dict)
  68. self.mock_serial.get_status.return_value = {'connected': True, 'config': None, 'has_error': False}
  69. # 发送GET请求
  70. response = self.client.get('/api/serial/status')
  71. # 验证响应
  72. assert response.status_code == 200
  73. data = json.loads(response.data)
  74. assert data['connected'] is True
  75. def test_serial_send_api(self):
  76. """测试串口发送数据API"""
  77. # 模拟发送成功
  78. self.mock_serial.send_data.return_value = (True, "数据发送成功")
  79. # 发送POST请求 (endpoint 使用 'message' 字段)
  80. response = self.client.post('/api/serial/send',
  81. data=json.dumps({
  82. 'message': 'test message'
  83. }),
  84. content_type='application/json')
  85. # 验证响应
  86. assert response.status_code == 200
  87. data = json.loads(response.data)
  88. assert data['success'] is True
  89. # 验证调用
  90. self.mock_serial.send_data.assert_called_once_with('test message')
  91. def test_mqtt_connect_api(self):
  92. """测试MQTT连接API"""
  93. # 模拟连接成功
  94. self.mock_mqtt.connect.return_value = (True, "MQTT连接成功")
  95. # 发送POST请求
  96. mqtt_config = {
  97. 'broker': 'localhost',
  98. 'port': 1883,
  99. 'username': '',
  100. 'password': '',
  101. 'client_id': 'test_client',
  102. 'keepalive': 60,
  103. 'use_tls': False
  104. }
  105. response = self.client.post('/api/mqtt/connect',
  106. data=json.dumps(mqtt_config),
  107. content_type='application/json')
  108. # 验证响应
  109. assert response.status_code == 200
  110. data = json.loads(response.data)
  111. assert data['success'] is True
  112. assert data['message'] == "MQTT连接成功"
  113. # 验证调用 (endpoint 使用 broker= 关键字参数)
  114. self.mock_mqtt.connect.assert_called_once()
  115. _, kwargs = self.mock_mqtt.connect.call_args
  116. assert kwargs['broker'] == 'localhost'
  117. assert kwargs['port'] == 1883
  118. assert kwargs['client_id'] == 'test_client'
  119. def test_mqtt_disconnect_api(self):
  120. """测试MQTT断开连接API"""
  121. # 模拟断开返回 (success, message)
  122. self.mock_mqtt.disconnect.return_value = (True, "已断开连接")
  123. # 发送POST请求
  124. response = self.client.post('/api/mqtt/disconnect')
  125. # 验证响应
  126. assert response.status_code == 200
  127. data = json.loads(response.data)
  128. assert data['success'] is True
  129. assert data['message'] == "已断开连接"
  130. # 验证调用
  131. self.mock_mqtt.disconnect.assert_called_once()
  132. def test_mqtt_status_api(self):
  133. """测试获取MQTT状态API"""
  134. # 模拟MQTT已连接 (get_status 返回 dict)
  135. self.mock_mqtt.get_status.return_value = {'connected': True, 'broker': 'localhost'}
  136. # 发送GET请求
  137. response = self.client.get('/api/mqtt/status')
  138. # 验证响应
  139. assert response.status_code == 200
  140. data = json.loads(response.data)
  141. assert data['connected'] is True
  142. def test_mqtt_publish_api(self):
  143. """测试MQTT发布消息API"""
  144. # 模拟发布成功
  145. self.mock_mqtt.publish.return_value = (True, "消息发布成功")
  146. # 发送POST请求
  147. response = self.client.post('/api/mqtt/publish',
  148. data=json.dumps({
  149. 'topic': 'test/topic',
  150. 'message': 'test message'
  151. }),
  152. content_type='application/json')
  153. # 验证响应
  154. assert response.status_code == 200
  155. data = json.loads(response.data)
  156. assert data['success'] is True
  157. # 验证调用
  158. self.mock_mqtt.publish.assert_called_once_with('test/topic', 'test message')
  159. def test_mqtt_subscribe_api(self):
  160. """测试MQTT订阅主题API"""
  161. # 模拟订阅成功
  162. self.mock_mqtt.subscribe.return_value = (True, "主题订阅成功")
  163. # 发送POST请求 (endpoint 使用 'topics' 字段)
  164. response = self.client.post('/api/mqtt/subscribe',
  165. data=json.dumps({
  166. 'topics': ['test/topic']
  167. }),
  168. content_type='application/json')
  169. # 验证响应
  170. assert response.status_code == 200
  171. data = json.loads(response.data)
  172. assert data['success'] is True
  173. # 验证调用
  174. self.mock_mqtt.subscribe.assert_called_once_with(['test/topic'])
  175. def test_get_serial_data_api(self):
  176. """测试获取串口数据API"""
  177. # 发送GET请求
  178. response = self.client.get('/api/data/serial')
  179. # 验证响应 (endpoint 返回 {'data': [...]})
  180. assert response.status_code == 200
  181. data = json.loads(response.data)
  182. assert 'data' in data
  183. assert isinstance(data['data'], list)
  184. def test_get_mqtt_data_api(self):
  185. """测试获取MQTT数据API"""
  186. # 发送GET请求
  187. response = self.client.get('/api/data/mqtt')
  188. # 验证响应 (endpoint 返回 {'data': [...]})
  189. assert response.status_code == 200
  190. data = json.loads(response.data)
  191. assert 'data' in data
  192. assert isinstance(data['data'], list)
  193. def test_forward_config_api(self):
  194. """测试设置转发配置API"""
  195. # 发送POST请求
  196. response = self.client.post('/api/forward/config',
  197. data=json.dumps({
  198. 'serial_to_mqtt': True,
  199. 'mqtt_to_serial': False,
  200. 'publish_topic': 'serial/data'
  201. }),
  202. content_type='application/json')
  203. # 验证响应
  204. assert response.status_code == 200
  205. data = json.loads(response.data)
  206. assert data['success'] is True
  207. assert data['message'] == "转发配置已更新"
  208. def test_get_forward_status_api(self):
  209. """测试获取转发状态API"""
  210. # 发送GET请求
  211. response = self.client.get('/api/forward/status')
  212. # 验证响应
  213. assert response.status_code == 200
  214. data = json.loads(response.data)
  215. assert 'serial_to_mqtt' in data
  216. assert 'mqtt_to_serial' in data
  217. assert 'publish_topic' in data
  218. def test_health_check_api(self):
  219. """测试健康检查API"""
  220. # 发送GET请求
  221. response = self.client.get('/api/health')
  222. # 验证响应
  223. assert response.status_code == 200
  224. data = json.loads(response.data)
  225. assert data['status'] == 'healthy'
  226. assert 'timestamp' in data
  227. assert 'services' in data
  228. def test_socketio_data_namespace(self):
  229. """测试数据命名空间的WebSocket连接"""
  230. # 连接到数据命名空间
  231. self.socketio_client.connect(namespace='/data')
  232. # 验证连接成功
  233. received = self.socketio_client.get_received('/data')
  234. # 应该收到历史数据事件
  235. events = [event['name'] for event in received]
  236. assert 'serial_data_history' in events or 'mqtt_data_history' in events
  237. # 断开连接
  238. self.socketio_client.disconnect(namespace='/data')
  239. def test_socketio_status_namespace(self):
  240. """测试状态命名空间的WebSocket连接"""
  241. # 连接到状态命名空间
  242. self.socketio_client.connect(namespace='/status')
  243. # 验证连接成功
  244. received = self.socketio_client.get_received('/status')
  245. # 应该收到当前状态事件
  246. events = [event['name'] for event in received]
  247. assert 'serial_status' in events
  248. assert 'mqtt_status' in events
  249. assert 'forward_status' in events
  250. # 断开连接
  251. self.socketio_client.disconnect(namespace='/status')
  252. def test_switch_screen_skips_offline_panels(self):
  253. """测试屏幕切换跳过离线终端:只落到在线面板"""
  254. import app as app_module
  255. from app import switch_screen_panel, panel_config, screen_lock
  256. orig_config = dict(panel_config)
  257. orig_index = app_module.screen_current_panel_index
  258. def _current_addr():
  259. with screen_lock:
  260. panels = app_module.get_sorted_panels()
  261. idx = app_module.screen_current_panel_index
  262. return panels[idx][1]['address'] if panels else None
  263. try:
  264. panel_config.clear()
  265. panel_config.update({
  266. 'PANEL_1': {'address': 1, 'position': 1, 'status': 'offline'},
  267. 'PANEL_2': {'address': 2, 'position': 2, 'status': 'online'},
  268. 'PANEL_3': {'address': 3, 'position': 3, 'status': 'offline'},
  269. 'PANEL_4': {'address': 4, 'position': 4, 'status': 'online'},
  270. })
  271. with patch.object(app_module, 'refresh_screen_panel', return_value=(True, 'ok')):
  272. # 当前显示 PANEL_1(离线),下一终端 -> 跳过 -> PANEL_2(在线)
  273. with screen_lock:
  274. app_module.screen_current_panel_index = 0
  275. assert switch_screen_panel(1)[0]
  276. assert _current_addr() == 2
  277. # PANEL_2(在线) -> 下一终端 -> 跳过 PANEL_3(离线) -> PANEL_4(在线)
  278. assert switch_screen_panel(1)[0]
  279. assert _current_addr() == 4
  280. # PANEL_4(在线) -> 下一终端 -> 环绕跳过 PANEL_1(离线) -> PANEL_2(在线)
  281. assert switch_screen_panel(1)[0]
  282. assert _current_addr() == 2
  283. # PANEL_2(在线) -> 上一终端 -> 环绕跳过 PANEL_1(离线) -> PANEL_4(在线)
  284. assert switch_screen_panel(-1)[0]
  285. assert _current_addr() == 4
  286. finally:
  287. panel_config.clear()
  288. panel_config.update(orig_config)
  289. with screen_lock:
  290. app_module.screen_current_panel_index = orig_index
  291. def test_switch_screen_all_offline_falls_back(self):
  292. """测试全部终端离线时切换退化为相邻切换,不卡死"""
  293. import app as app_module
  294. from app import switch_screen_panel, panel_config, screen_lock
  295. orig_config = dict(panel_config)
  296. orig_index = app_module.screen_current_panel_index
  297. def _current_panel():
  298. with screen_lock:
  299. panels = app_module.get_sorted_panels()
  300. idx = app_module.screen_current_panel_index
  301. return panels[idx][0] if panels else None
  302. try:
  303. panel_config.clear()
  304. panel_config.update({
  305. 'PANEL_1': {'address': 1, 'position': 1, 'status': 'offline'},
  306. 'PANEL_2': {'address': 2, 'position': 2, 'status': 'offline'},
  307. 'PANEL_3': {'address': 3, 'position': 3, 'status': 'offline'},
  308. })
  309. with patch.object(app_module, 'refresh_screen_panel', return_value=(True, 'ok')):
  310. with screen_lock:
  311. app_module.screen_current_panel_index = 0
  312. assert switch_screen_panel(1)[0]
  313. assert _current_panel() == 'PANEL_2'
  314. finally:
  315. panel_config.clear()
  316. panel_config.update(orig_config)
  317. with screen_lock:
  318. app_module.screen_current_panel_index = orig_index
  319. if __name__ == "__main__":
  320. pytest.main(["-v", __file__])