test_duty_missing_identity_patch.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. """Current duty save path: SQLite-only missing-identity regressions."""
  2. from datetime import date, datetime
  3. from unittest.mock import patch
  4. import pymysql
  5. import pytest
  6. from sqlalchemy import create_engine
  7. from sqlalchemy.orm import Session
  8. from app.models.duty_models import DutyAssignment, DutyDay, DutySlot
  9. from app.schemas.duty import DutyDaySaveBody
  10. from app.services.duty_schedule import DutyBizError, save_day
  11. @pytest.fixture
  12. def db(monkeypatch):
  13. def forbidden(*args, **kwargs):
  14. raise AssertionError('Business database connections forbidden')
  15. monkeypatch.setattr(pymysql, 'connect', forbidden)
  16. monkeypatch.setattr(pymysql.connections.Connection, 'connect', forbidden)
  17. engine = create_engine('sqlite://')
  18. for model in (DutyDay, DutySlot, DutyAssignment):
  19. model.__table__.create(engine)
  20. with Session(engine, autoflush=False, expire_on_commit=False) as session:
  21. session.add(DutyDay(id='day', duty_date=date(2026, 10, 27), access_token='token', tenant_no='t'))
  22. session.add(DutySlot(id='slot', duty_day_id='day', shift_type='morning', capacity=5, tenant_no='t'))
  23. session.add(DutyAssignment(id='old', duty_slot_id='slot', source='assign', user_id='u',
  24. person_name='Existing', tenant_no='t', create_by='creator', create_time=datetime(2020, 1, 1)))
  25. session.add(DutyAssignment(id='grab', duty_slot_id='slot', source='grab', user_id='g',
  26. person_name='Grab', tenant_no='t'))
  27. session.commit()
  28. with patch('app.services.duty_schedule._serialize_day', return_value={}), \
  29. patch('app.services.duty_schedule.assert_user_is_duty_staff'):
  30. yield session
  31. engine.dispose()
  32. def save(db, assignments):
  33. body = DutyDaySaveBody.model_validate({'dutyDate': '2026-10-27', 'slots': [
  34. {'shiftType': 'morning', 'capacity': 5, 'assignments': assignments}]})
  35. return save_day(db, 't', {'id': 'actor'}, body)
  36. def test_name_only_save_preserves_bound_user_assignment_identity(db):
  37. for _ in range(2):
  38. save(db, [{'personName': 'Existing'}])
  39. row = db.get(DutyAssignment, 'old')
  40. assert row.del_flag == 0 and row.user_id == 'u'
  41. assert row.create_by == 'creator' and row.create_time == datetime(2020, 1, 1)
  42. assert db.query(DutyAssignment).count() == 2
  43. assert db.get(DutyAssignment, 'grab').del_flag == 0
  44. def test_name_only_ambiguous_bound_users_are_rejected_without_replacement(db):
  45. db.add(DutyAssignment(id='same-name', duty_slot_id='slot', source='assign', user_id='another',
  46. person_name='Existing', tenant_no='t'))
  47. db.commit()
  48. with pytest.raises(DutyBizError):
  49. save(db, [{'personName': 'Existing'}])
  50. assert db.get(DutyAssignment, 'old').del_flag == 0
  51. assert db.get(DutyAssignment, 'same-name').del_flag == 0
  52. assert not db.new
  53. def test_explicit_user_id_without_child_id_keeps_current_identity(db):
  54. save(db, [{'userId': 'u', 'personName': 'Existing'}])
  55. assert db.get(DutyAssignment, 'old').del_flag == 0
  56. assert db.query(DutyAssignment).count() == 2
  57. def test_invalid_later_assignment_does_not_replace_earlier_assignment(db):
  58. with pytest.raises(DutyBizError):
  59. save(db, [{'personName': 'Existing'}, {'id': 'missing', 'personName': 'Missing'}])
  60. assert db.get(DutyAssignment, 'old').del_flag == 0
  61. assert not db.new