model_train_dm/backend/apps/datasets/tests/test_smart_terminal.py
2026-07-27 17:51:49 +08:00

288 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""智能终端 API 接口测试"""
import uuid
from django.contrib.auth import get_user_model
from django.db import connection
from django.test import TestCase
from rest_framework.test import APIClient
from apps.core.models import AiSmartTerminal
User = get_user_model()
# ---------- 5 条测试设备数据 ----------
TEST_DEVICES = [
{
"stcd": "ZD001",
"stnm": "碧水电站-鱼类识别AI盒子",
"sttp": "01",
"lgtd": 104.065800,
"lttd": 30.572800,
"elev": 512.500000,
"rstcd": "BS001",
"base_id": "YJ001",
"rvcd": "MJ001",
"aipz": '{"cpu":"ARM Cortex-A78","memory":"8GB","storage":"64GB"}',
"ipaddr": "192.168.1.101",
"usfl": 1,
"dtin": 1,
"mway": 1,
"sync_check_interval": 30,
},
{
"stcd": "ZD002",
"stnm": "青湖-鸟类识别AI盒子",
"sttp": "02",
"lgtd": 104.125600,
"lttd": 30.681200,
"elev": 489.300000,
"rstcd": "QH002",
"base_id": "YJ001",
"rvcd": "MJ002",
"aipz": '{"cpu":"ARM Cortex-A78","memory":"4GB","storage":"32GB"}',
"ipaddr": "192.168.1.102",
"usfl": 1,
"dtin": 1,
"mway": 1,
"sync_check_interval": 60,
},
{
"stcd": "ZD003",
"stnm": "卧龙自然保护区-野生动物识别AI盒子",
"sttp": "03",
"lgtd": 103.298400,
"lttd": 30.985600,
"elev": 1820.000000,
"rstcd": "WL003",
"base_id": "YJ002",
"rvcd": "MJ003",
"aipz": '{"cpu":"NVIDIA Jetson Orin","memory":"16GB","storage":"256GB"}',
"ipaddr": "192.168.2.101",
"usfl": 1,
"dtin": 0,
"mway": 2,
"sync_check_interval": 120,
},
{
"stcd": "ZD004",
"stnm": "长江上游-鱼类分拣装置",
"sttp": "04",
"lgtd": 104.545200,
"lttd": 30.127800,
"elev": 345.000000,
"rstcd": "CJ004",
"base_id": "YJ003",
"rvcd": "CJ001",
"aipz": '{"cpu":"Intel i5-1135G7","memory":"8GB","storage":"128GB","grab_arm":true}',
"ipaddr": "192.168.3.201",
"usfl": 0,
"dtin": 0,
"mway": 0,
"sync_check_interval": None,
},
{
"stcd": "ZD005",
"stnm": "洞庭湖-鸟类识别AI盒子",
"sttp": "02",
"lgtd": 112.946800,
"lttd": 29.358700,
"elev": 35.200000,
"rstcd": "DT005",
"base_id": "YJ004",
"rvcd": "CJ002",
"aipz": '{"cpu":"ARM Cortex-A78","memory":"8GB","storage":"64GB"}',
"ipaddr": "192.168.4.101",
"usfl": 1,
"dtin": 1,
"mway": 1,
"sync_check_interval": 45,
},
]
class SmartTerminalAPITest(TestCase):
"""智能终端 API 集成测试 — 覆盖全部 7 个接口"""
@classmethod
def setUpTestData(cls):
"""类级别:测试库建表 + 创建 admin 用户 + 插入 5 条测试设备"""
cls.base_url = "/server/terminal"
# Django 测试每次新建 test_ 库,需在测试库中建表
with connection.schema_editor() as schema_editor:
schema_editor.create_model(AiSmartTerminal)
# 创建 admin 用户
cls.admin_user = User.objects.create_user(
username="admin", password="123456", is_active=True, is_staff=True,
)
# 通过 ORM 直接插入 5 条设备
cls.created_ids = []
for device in TEST_DEVICES:
obj = AiSmartTerminal.objects.create(
id=uuid.uuid4().hex,
record_user="admin",
**device,
)
cls.created_ids.append(obj.id)
assert AiSmartTerminal.objects.filter(is_deleted=0).count() == 5
def setUp(self):
"""每个测试用例前:用 admin 强制登录"""
self.client = APIClient()
self.client.force_login(self.admin_user)
# ==================== 1. 创建 ====================
def test_01_create_duplicate_stcd_should_reject(self):
"""重复的 stcd 应被拒绝"""
resp = self.client.post(f"{self.base_url}/create/", {
"stcd": "ZD001",
"stnm": "重复设备",
"sttp": "01",
}, format="json")
self.assertEqual(resp.status_code, 400)
# ==================== 2. 修改 ====================
def test_02_update_terminal_info(self):
"""修改设备基本信息"""
target = self.created_ids[0]
resp = self.client.put(f"{self.base_url}/update/", {
"id": target,
"stnm": "碧水电站-鱼类识别AI盒子已升级",
"sync_check_interval": 15,
}, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data["stnm"], "碧水电站-鱼类识别AI盒子已升级")
def test_03_update_protected_fields_should_be_ignored(self):
"""修改受保护字段应被忽略"""
target = self.created_ids[0]
resp = self.client.put(f"{self.base_url}/update/", {
"id": target,
"model_version": "hacked_v99",
"online_status": 0,
}, format="json")
self.assertEqual(resp.status_code, 200)
# 受保护字段不应被修改
self.assertIsNone(resp.data["model_version"])
self.assertIsNone(resp.data["online_status"])
# ==================== 3. 发布模型 ====================
def test_04_publish_model_to_single_terminal(self):
"""发布模型到单个终端"""
target = self.created_ids[0]
resp = self.client.put(f"{self.base_url}/publishModel/", {
"id": target,
"model_id": "model_fish_v2",
"model_version": "v2.1.0",
"model_path": "models/fish_detect/v2.1.0/model.onnx",
}, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data["published_count"], 1)
updated = AiSmartTerminal.objects.get(id=target)
self.assertEqual(updated.model_version, "v2.1.0")
self.assertIsNotNone(updated.model_publish_time)
def test_05_publish_model_to_multiple_terminals(self):
"""发布模型到多个终端"""
targets = self.created_ids[:3]
resp = self.client.put(f"{self.base_url}/publishModel/", {
"ids": targets,
"model_id": "model_bird_v1",
"model_version": "v1.5.0",
"model_path": "models/bird_detect/v1.5.0/model.onnx",
}, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data["published_count"], 3)
for tid in targets:
t = AiSmartTerminal.objects.get(id=tid)
self.assertEqual(t.model_version, "v1.5.0")
def test_06_publish_model_missing_params(self):
"""发布模型缺少参数应报错"""
resp = self.client.put(f"{self.base_url}/publishModel/", {
"id": self.created_ids[0],
"model_id": "m1",
}, format="json")
self.assertEqual(resp.status_code, 400)
# ==================== 4. 分页查询 ====================
def test_07_search_all_returns_paginated(self):
"""查询全部设备应返回分页数据"""
resp = self.client.get(f"{self.base_url}/list/")
self.assertEqual(resp.status_code, 200)
self.assertIn("count", resp.data)
self.assertIn("results", resp.data)
self.assertEqual(resp.data["count"], 5)
def test_08_search_by_sttp(self):
"""按终端分类过滤"""
resp = self.client.get(f"{self.base_url}/list/?sttp=02")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data["count"], 2) # ZD002 + ZD005
def test_09_search_by_usfl(self):
"""按启用状态过滤"""
resp = self.client.get(f"{self.base_url}/list/?usfl=0")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data["count"], 1) # ZD004
def test_10_search_by_stnm_fuzzy(self):
"""按终端名称模糊搜索"""
resp = self.client.get(f"{self.base_url}/list/?stnm=鱼类")
self.assertEqual(resp.status_code, 200)
self.assertGreaterEqual(resp.data["count"], 2) # ZD001 + ZD004
def test_11_pagination_page_size(self):
"""自定义分页大小"""
resp = self.client.get(f"{self.base_url}/list/?page_size=2")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data["results"]), 2)
self.assertEqual(resp.data["count"], 5)
# ==================== 5. 心跳上报 ====================
def test_12_heartbeat_updates_online_status(self):
"""心跳上报应更新在线状态和心跳时间"""
resp = self.client.put(f"{self.base_url}/heartbeat/", {
"stcd": "ZD001",
}, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data["online_status"], 1)
self.assertIsNotNone(resp.data["last_heartbeat_time"])
def test_13_heartbeat_missing_stcd(self):
"""心跳上报缺少 stcd 应报错"""
resp = self.client.put(f"{self.base_url}/heartbeat/", {}, format="json")
self.assertEqual(resp.status_code, 400)
def test_14_heartbeat_unknown_stcd(self):
"""心跳上报未知 stcd 应返回 404"""
resp = self.client.put(f"{self.base_url}/heartbeat/", {
"stcd": "UNKNOWN",
}, format="json")
self.assertEqual(resp.status_code, 404)
# ==================== 6. 下载模型 ====================
def test_15_download_model_no_model_published(self):
"""未发布模型的终端下载应返回 404"""
resp = self.client.get(f"{self.base_url}/downloadModel/?stcd=ZD005")
self.assertEqual(resp.status_code, 404)
def test_16_download_model_missing_stcd(self):
"""下载模型缺少 stcd 应报错"""
resp = self.client.get(f"{self.base_url}/downloadModel/")
self.assertEqual(resp.status_code, 400)
# ==================== 7. 逻辑删除 ====================
def test_17_delete_soft(self):
"""逻辑删除后 is_deleted 应为 1"""
target = self.created_ids[4]
resp = self.client.delete(f"{self.base_url}/delete/?id={target}")
self.assertEqual(resp.status_code, 200)
deleted = AiSmartTerminal.objects.get(id=target)
self.assertEqual(deleted.is_deleted, 1)
def test_18_delete_missing_id(self):
"""删除缺少 id 应报错"""
resp = self.client.delete(f"{self.base_url}/delete/")
self.assertEqual(resp.status_code, 400)