test_crm_product_metadata_diff.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. """Product metadata saves use stable identities; SQLite only."""
  2. from datetime import datetime
  3. import pytest
  4. from sqlalchemy import create_engine
  5. from sqlalchemy.orm import Session
  6. from app.crud.crm import product as crud
  7. from app.models.crm_models import CrmProduct, CrmProductAlias, CrmProductBrand
  8. from app.models.pl_models import PlInventory, PlPlan, PlPlanProduct
  9. @pytest.fixture
  10. def db():
  11. engine = create_engine('sqlite://')
  12. for model in (CrmProduct, CrmProductAlias, CrmProductBrand, PlInventory, PlPlan, PlPlanProduct):
  13. model.__table__.create(engine)
  14. with Session(engine) as session:
  15. yield session
  16. def seed(db, kind, identity='old', name='Original', **kwargs):
  17. model = CrmProductBrand if kind == 'brands' else CrmProductAlias
  18. attr = 'brand_name' if kind == 'brands' else 'alias_name'
  19. values = dict(id=identity, tenant_no='t', product_id='p', del_flag=0,
  20. create_by='creator', create_time=datetime(2020, 1, 1), **{attr: name})
  21. values.update(kwargs)
  22. row = model(**values)
  23. db.add(row)
  24. db.flush()
  25. return row
  26. def save(db, kind, values):
  27. getattr(crud, '_replace_product_' + kind)(db, product_id='p', tenant_no='t',
  28. user_id='editor', **{kind: values})
  29. db.flush()
  30. def item(kind, name='Original', **kwargs):
  31. return {'brandName' if kind == 'brands' else 'aliasName': name, **kwargs}
  32. @pytest.mark.parametrize('kind', ['brands', 'aliases'])
  33. def test_legacy_unique_name_reuses_identity(db, kind):
  34. old = seed(db, kind)
  35. save(db, kind, [item(kind)])
  36. assert old.del_flag == 0
  37. assert db.query(type(old)).count() == 1
  38. assert old.create_by == 'creator' and old.create_time == datetime(2020, 1, 1)
  39. @pytest.mark.parametrize('kind', ['brands', 'aliases'])
  40. @pytest.mark.parametrize('bad', ['missing', 'foreign_tenant', 'foreign_product', 'deleted', 'duplicate'])
  41. def test_bad_ids_rejected_before_mutation(db, kind, bad):
  42. old = seed(db, kind)
  43. foreign = {'foreign_tenant': {'tenant_no': 'other'}, 'foreign_product': {'product_id': 'other'},
  44. 'deleted': {'del_flag': 1}}
  45. if bad in foreign:
  46. seed(db, kind, 'bad', **foreign[bad])
  47. values = [item(kind, 'Changed', id='old'), item(kind, 'Other', id='old' if bad == 'duplicate' else 'bad')]
  48. with pytest.raises(ValueError):
  49. save(db, kind, values)
  50. assert old.del_flag == 0 and not db.dirty and not db.new
  51. @pytest.mark.parametrize('kind', ['brands', 'aliases'])
  52. def test_ambiguous_legacy_name_is_rejected(db, kind):
  53. old = seed(db, kind)
  54. seed(db, kind, 'second')
  55. with pytest.raises(ValueError):
  56. save(db, kind, [item(kind)])
  57. assert old.del_flag == 0 and not db.dirty
  58. @pytest.mark.parametrize('kind', ['brands', 'aliases'])
  59. def test_edit_add_remove_and_omission(db, kind):
  60. old = seed(db, kind)
  61. removed = seed(db, kind, 'removed', name='Remove')
  62. save(db, kind, None)
  63. assert old.del_flag == removed.del_flag == 0
  64. save(db, kind, [item(kind, 'Changed', id='old'), item(kind, 'New')])
  65. assert old.del_flag == 0 and removed.del_flag == 1
  66. rows = db.query(type(old)).filter_by(del_flag=0).all()
  67. assert len(rows) == 2 and len({row.id for row in rows}) == 2
  68. save(db, kind, [])
  69. assert db.query(type(old)).filter_by(del_flag=0).count() == 0
  70. def test_validate_both_collections_before_header_or_other_collection_mutation(db):
  71. parent = CrmProduct(id='p', tenant_no='t', product_no='P', product_name='Original', del_flag=0)
  72. db.add(parent)
  73. brand = seed(db, 'brands')
  74. seed(db, 'aliases', 'alias')
  75. with pytest.raises(ValueError):
  76. crud.update_product(db, 'p', product_name='Changed', brands=[],
  77. aliases=[item('aliases', id='foreign')])
  78. assert parent.product_name == 'Original' and brand.del_flag == 0
  79. assert not db.dirty and not db.new
  80. def test_existing_brand_omitted_status_preserves_it(db):
  81. old = seed(db, 'brands', status='off')
  82. save(db, 'brands', [item('brands', 'Renamed', id='old')])
  83. assert old.status == 'off'
  84. def test_brand_removal_guard_runs_before_any_mutation(db):
  85. from fastapi import HTTPException
  86. old = seed(db, 'brands')
  87. guarded = seed(db, 'brands', 'guarded', name='Guarded')
  88. db.add(PlInventory(id='inventory', tenant_no='t', product_id='p', brand_id='guarded',
  89. product_no='P', product_name='Product', stock_qty=1, del_flag=0))
  90. db.flush()
  91. with pytest.raises(HTTPException):
  92. save(db, 'brands', [])
  93. assert old.del_flag == guarded.del_flag == 0 and not db.dirty
  94. @pytest.mark.parametrize('kind', ['brands', 'aliases'])
  95. def test_create_rejects_source_ids_before_adding_parent(db, kind):
  96. with pytest.raises(ValueError):
  97. crud.create_product(db, tenant_no='t', product_no='P', product_name='New', barcode='123',
  98. **{kind: [item(kind, id='source')]})
  99. assert db.query(CrmProduct).count() == 0 and not db.new
  100. @pytest.mark.parametrize('kind', ['brands', 'aliases'])
  101. def test_explicit_same_name_and_new_row_have_distinct_identity(db, kind):
  102. old = seed(db, kind)
  103. save(db, kind, [item(kind, id='old'), item(kind)])
  104. assert old.del_flag == 0
  105. assert db.query(type(old)).filter_by(del_flag=0).count() == 2
  106. @pytest.mark.parametrize('kind', ['brands', 'aliases'])
  107. def test_repeated_anonymous_names_rejected_before_mutation(db, kind):
  108. old = seed(db, kind)
  109. with pytest.raises(ValueError):
  110. save(db, kind, [item(kind), item(kind)])
  111. assert old.del_flag == 0 and not db.dirty
  112. def test_omitted_product_images_stay_and_empty_string_clears(db):
  113. parent = CrmProduct(
  114. id='p', tenant_no='t', product_no='P', product_name='甲醇',
  115. images='https://a/1.jpg,https://a/2.jpg', del_flag=0,
  116. )
  117. db.add(parent)
  118. db.commit()
  119. crud.update_product(db, 'p', remark='只改备注')
  120. assert parent.images == 'https://a/1.jpg,https://a/2.jpg'
  121. crud.update_product(db, 'p', images='')
  122. assert parent.images == ''