test_pl_cert_differential_save.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. """Certificate reconciliation regressions, using isolated SQLite only."""
  2. from datetime import date, datetime
  3. import pytest
  4. from fastapi import HTTPException
  5. from sqlalchemy import create_engine
  6. from sqlalchemy.orm import Session
  7. from app.models.pl_models import PlCert, PlDocumentReminder
  8. from app.schemas.pl.base_data import CertRow, PersonnelUpdate, VehicleUpdate
  9. from app.services.pl_cert_service import save_owner_certs
  10. @pytest.fixture
  11. def db():
  12. engine = create_engine('sqlite://')
  13. PlCert.__table__.create(engine)
  14. PlDocumentReminder.__table__.create(engine)
  15. with Session(engine) as session:
  16. yield session
  17. engine.dispose()
  18. def seed(db, id='old', **overrides):
  19. values = dict(id=id, tenant_no='t', owner_type='vehicle', owner_id='v',
  20. vehicle_part='head', cert_name='行驶证', cert_no='N1', status='revoked',
  21. create_by='creator', create_dept_id='dept', create_time=datetime(2020, 1, 1))
  22. values.update(overrides)
  23. cert = PlCert(**values)
  24. db.add(cert)
  25. db.flush()
  26. return cert
  27. def row(**overrides):
  28. values = dict(vehiclePart='head', certName='行驶证', certNo='N1')
  29. values.update(overrides)
  30. return values
  31. def save(db, rows):
  32. save_owner_certs(db, 'vehicle', 'v', rows, 't', user_id='editor', dept_id='new-dept')
  33. db.flush()
  34. def test_legacy_no_id_reuses_unique_business_key_and_preserves_status(db):
  35. cert = seed(db)
  36. save(db, [row(attachmentUrls='changed', remindDays=15)])
  37. assert db.query(PlCert).count() == 1
  38. assert cert.del_flag == 0
  39. assert cert.status == 'revoked'
  40. assert cert.create_by == 'creator'
  41. assert cert.create_time == datetime(2020, 1, 1)
  42. assert cert.attachment_urls == 'changed'
  43. assert cert.remind_days == 15
  44. @pytest.mark.parametrize('foreign', [
  45. dict(id='missing'), dict(id='foreign', tenant_no='other'),
  46. dict(id='foreign', owner_id='other'), dict(id='foreign', owner_type='personnel'),
  47. dict(id='foreign', del_flag=1),
  48. ])
  49. def test_invalid_id_rejected_before_any_certificate_mutation(db, foreign):
  50. cert = seed(db)
  51. if foreign['id'] != 'missing':
  52. seed(db, **foreign)
  53. before = db.query(PlCert).count()
  54. with pytest.raises(HTTPException) as exc:
  55. save(db, [row(certId='old', certName='must not change'), row(certId=foreign['id'])])
  56. assert exc.value.status_code == 400
  57. assert cert.cert_name == '行驶证'
  58. assert cert.del_flag == 0
  59. assert db.query(PlCert).count() == before
  60. def test_duplicate_explicit_id_is_rejected_before_mutation(db):
  61. cert = seed(db)
  62. with pytest.raises(HTTPException):
  63. save(db, [row(certId='old', certName='first'), row(certId='old', certName='second')])
  64. assert cert.cert_name == '行驶证'
  65. def test_ambiguous_legacy_key_requires_explicit_ids(db):
  66. seed(db)
  67. seed(db, 'duplicate')
  68. with pytest.raises(HTTPException):
  69. save(db, [row()])
  70. assert all(c.del_flag == 0 for c in db.query(PlCert).all())
  71. def test_duplicate_legacy_rows_rejected_before_mutation(db):
  72. cert = seed(db)
  73. with pytest.raises(HTTPException):
  74. save(db, [row(), row()])
  75. assert cert.del_flag == 0
  76. assert db.query(PlCert).count() == 1
  77. def test_explicit_update_add_and_remove_only_changes_requested_rows(db):
  78. cert = seed(db)
  79. removed = seed(db, 'removed', cert_no='N2')
  80. save(db, [row(certId='old', certNo='edited'), row(certNo='new')])
  81. assert cert.cert_no == 'edited'
  82. assert cert.status == 'revoked'
  83. assert cert.del_flag == 0
  84. assert removed.del_flag == 1
  85. new = db.query(PlCert).filter(PlCert.cert_no == 'new').one()
  86. assert new.id not in ('old', 'removed')
  87. assert new.status == 'valid'
  88. assert db.query(PlCert).count() == 3
  89. def test_empty_collection_explicitly_clears(db):
  90. cert = seed(db)
  91. save(db, [])
  92. assert cert.del_flag == 1
  93. assert db.query(PlCert).count() == 1
  94. def test_explicit_row_can_coexist_with_new_same_key_row(db):
  95. cert = seed(db)
  96. save(db, [row(), row(certId='old')])
  97. assert cert.del_flag == 0
  98. assert db.query(PlCert).filter_by(del_flag=0).count() == 2
  99. @pytest.mark.parametrize('source', ['dict', 'cert_schema', 'vehicle_schema', 'personnel_schema'])
  100. def test_submitted_certificate_row_uses_schema_defaults_for_omitted_fields(db, source):
  101. cert = seed(db, expire_date=date(2030, 1, 1), remind_days=7,
  102. attachment_urls='stored-attachment')
  103. submitted = {'certId': 'old', 'certName': '新名称'}
  104. if source == 'cert_schema':
  105. submitted = CertRow.model_validate(submitted)
  106. elif source == 'vehicle_schema':
  107. submitted = VehicleUpdate(certList=[submitted]).cert_list[0]
  108. elif source == 'personnel_schema':
  109. submitted = PersonnelUpdate(certList=[submitted]).cert_list[0]
  110. save(db, [submitted])
  111. assert cert.cert_name == '新名称'
  112. assert cert.id == 'old' and db.query(PlCert).count() == 1
  113. assert cert.cert_no is None
  114. assert cert.vehicle_part is None
  115. assert cert.expire_date is None
  116. assert cert.remind_days == 30
  117. assert cert.attachment_urls is None
  118. assert cert.status == 'revoked'
  119. def test_explicit_null_clears_nullable_certificate_fields(db):
  120. cert = seed(db, expire_date=date(2030, 1, 1), attachment_urls='stored-attachment')
  121. save(db, [dict(certId='old', certName='行驶证', certNo=None, vehiclePart=None,
  122. expireDate=None, attachmentUrls=None, remindDays=0)])
  123. assert cert.cert_no is None
  124. assert cert.vehicle_part is None
  125. assert cert.expire_date is None
  126. assert cert.attachment_urls is None
  127. assert cert.remind_days == 0
  128. def test_new_certificate_uses_schema_defaults(db):
  129. save(db, [{'certName': '新证书'}])
  130. cert = db.query(PlCert).one()
  131. assert cert.remind_days == 30
  132. assert cert.cert_no is None
  133. assert cert.expire_date is None
  134. assert cert.attachment_urls is None
  135. assert cert.status == 'valid'