|
|
@@ -13,6 +13,7 @@ from app.models.duty_models import (
|
|
|
SHIFT_EVENING,
|
|
|
SHIFT_LABELS,
|
|
|
SHIFT_MORNING,
|
|
|
+ SOURCE_ASSIGN,
|
|
|
SOURCE_GRAB,
|
|
|
)
|
|
|
from app.schemas.duty import DutyAssignmentIn, DutyDaySaveBody, DutySlotSaveIn
|
|
|
@@ -375,3 +376,177 @@ def test_grab_rejects_non_duty_staff():
|
|
|
db.query.side_effect = query_side_effect
|
|
|
with pytest.raises(DutyBizError, match="值班人员"):
|
|
|
grab(db, "tok", SHIFT_EVENING, "u1")
|
|
|
+
|
|
|
+
|
|
|
+def test_save_day_grandfathers_existing_assign_without_role():
|
|
|
+ """历史指定用户无值班人员角色时,原样再保存不得因角色校验失败。"""
|
|
|
+ db = MagicMock()
|
|
|
+ day = SimpleNamespace(
|
|
|
+ id="day1",
|
|
|
+ duty_date=date(2026, 8, 20),
|
|
|
+ access_token="tok",
|
|
|
+ link_status=LINK_OPEN,
|
|
|
+ tenant_no="t1",
|
|
|
+ del_flag=0,
|
|
|
+ update_by=None,
|
|
|
+ update_time=None,
|
|
|
+ )
|
|
|
+ slot = SimpleNamespace(
|
|
|
+ id="slot1",
|
|
|
+ duty_day_id="day1",
|
|
|
+ shift_type=SHIFT_EVENING,
|
|
|
+ capacity=2,
|
|
|
+ del_flag=0,
|
|
|
+ order_num=1,
|
|
|
+ update_by=None,
|
|
|
+ update_time=None,
|
|
|
+ )
|
|
|
+ old_assign = SimpleNamespace(
|
|
|
+ id="a_old",
|
|
|
+ duty_slot_id="slot1",
|
|
|
+ source=SOURCE_ASSIGN,
|
|
|
+ user_id="u_old",
|
|
|
+ person_name="老员工",
|
|
|
+ del_flag=0,
|
|
|
+ update_by=None,
|
|
|
+ update_time=None,
|
|
|
+ )
|
|
|
+
|
|
|
+ day_q = MagicMock()
|
|
|
+ day_q.filter.return_value.first.return_value = day
|
|
|
+ slot_list_q = MagicMock()
|
|
|
+ slot_list_q.filter.return_value.all.return_value = [slot]
|
|
|
+ role = SimpleNamespace(id=DUTY_STAFF_ROLE_ID)
|
|
|
+ role_q = MagicMock()
|
|
|
+ role_q.filter.return_value.first.return_value = role
|
|
|
+ ur_q = MagicMock()
|
|
|
+ ur_q.filter.return_value.first.return_value = None # 无角色关联
|
|
|
+ user_q = MagicMock()
|
|
|
+ user_q.filter.return_value.first.return_value = SimpleNamespace(
|
|
|
+ id="u_old", tenant_no="t1", del_flag=0, status="1"
|
|
|
+ )
|
|
|
+
|
|
|
+ def query_side_effect(model):
|
|
|
+ name = getattr(model, "__name__", "")
|
|
|
+ if name == "DutyDay":
|
|
|
+ return day_q
|
|
|
+ if name == "DutySlot":
|
|
|
+ return slot_list_q
|
|
|
+ if name == "DutyAssignment":
|
|
|
+ m = MagicMock()
|
|
|
+ m.filter.return_value.count.return_value = 1
|
|
|
+ m.filter.return_value.all.return_value = [old_assign]
|
|
|
+ return m
|
|
|
+ if name == "SysRole":
|
|
|
+ return role_q
|
|
|
+ if name == "SysUserRole":
|
|
|
+ return ur_q
|
|
|
+ if name == "SysUser":
|
|
|
+ return user_q
|
|
|
+ return MagicMock()
|
|
|
+
|
|
|
+ db.query.side_effect = query_side_effect
|
|
|
+
|
|
|
+ body = DutyDaySaveBody(
|
|
|
+ dutyDate=date(2026, 8, 20),
|
|
|
+ slots=[
|
|
|
+ DutySlotSaveIn(
|
|
|
+ shiftType=SHIFT_EVENING,
|
|
|
+ capacity=2,
|
|
|
+ assignments=[
|
|
|
+ DutyAssignmentIn(source="assign", userId="u_old", personName="老员工")
|
|
|
+ ],
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ )
|
|
|
+
|
|
|
+ # 不得因「无值班人员角色」抛错;序列化侧 mock 足够跑完即可
|
|
|
+ result = save_day(db, "t1", {"user_id": "op1"}, body)
|
|
|
+ assert result is not None
|
|
|
+ db.commit.assert_called()
|
|
|
+
|
|
|
+
|
|
|
+def test_save_day_rejects_new_assign_without_role():
|
|
|
+ """新指定用户无值班人员角色时必须拒绝。"""
|
|
|
+ db = MagicMock()
|
|
|
+ day = SimpleNamespace(
|
|
|
+ id="day1",
|
|
|
+ duty_date=date(2026, 8, 20),
|
|
|
+ access_token="tok",
|
|
|
+ link_status=LINK_OPEN,
|
|
|
+ tenant_no="t1",
|
|
|
+ del_flag=0,
|
|
|
+ update_by=None,
|
|
|
+ update_time=None,
|
|
|
+ )
|
|
|
+ slot = SimpleNamespace(
|
|
|
+ id="slot1",
|
|
|
+ duty_day_id="day1",
|
|
|
+ shift_type=SHIFT_EVENING,
|
|
|
+ capacity=2,
|
|
|
+ del_flag=0,
|
|
|
+ order_num=1,
|
|
|
+ update_by=None,
|
|
|
+ update_time=None,
|
|
|
+ )
|
|
|
+ old_assign = SimpleNamespace(
|
|
|
+ id="a_old",
|
|
|
+ duty_slot_id="slot1",
|
|
|
+ source=SOURCE_ASSIGN,
|
|
|
+ user_id="u_old",
|
|
|
+ person_name="老员工",
|
|
|
+ del_flag=0,
|
|
|
+ update_by=None,
|
|
|
+ update_time=None,
|
|
|
+ )
|
|
|
+
|
|
|
+ day_q = MagicMock()
|
|
|
+ day_q.filter.return_value.first.return_value = day
|
|
|
+ slot_list_q = MagicMock()
|
|
|
+ slot_list_q.filter.return_value.all.return_value = [slot]
|
|
|
+ role = SimpleNamespace(id=DUTY_STAFF_ROLE_ID)
|
|
|
+ role_q = MagicMock()
|
|
|
+ role_q.filter.return_value.first.return_value = role
|
|
|
+ ur_q = MagicMock()
|
|
|
+ ur_q.filter.return_value.first.return_value = None
|
|
|
+ user_q = MagicMock()
|
|
|
+ user_q.filter.return_value.first.return_value = SimpleNamespace(
|
|
|
+ id="u_new", tenant_no="t1", del_flag=0, status="1"
|
|
|
+ )
|
|
|
+
|
|
|
+ def query_side_effect(model):
|
|
|
+ name = getattr(model, "__name__", "")
|
|
|
+ if name == "DutyDay":
|
|
|
+ return day_q
|
|
|
+ if name == "DutySlot":
|
|
|
+ return slot_list_q
|
|
|
+ if name == "DutyAssignment":
|
|
|
+ m = MagicMock()
|
|
|
+ m.filter.return_value.count.return_value = 1
|
|
|
+ m.filter.return_value.all.return_value = [old_assign]
|
|
|
+ return m
|
|
|
+ if name == "SysRole":
|
|
|
+ return role_q
|
|
|
+ if name == "SysUserRole":
|
|
|
+ return ur_q
|
|
|
+ if name == "SysUser":
|
|
|
+ return user_q
|
|
|
+ return MagicMock()
|
|
|
+
|
|
|
+ db.query.side_effect = query_side_effect
|
|
|
+
|
|
|
+ body = DutyDaySaveBody(
|
|
|
+ dutyDate=date(2026, 8, 20),
|
|
|
+ slots=[
|
|
|
+ DutySlotSaveIn(
|
|
|
+ shiftType=SHIFT_EVENING,
|
|
|
+ capacity=2,
|
|
|
+ assignments=[
|
|
|
+ DutyAssignmentIn(source="assign", userId="u_new", personName="新员工")
|
|
|
+ ],
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ )
|
|
|
+
|
|
|
+ with pytest.raises(DutyBizError, match="值班人员"):
|
|
|
+ save_day(db, "t1", {"user_id": "op1"}, body)
|