# ============================================================================= # IT智能服务台 — 会议室预定功能测试 # ============================================================================= # 测试覆盖: # MeetingroomService (mock WecomService + MockRedis): # 1. get_room_list — 缓存命中/未命中 # 2. get_room_status — 预定状态查询 # 3. get_current_status — 实时状态(free/busy/starting_soon) # 4. book_room — 预定成功后缓存清除 # 5. book_room — 时间冲突异常 # 6. cancel_booking — 取消成功后缓存清除 # 7. invalidate_room_cache — 缓存清除 # 8. get_booking_detail — 预定详情缓存 # # API端点 (mock _get_meetingroom_service): # 9. GET /itportal/meetingroom/list # 10. GET /itportal/meetingroom/{id}/booking # 11. GET /itportal/meetingroom/{id}/status # 12. POST /itportal/meetingroom/book # 13. DELETE /itportal/meetingroom/booking/{id} # 14. GET /itportal/meetingroom/terminal/{sn}/binding # # 终端绑定CRUD (db_session + mock auth): # 15. POST /itportal/admin/terminal-bindings — 新增 # 16. GET /itportal/admin/terminal-bindings — 列表 # 17. PUT /itportal/admin/terminal-bindings/{id} — 更新 # 18. DELETE /itportal/admin/terminal-bindings/{id} — 删除 # # 模型: # 19. TerminalRoomBinding — 字段定义 # 20. MeetingroomBookingSnapshot — 字段定义 # ============================================================================= import json from datetime import datetime, timedelta from unittest.mock import AsyncMock, MagicMock, patch import pytest import pytest_asyncio from app.services.meetingroom_service import MeetingroomService from app.models.terminal_room_binding import TerminalRoomBinding from app.models.meetingroom_booking_snapshot import MeetingroomBookingSnapshot from app.schemas.meetingroom import ( BookRequest, TerminalBindingCreate, TerminalBindingUpdate, ) from app.dependencies import UserInfo # ============================================================================= # 测试数据工厂 # ============================================================================= def make_room(id: int = 1, name: str = "测试会议室") -> dict: """构造企微API返回的会议室数据""" return { "meetingroom_id": id, "name": name, "capacity": 10, "location": "18F东区", "equipment": [1, 2], "need_approval": 0, } def make_booking( booking_id: str = "bk001", subject: str = "项目周会", start_offset_min: int = -30, duration_min: int = 60, status: int = 0, ) -> dict: """构造企微API返回的预定记录(start_offset_min 相对当前时间的偏移分钟数)""" now = datetime.now() start = now + timedelta(minutes=start_offset_min) end = start + timedelta(minutes=duration_min) return { "booking_id": booking_id, "subject": subject, "booker": "zhangsan", "booker_name": "张三", "start_time": int(start.timestamp()), "end_time": int(end.timestamp()), "status": status, } # ============================================================================= # MockRedis — 复用 conftest 中的 MockRedis 实现 # ============================================================================= class MockRedisHelper: """轻量级 MockRedis,用于 service 层测试(独立于 conftest)""" def __init__(self): self._data: dict = {} async def get(self, key: str): v = self._data.get(key) if v is not None: return v.encode("utf-8") if isinstance(v, str) else v return None async def setex(self, name: str, time: int, value: str): self._data[name] = value async def set(self, name: str, value: str, **kwargs): self._data[name] = value async def delete(self, *names): count = 0 for n in names: if n in self._data: del self._data[n] count += 1 return count async def exists(self, *keys): return sum(1 for k in keys if k in self._data) async def expire(self, name: str, time: int): return name in self._data async def close(self): pass # ============================================================================= # MeetingroomService 单元测试 # ============================================================================= class TestMeetingroomService: """MeetingroomService 业务逻辑测试""" @pytest.fixture def mock_wecom(self): """创建 mock WecomService""" wecom = AsyncMock() wecom.get_meetingroom_list.return_value = [make_room(1, "A会议室"), make_room(2, "B会议室")] wecom.get_booking_info.return_value = [] wecom.book_meetingroom.return_value = {"booking_id": "bk_new_001"} wecom.cancel_booking.return_value = {"errcode": 0} wecom.get_booking_detail.return_value = { "booking_id": "bk001", "subject": "详情测试", "booker": "zhangsan", } return wecom @pytest.fixture def mock_redis(self): return MockRedisHelper() @pytest.fixture def service(self, mock_wecom, mock_redis): return MeetingroomService(wecom_service=mock_wecom, redis_client=mock_redis) # -------------------------------------------------------------------------- # get_room_list # -------------------------------------------------------------------------- @pytest.mark.asyncio async def test_get_room_list_with_cache(self, service, mock_wecom, mock_redis): """缓存命中时不调用企微API""" # 第一次调用 — 应调用API并缓存 result1 = await service.get_room_list() assert len(result1) == 2 assert mock_wecom.get_meetingroom_list.call_count == 1 # 第二次调用 — 应命中缓存,不调用API result2 = await service.get_room_list() assert len(result2) == 2 assert mock_wecom.get_meetingroom_list.call_count == 1 # 仍是1次 @pytest.mark.asyncio async def test_get_room_list_no_redis(self, mock_wecom): """无Redis时直接调用API""" service = MeetingroomService(wecom_service=mock_wecom, redis_client=None) result = await service.get_room_list() assert len(result) == 2 assert mock_wecom.get_meetingroom_list.call_count == 1 # -------------------------------------------------------------------------- # get_room_status # -------------------------------------------------------------------------- @pytest.mark.asyncio async def test_get_room_status(self, service, mock_wecom): """验证预定状态查询""" mock_wecom.get_booking_info.return_value = [ make_booking("bk001", "会议A", start_offset_min=60), make_booking("bk002", "会议B", start_offset_min=120), ] result = await service.get_room_status(meetingroom_id=1, date="2026-07-15") assert len(result) == 2 assert result[0]["booking_id"] == "bk001" assert mock_wecom.get_booking_info.call_count == 1 # 验证传给企微API的时间范围参数 call_args = mock_wecom.get_booking_info.call_args assert call_args[0][0] == 1 # meetingroom_id assert "2026-07-15T00:00:00" in call_args[0][1] # start_time assert "2026-07-15T23:59:59" in call_args[0][2] # end_time @pytest.mark.asyncio async def test_get_room_status_default_date(self, service, mock_wecom): """未传date时默认今天""" await service.get_room_status(meetingroom_id=1) today = datetime.now().strftime("%Y-%m-%d") call_args = mock_wecom.get_booking_info.call_args assert today in call_args[0][1] # -------------------------------------------------------------------------- # get_current_status # -------------------------------------------------------------------------- @pytest.mark.asyncio async def test_get_current_status_free(self, service, mock_wecom): """当前空闲状态判断""" # 预定在未来2小时,不在15分钟内 → free mock_wecom.get_booking_info.return_value = [ make_booking("bk001", "未来会议", start_offset_min=120, duration_min=60), ] result = await service.get_current_status(meetingroom_id=1) assert result["status"] == "free" assert result["current_meeting"] is None assert result["next_meeting"] is not None assert result["minutes_to_next"] is not None assert result["minutes_to_next"] > 15 @pytest.mark.asyncio async def test_get_current_status_busy(self, service, mock_wecom): """当前使用中状态判断""" # 预定在当前时间段内 → busy mock_wecom.get_booking_info.return_value = [ make_booking("bk001", "进行中会议", start_offset_min=-10, duration_min=30), ] result = await service.get_current_status(meetingroom_id=1) assert result["status"] == "busy" assert result["current_meeting"] is not None assert result["current_meeting"]["subject"] == "进行中会议" @pytest.mark.asyncio async def test_get_current_status_starting_soon(self, service, mock_wecom): """即将开始状态判断(15分钟内)""" # 预定在5分钟后开始 → starting_soon mock_wecom.get_booking_info.return_value = [ make_booking("bk001", "即将开始", start_offset_min=5, duration_min=30), ] result = await service.get_current_status(meetingroom_id=1) assert result["status"] == "starting_soon" assert result["next_meeting"] is not None assert result["minutes_to_next"] <= 15 @pytest.mark.asyncio async def test_get_current_status_no_bookings(self, service, mock_wecom): """无预定时返回free""" mock_wecom.get_booking_info.return_value = [] result = await service.get_current_status(meetingroom_id=1) assert result["status"] == "free" assert result["current_meeting"] is None assert result["next_meeting"] is None assert result["minutes_to_next"] is None @pytest.mark.asyncio async def test_get_current_status_skip_cancelled(self, service, mock_wecom): """跳过已取消的预定""" mock_wecom.get_booking_info.return_value = [ make_booking("bk001", "已取消", start_offset_min=-10, duration_min=30, status=1), ] result = await service.get_current_status(meetingroom_id=1) assert result["status"] == "free" # -------------------------------------------------------------------------- # book_room # -------------------------------------------------------------------------- @pytest.mark.asyncio async def test_book_room_success(self, service, mock_wecom, mock_redis): """预定成功后缓存被清除""" # 先写入缓存 cache_key = service.CACHE_KEY_BOOKING_INFO.format(room_id=1, date=datetime.now().strftime("%Y-%m-%d")) await mock_redis.setex(cache_key, 30, json.dumps([{"old": True}])) # 执行预定 result = await service.book_room( meetingroom_id=1, subject="测试预定", start_time="2026-07-15T10:00:00+08:00", end_time="2026-07-15T11:00:00+08:00", booker="zhangsan", ) assert result["booking_id"] == "bk_new_001" assert mock_wecom.book_meetingroom.call_count == 1 # 验证缓存被清除 cached = await mock_redis.get(cache_key) assert cached is None @pytest.mark.asyncio async def test_book_room_conflict(self, service, mock_wecom): """时间冲突处理 — 企微API抛异常时透传""" mock_wecom.book_meetingroom.side_effect = Exception("时间冲突: 该时段已被预定") with pytest.raises(Exception, match="时间冲突"): await service.book_room( meetingroom_id=1, subject="冲突预定", start_time="2026-07-15T10:00:00+08:00", end_time="2026-07-15T11:00:00+08:00", booker="zhangsan", ) # -------------------------------------------------------------------------- # cancel_booking # -------------------------------------------------------------------------- @pytest.mark.asyncio async def test_cancel_booking_success(self, service, mock_wecom, mock_redis): """取消成功后缓存被清除""" # 先写入状态缓存 status_key = service.CACHE_KEY_STATUS.format(room_id=1) await mock_redis.setex(status_key, 10, json.dumps({"status": "busy"})) # 执行取消 result = await service.cancel_booking(booking_id="bk001", meetingroom_id=1) assert result["errcode"] == 0 assert mock_wecom.cancel_booking.call_count == 1 # 验证缓存被清除 cached = await mock_redis.get(status_key) assert cached is None # -------------------------------------------------------------------------- # invalidate_room_cache # -------------------------------------------------------------------------- @pytest.mark.asyncio async def test_invalidate_room_cache(self, service, mock_redis): """缓存清除""" today = datetime.now().strftime("%Y-%m-%d") booking_key = service.CACHE_KEY_BOOKING_INFO.format(room_id=1, date=today) status_key = service.CACHE_KEY_STATUS.format(room_id=1) # 写入缓存 await mock_redis.setex(booking_key, 30, json.dumps([{"test": True}])) await mock_redis.setex(status_key, 10, json.dumps({"status": "busy"})) # 清除缓存 await service.invalidate_room_cache(meetingroom_id=1) # 验证两个缓存key都被删除 assert await mock_redis.get(booking_key) is None assert await mock_redis.get(status_key) is None @pytest.mark.asyncio async def test_invalidate_room_cache_no_redis(self, mock_wecom): """无Redis时invalidate不报错""" service = MeetingroomService(wecom_service=mock_wecom, redis_client=None) await service.invalidate_room_cache(meetingroom_id=1) # 不应抛异常 # -------------------------------------------------------------------------- # get_booking_detail # -------------------------------------------------------------------------- @pytest.mark.asyncio async def test_get_booking_detail_with_cache(self, service, mock_wecom, mock_redis): """预定详情缓存""" result1 = await service.get_booking_detail(meetingroom_id=1, booking_id="bk001") assert result1["booking_id"] == "bk001" assert mock_wecom.get_booking_detail.call_count == 1 # 第二次应命中缓存 result2 = await service.get_booking_detail(meetingroom_id=1, booking_id="bk001") assert result2["booking_id"] == "bk001" assert mock_wecom.get_booking_detail.call_count == 1 # 仍1次 # -------------------------------------------------------------------------- # _parse_booking_time # -------------------------------------------------------------------------- def test_parse_booking_time_timestamp(self): """解析时间戳格式""" ts = int(datetime(2026, 7, 15, 10, 0, 0).timestamp()) result = MeetingroomService._parse_booking_time(ts) assert result is not None assert result.year == 2026 assert result.month == 7 assert result.day == 15 def test_parse_booking_time_iso(self): """解析ISO字符串格式""" result = MeetingroomService._parse_booking_time("2026-07-15T10:00:00") assert result is not None assert result.year == 2026 def test_parse_booking_time_none(self): """None输入返回None""" assert MeetingroomService._parse_booking_time(None) is None def test_parse_booking_time_invalid(self): """无效输入返回None""" assert MeetingroomService._parse_booking_time("invalid") is None # ============================================================================= # API 端点测试 — 会议室预定 # ============================================================================= class TestMeetingroomAPI: """会议室预定API端点测试""" @pytest.fixture def mock_service(self): """创建 mock MeetingroomService""" svc = AsyncMock(spec=MeetingroomService) svc.get_room_list.return_value = [make_room(1, "A会议室"), make_room(2, "B会议室")] svc.get_room_status.return_value = [ make_booking("bk001", "会议A", start_offset_min=60), ] svc.get_current_status.return_value = { "status": "free", "current_meeting": None, "next_meeting": None, "minutes_to_next": None, "bookings": [], } svc.book_room.return_value = {"booking_id": "bk_new_001"} svc.cancel_booking.return_value = {"errcode": 0} svc.get_booking_detail.return_value = { "booking_id": "bk001", "subject": "详情", "booker": "zhangsan", "booker_name": "张三", "attendees": ["lisi"], "start_time": "2026-07-15T10:00:00", "end_time": "2026-07-15T11:00:00", } svc._format_booking = MeetingroomService._format_booking return svc @pytest_asyncio.fixture async def meetingroom_client(self, mock_service, db_session): """创建测试客户端,mock掉 _get_meetingroom_service。 使用独立 FastAPI app(只挂载 meetingroom 路由), 避免全量 create_app() 触发 itsm_service.py 中 httpx.Timeout 兼容性问题。 """ from httpx import ASGITransport, AsyncClient from fastapi import FastAPI from app.database import get_db from app.utils.response import AppException, app_exception_handler app = FastAPI() # 注册全局异常处理器(让 AppException 返回 JSON 而非 500) app.add_exception_handler(AppException, app_exception_handler) # 只挂载会议室预定路由 from app.api.meetingroom import router as meetingroom_router app.include_router(meetingroom_router) async def _override_db(): yield db_session app.dependency_overrides[get_db] = _override_db with patch("app.api.meetingroom._get_meetingroom_service", return_value=mock_service): with patch("app.api.meetingroom.WecomService", return_value=AsyncMock()): with patch("app.services.wecom_service.WecomService", return_value=AsyncMock()): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac app.dependency_overrides.clear() @pytest.mark.asyncio async def test_get_meetingroom_list(self, meetingroom_client, mock_service): """GET /itportal/meetingroom/list""" resp = await meetingroom_client.get("/itportal/meetingroom/list") assert resp.status_code == 200 data = resp.json() assert data["code"] == 0 assert len(data["data"]["rooms"]) == 2 assert data["data"]["rooms"][0]["name"] == "A会议室" mock_service.get_room_list.assert_called_once() @pytest.mark.asyncio async def test_get_booking_info(self, meetingroom_client, mock_service): """GET /itportal/meetingroom/{id}/booking""" resp = await meetingroom_client.get("/itportal/meetingroom/1/booking") assert resp.status_code == 200 data = resp.json() assert data["code"] == 0 assert len(data["data"]["bookings"]) == 1 @pytest.mark.asyncio async def test_get_current_status(self, meetingroom_client, mock_service): """GET /itportal/meetingroom/{id}/status""" resp = await meetingroom_client.get("/itportal/meetingroom/1/status") assert resp.status_code == 200 data = resp.json() assert data["code"] == 0 assert data["data"]["status"] == "free" @pytest.mark.asyncio async def test_get_terminal_binding_not_found(self, meetingroom_client): """GET /itportal/meetingroom/terminal/{sn}/binding — 未绑定时返回null""" resp = await meetingroom_client.get("/itportal/meetingroom/terminal/UNKNOWN_SN/binding") assert resp.status_code == 200 data = resp.json() assert data["code"] == 0 assert data["data"] is None # ============================================================================= # API 端点测试 — 终端绑定CRUD # ============================================================================= class TestTerminalBindingCRUD: """终端绑定管理CRUD测试""" @pytest_asyncio.fixture async def admin_client(self, db_session): """创建带admin权限的测试客户端。 使用独立 FastAPI app(只挂载 terminal_binding 路由), 避免全量 create_app() 触发 itsm_service.py 中 httpx.Timeout 兼容性问题。 """ from httpx import ASGITransport, AsyncClient from fastapi import FastAPI from app.database import get_db from app.dependencies import get_current_user from app.utils.response import AppException, app_exception_handler app = FastAPI() # 注册全局异常处理器(让 AppException 返回 JSON 而非 500) app.add_exception_handler(AppException, app_exception_handler) from app.api.admin.terminal_binding import router as terminal_binding_router app.include_router(terminal_binding_router) async def _override_db(): yield db_session admin_user = UserInfo( employee_id="admin001", name="管理员", department="IT部", avatar="", roles=["admin"], current_role="admin", login_source="test", ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_current_user] = lambda: admin_user transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac app.dependency_overrides.clear() @pytest.mark.asyncio async def test_create_binding(self, admin_client, db_session): """POST /itportal/admin/terminal-bindings — 新增绑定""" resp = await admin_client.post("/itportal/admin/terminal-bindings", json={ "terminal_sn": "SN_TEST_001", "terminal_name": "18F东区大屏", "meetingroom_id": 1, "meetingroom_name": "18F东区会议室", "location": "18F东区", }) assert resp.status_code == 200 data = resp.json() assert data["code"] == 0 assert "id" in data["data"] binding_id = data["data"]["id"] # 验证数据已写入 from sqlalchemy import select result = await db_session.execute( select(TerminalRoomBinding).where(TerminalRoomBinding.id == binding_id) ) binding = result.scalar_one_or_none() assert binding is not None assert binding.terminal_sn == "SN_TEST_001" assert binding.meetingroom_id == 1 assert binding.is_active is True @pytest.mark.asyncio async def test_create_binding_duplicate(self, admin_client, db_session): """重复绑定同一terminal_sn应报错""" # 先创建一条 db_session.add(TerminalRoomBinding( terminal_sn="SN_DUP_001", terminal_name="终端1", meetingroom_id=1, meetingroom_name="会议室A", location="18F", is_active=True, )) await db_session.flush() # 再创建同SN的 resp = await admin_client.post("/itportal/admin/terminal-bindings", json={ "terminal_sn": "SN_DUP_001", "terminal_name": "终端2", "meetingroom_id": 2, "meetingroom_name": "会议室B", "location": "19F", }) assert resp.status_code == 200 data = resp.json() assert data["code"] != 0 # 应返回错误 @pytest.mark.asyncio async def test_list_bindings(self, admin_client, db_session): """GET /itportal/admin/terminal-bindings — 列表查询""" # 插入测试数据 for i in range(3): db_session.add(TerminalRoomBinding( terminal_sn=f"SN_LIST_{i:03d}", terminal_name=f"终端{i}", meetingroom_id=i + 1, meetingroom_name=f"会议室{i+1}", location=f"{i+10}F", is_active=True, )) await db_session.flush() resp = await admin_client.get("/itportal/admin/terminal-bindings") assert resp.status_code == 200 data = resp.json() assert data["code"] == 0 assert data["data"]["total"] >= 3 assert len(data["data"]["list"]) >= 3 @pytest.mark.asyncio async def test_list_bindings_with_keyword(self, admin_client, db_session): """GET /itportal/admin/terminal-bindings?keyword=xxx — 关键词搜索""" db_session.add(TerminalRoomBinding( terminal_sn="SN_KW_001", terminal_name="大屏A", meetingroom_id=1, meetingroom_name="会议室A", location="18F", is_active=True, )) db_session.add(TerminalRoomBinding( terminal_sn="SN_KW_002", terminal_name="大屏B", meetingroom_id=2, meetingroom_name="会议室B", location="19F", is_active=True, )) await db_session.flush() resp = await admin_client.get("/itportal/admin/terminal-bindings?keyword=大屏A") assert resp.status_code == 200 data = resp.json() assert data["code"] == 0 # 至少包含"大屏A"的记录 names = [item["terminal_name"] for item in data["data"]["list"]] assert "大屏A" in names @pytest.mark.asyncio async def test_update_binding(self, admin_client, db_session): """PUT /itportal/admin/terminal-bindings/{id} — 更新绑定""" binding = TerminalRoomBinding( terminal_sn="SN_UPD_001", terminal_name="旧名称", meetingroom_id=1, meetingroom_name="旧会议室", location="18F", is_active=True, ) db_session.add(binding) await db_session.flush() resp = await admin_client.put(f"/itportal/admin/terminal-bindings/{binding.id}", json={ "terminal_name": "新名称", "meetingroom_name": "新会议室", "is_active": False, }) assert resp.status_code == 200 data = resp.json() assert data["code"] == 0 # 验证更新生效(flush 确保修改写入 session,再检查属性) await db_session.flush() assert binding.terminal_name == "新名称" assert binding.meetingroom_name == "新会议室" assert binding.is_active is False @pytest.mark.asyncio async def test_update_binding_not_found(self, admin_client): """更新不存在的绑定应报错""" resp = await admin_client.put("/itportal/admin/terminal-bindings/99999", json={ "terminal_name": "测试", }) assert resp.status_code == 200 data = resp.json() assert data["code"] != 0 @pytest.mark.asyncio async def test_delete_binding(self, admin_client, db_session): """DELETE /itportal/admin/terminal-bindings/{id} — 删除绑定""" binding = TerminalRoomBinding( terminal_sn="SN_DEL_001", terminal_name="待删除", meetingroom_id=1, meetingroom_name="会议室", location="18F", is_active=True, ) db_session.add(binding) await db_session.flush() binding_id = binding.id resp = await admin_client.delete(f"/itportal/admin/terminal-bindings/{binding_id}") assert resp.status_code == 200 data = resp.json() assert data["code"] == 0 @pytest.mark.asyncio async def test_delete_binding_not_found(self, admin_client): """删除不存在的绑定应报错""" resp = await admin_client.delete("/itportal/admin/terminal-bindings/99999") assert resp.status_code == 200 data = resp.json() assert data["code"] != 0 # ============================================================================= # 模型测试 # ============================================================================= class TestModels: """模型字段定义测试""" def test_terminal_room_binding_model(self): """TerminalRoomBinding 模型字段定义正确""" # 验证表名 assert TerminalRoomBinding.__tablename__ == "terminal_room_binding" # 验证字段存在 columns = {c.name for c in TerminalRoomBinding.__table__.columns} expected = { "id", "terminal_sn", "terminal_name", "meetingroom_id", "meetingroom_name", "location", "is_active", "created_at", "updated_at", } assert expected.issubset(columns), f"缺失字段: {expected - columns}" # 验证 terminal_sn 唯一索引 sn_col = TerminalRoomBinding.__table__.columns["terminal_sn"] assert sn_col.unique is True # 验证 meetingroom_id 有索引(非唯一) room_col = TerminalRoomBinding.__table__.columns["meetingroom_id"] assert room_col.index is True # 验证默认值 assert TerminalRoomBinding.__table__.columns["is_active"].default.arg is True def test_meetingroom_booking_snapshot_model(self): """MeetingroomBookingSnapshot 模型字段定义正确""" # 验证表名 assert MeetingroomBookingSnapshot.__tablename__ == "meetingroom_booking_snapshot" # 验证字段存在 columns = {c.name for c in MeetingroomBookingSnapshot.__table__.columns} expected = { "id", "meetingroom_id", "booking_id", "subject", "booker", "start_time", "end_time", "status", "snapshot_date", "created_at", } assert expected.issubset(columns), f"缺失字段: {expected - columns}" # 验证索引字段 assert MeetingroomBookingSnapshot.__table__.columns["meetingroom_id"].index is True assert MeetingroomBookingSnapshot.__table__.columns["booking_id"].index is True assert MeetingroomBookingSnapshot.__table__.columns["snapshot_date"].index is True # 验证默认值 assert MeetingroomBookingSnapshot.__table__.columns["status"].default.arg == 0 @pytest.mark.asyncio async def test_terminal_room_binding_repr(self): """TerminalRoomBinding __repr__ 方法""" binding = TerminalRoomBinding( terminal_sn="SN_REPR_001", terminal_name="测试终端", meetingroom_id=1, meetingroom_name="测试会议室", ) repr_str = repr(binding) assert "SN_REPR_001" in repr_str assert "测试会议室" in repr_str @pytest.mark.asyncio async def test_meetingroom_booking_snapshot_repr(self): """MeetingroomBookingSnapshot __repr__ 方法""" from datetime import date snapshot = MeetingroomBookingSnapshot( meetingroom_id=1, booking_id="bk_repr_001", subject="测试快照", start_time=datetime(2026, 7, 15, 10, 0, 0), end_time=datetime(2026, 7, 15, 11, 0, 0), snapshot_date=date(2026, 7, 15), ) repr_str = repr(snapshot) assert "bk_repr_001" in repr_str assert "测试快照" in repr_str # ============================================================================= # Pydantic Schema 测试 # ============================================================================= class TestSchemas: """Pydantic 请求/响应模型测试""" def test_book_request_valid(self): """BookRequest 验证合法请求""" req = BookRequest( meetingroom_id=1, subject="测试预定", start_time="2026-07-15T10:00:00+08:00", end_time="2026-07-15T11:00:00+08:00", booker="zhangsan", ) assert req.meetingroom_id == 1 assert req.subject == "测试预定" def test_book_request_empty_subject(self): """BookRequest 空主题应验证失败""" with pytest.raises(Exception): BookRequest( meetingroom_id=1, subject="", start_time="2026-07-15T10:00:00+08:00", end_time="2026-07-15T11:00:00+08:00", booker="zhangsan", ) def test_terminal_binding_create_valid(self): """TerminalBindingCreate 验证""" req = TerminalBindingCreate( terminal_sn="SN001", terminal_name="终端1", meetingroom_id=1, meetingroom_name="会议室1", location="18F", ) assert req.terminal_sn == "SN001" def test_terminal_binding_create_empty_sn(self): """空terminal_sn应验证失败""" with pytest.raises(Exception): TerminalBindingCreate( terminal_sn="", meetingroom_id=1, ) def test_terminal_binding_update_partial(self): """TerminalBindingUpdate 部分更新""" req = TerminalBindingUpdate(terminal_name="新名称") assert req.terminal_name == "新名称" assert req.meetingroom_id is None assert req.is_active is None