test_video_platform_api.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. from __future__ import annotations
  2. from pathlib import Path
  3. from fastapi.testclient import TestClient
  4. from app.core.config import settings
  5. from app.db.init_db import init_db
  6. from app.db.seed import seed_demo_data
  7. from app.db.session import SessionLocal
  8. from app.main import app
  9. def _prepare() -> None:
  10. init_db()
  11. with SessionLocal() as db:
  12. seed_demo_data(db)
  13. def _mine_id(client: TestClient) -> str:
  14. response = client.get("/api/v1/mines")
  15. assert response.status_code == 200
  16. return response.json()[0]["id"]
  17. def test_video_platform_overview_and_modules() -> None:
  18. _prepare()
  19. with TestClient(app) as client:
  20. mine_id = _mine_id(client)
  21. overview = client.get(
  22. "/api/v1/video-platform/overview", params={"mine_id": mine_id}
  23. )
  24. assert overview.status_code == 200
  25. payload = overview.json()
  26. assert payload["channel_total"] == 18
  27. assert payload["channel_online"] == 15
  28. assert payload["channel_offline"] == 1
  29. assert payload["channel_fault"] == 2
  30. assert payload["snapshot_count"] > 0
  31. assert len(payload["summary"]) == 4
  32. modules = client.get(
  33. "/api/v1/video-platform/modules", params={"mine_id": mine_id}
  34. )
  35. assert modules.status_code == 200
  36. configs = {item["key"]: item for item in modules.json()}
  37. assert set(configs) == {"door", "local_fan", "main_fan"}
  38. assert len(configs["door"]["record_tabs"]) == 3
  39. assert configs["door"]["record_tabs"][0]["record_type"] == "vehicle_passage"
  40. assert "AI防夹人预警" in configs["door"]["type_filter_options"]
  41. assert "全部设备" in configs["local_fan"]["scope_filter_options"]
  42. assert len(configs["local_fan"]["scope_filter_options"]) == 6
  43. def test_video_platform_channels_keep_disk_paths() -> None:
  44. _prepare()
  45. with TestClient(app) as client:
  46. mine_id = _mine_id(client)
  47. response = client.get(
  48. "/api/v1/video-platform/channels",
  49. params={"mine_id": mine_id, "module": "door"},
  50. )
  51. assert response.status_code == 200
  52. channels = response.json()
  53. assert len(channels) == 8
  54. doo_channel = channels[0]
  55. assert Path(doo_channel["file_path"]).is_absolute()
  56. assert doo_channel["file_path"].startswith(str(settings.video_media_path))
  57. assert doo_channel["file_name"].endswith(".mp4")
  58. assert doo_channel["playback_url"].endswith("/stream")
  59. assert doo_channel["snapshot_url"].endswith("/snapshot")
  60. assert doo_channel["has_video"] is True
  61. search = client.get(
  62. "/api/v1/video-platform/channels",
  63. params={"mine_id": mine_id, "keyword": "变电所"},
  64. )
  65. assert search.status_code == 200
  66. assert [item["code"] for item in search.json()] == ["VC-DOOR-04"]
  67. def test_video_platform_status_summary_and_records() -> None:
  68. _prepare()
  69. with TestClient(app) as client:
  70. mine_id = _mine_id(client)
  71. summary = client.get(
  72. "/api/v1/video-platform/modules/door/summary",
  73. params={"mine_id": mine_id},
  74. )
  75. assert summary.status_code == 200
  76. metrics = {item["key"]: item["value"] for item in summary.json()["metrics"]}
  77. assert metrics["normal_door"] == 6
  78. assert metrics["car_today"] == 12
  79. assert metrics["person_today"] == 11
  80. assert metrics["open_warning"] > 0
  81. fan_summary = client.get(
  82. "/api/v1/video-platform/modules/local_fan/summary",
  83. params={"mine_id": mine_id},
  84. ).json()
  85. fan_metrics = {item["key"]: item["value"] for item in fan_summary["metrics"]}
  86. assert fan_metrics["normal_fan"] == 4
  87. assert fan_metrics["fault_fan"] == 1
  88. assert fan_metrics["onsite_person"] == 2
  89. main_summary = client.get(
  90. "/api/v1/video-platform/modules/main_fan/summary",
  91. params={"mine_id": mine_id},
  92. ).json()
  93. main_metrics = {item["key"]: item["value"] for item in main_summary["metrics"]}
  94. assert main_metrics["running_fan"] == 1
  95. assert main_metrics["stopped_fan"] == 1
  96. assert main_metrics["person_detect"] == 6
  97. records = client.get(
  98. "/api/v1/video-platform/records",
  99. params={
  100. "mine_id": mine_id,
  101. "module": "door",
  102. "record_type": "vehicle_passage",
  103. "page_size": 5,
  104. },
  105. )
  106. assert records.status_code == 200
  107. record_page = records.json()
  108. assert record_page["total"] == 12
  109. assert len(record_page["items"]) == 5
  110. assert record_page["items"][0]["plate_no"].startswith("蒙K")
  111. assert record_page["items"][0]["snapshot_image_url"].endswith("/image")
  112. tracks = client.get(
  113. "/api/v1/video-platform/records",
  114. params={
  115. "mine_id": mine_id,
  116. "module": "local_fan",
  117. "record_type": "person_track",
  118. },
  119. ).json()
  120. assert tracks["total"] == 8
  121. area_rows = client.get(
  122. "/api/v1/video-platform/records",
  123. params={
  124. "mine_id": mine_id,
  125. "module": "local_fan",
  126. "record_type": "area_stay",
  127. },
  128. ).json()
  129. assert area_rows["total"] == 5
  130. assert "avg_stay" in area_rows["items"][0]["properties"]
  131. warn_rows = client.get(
  132. "/api/v1/video-platform/records",
  133. params={
  134. "mine_id": mine_id,
  135. "module": "main_fan",
  136. "record_type": "device_warn",
  137. },
  138. ).json()
  139. assert warn_rows["total"] == 4
  140. assert warn_rows["items"][0]["warn_type"]
  141. def test_video_platform_snapshots_and_events() -> None:
  142. _prepare()
  143. with TestClient(app) as client:
  144. mine_id = _mine_id(client)
  145. snapshots = client.get(
  146. "/api/v1/video-platform/snapshots",
  147. params={"mine_id": mine_id, "module": "door", "page_size": 100},
  148. )
  149. assert snapshots.status_code == 200
  150. page = snapshots.json()
  151. assert page["total"] == 40
  152. assert page["items"][0]["image_url"].endswith("/image")
  153. filtered = client.get(
  154. "/api/v1/video-platform/snapshots",
  155. params={
  156. "mine_id": mine_id,
  157. "module": "door",
  158. "event_type": "风门未关严",
  159. "page_size": 100,
  160. },
  161. ).json()
  162. assert filtered["total"] == 9
  163. assert all(item["gap_mm"] for item in filtered["items"])
  164. keyword = client.get(
  165. "/api/v1/video-platform/snapshots",
  166. params={"mine_id": mine_id, "keyword": "蒙K3571"},
  167. ).json()
  168. assert keyword["total"] == 1
  169. events = client.get(
  170. "/api/v1/video-platform/events",
  171. params={"mine_id": mine_id, "module": "door"},
  172. )
  173. assert events.status_code == 200
  174. event_items = events.json()
  175. assert len(event_items) == 10
  176. assert {item["level"] for item in event_items} >= {"danger", "warning", "info"}
  177. linked = [item for item in event_items if item["snapshot_image_url"]]
  178. assert linked
  179. open_events = client.get(
  180. "/api/v1/video-platform/events",
  181. params={"mine_id": mine_id, "module": "main_fan", "status": "pending"},
  182. ).json()
  183. assert open_events
  184. event_id = open_events[0]["id"]
  185. updated = client.patch(
  186. f"/api/v1/video-platform/events/{event_id}",
  187. json={"status": "handled", "handled_by": "通防科值班员", "handle_remark": "现场已复核"},
  188. )
  189. assert updated.status_code == 200
  190. updated_payload = updated.json()
  191. assert updated_payload["status"] == "handled"
  192. assert updated_payload["handled_at"] is not None
  193. assert updated_payload["handle_remark"] == "现场已复核"
  194. def test_video_platform_channel_media_stream_and_crud(tmp_path: Path) -> None:
  195. _prepare()
  196. video_file = tmp_path / "demo.mp4"
  197. video_file.write_bytes(b"0123456789")
  198. image_file = tmp_path / "demo.jpg"
  199. image_file.write_bytes(b"fake-image-bytes")
  200. with TestClient(app) as client:
  201. mine_id = _mine_id(client)
  202. created = client.post(
  203. "/api/v1/video-platform/channels",
  204. json={
  205. "mine_id": mine_id,
  206. "code": "VC-TEST-01",
  207. "name": "测试通道-煤仓口",
  208. "module": "door",
  209. "area_group": "风门区域",
  210. "location": "测试地点",
  211. "device_type": "door",
  212. "status": "online",
  213. "file_path": str(video_file),
  214. "properties": {"door_side": "前门"},
  215. },
  216. )
  217. assert created.status_code == 201
  218. channel = created.json()
  219. channel_id = channel["id"]
  220. assert channel["file_path"] == str(video_file)
  221. assert channel["file_name"] == "demo.mp4"
  222. assert channel["file_size"] == 10
  223. assert channel["media_available"] is True
  224. duplicated = client.post(
  225. "/api/v1/video-platform/channels",
  226. json={
  227. "mine_id": mine_id,
  228. "code": "VC-TEST-01",
  229. "name": "重复编码",
  230. "module": "door",
  231. },
  232. )
  233. assert duplicated.status_code == 409
  234. full = client.get(f"/api/v1/video-platform/channels/{channel_id}/stream")
  235. assert full.status_code == 200
  236. assert full.headers["accept-ranges"] == "bytes"
  237. assert full.content == b"0123456789"
  238. partial = client.get(
  239. f"/api/v1/video-platform/channels/{channel_id}/stream",
  240. headers={"Range": "bytes=2-5"},
  241. )
  242. assert partial.status_code == 206
  243. assert partial.headers["content-range"] == "bytes 2-5/10"
  244. assert partial.content == b"2345"
  245. patched = client.patch(
  246. f"/api/v1/video-platform/channels/{channel_id}",
  247. json={"name": "测试通道-煤仓口(改)", "status": "offline"},
  248. )
  249. assert patched.status_code == 200
  250. assert patched.json()["name"] == "测试通道-煤仓口(改)"
  251. assert patched.json()["status"] == "offline"
  252. snapshot = client.get(
  253. "/api/v1/video-platform/snapshots",
  254. params={"mine_id": mine_id, "page_size": 200},
  255. ).json()["items"][0]
  256. image = client.get(f"/api/v1/video-platform/snapshots/{snapshot['id']}/image")
  257. assert image.status_code in (200, 404)
  258. missing = client.post(
  259. "/api/v1/video-platform/channels",
  260. json={
  261. "mine_id": mine_id,
  262. "code": "VC-TEST-02",
  263. "name": "测试通道-缺失文件",
  264. "module": "door",
  265. "file_path": str(tmp_path / "not-found.mp4"),
  266. },
  267. ).json()
  268. missing_stream = client.get(
  269. f"/api/v1/video-platform/channels/{missing['id']}/stream"
  270. )
  271. assert missing_stream.status_code == 404
  272. assert missing_stream.json()["code"] == "video_media_missing"
  273. deleted = client.delete(f"/api/v1/video-platform/channels/{channel_id}")
  274. assert deleted.status_code == 204
  275. assert (
  276. client.get(f"/api/v1/video-platform/channels/{channel_id}").status_code
  277. == 404
  278. )
  279. client.delete(f"/api/v1/video-platform/channels/{missing['id']}")
  280. assert image_file.exists() # 图像文件保持磁盘原样,仅记录路径