| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- """Product metadata saves use stable identities; SQLite only."""
- from datetime import datetime
- import pytest
- from sqlalchemy import create_engine
- from sqlalchemy.orm import Session
- from app.crud.crm import product as crud
- from app.models.crm_models import CrmProduct, CrmProductAlias, CrmProductBrand
- from app.models.pl_models import PlInventory, PlPlan, PlPlanProduct
- @pytest.fixture
- def db():
- engine = create_engine('sqlite://')
- for model in (CrmProduct, CrmProductAlias, CrmProductBrand, PlInventory, PlPlan, PlPlanProduct):
- model.__table__.create(engine)
- with Session(engine) as session:
- yield session
- def seed(db, kind, identity='old', name='Original', **kwargs):
- model = CrmProductBrand if kind == 'brands' else CrmProductAlias
- attr = 'brand_name' if kind == 'brands' else 'alias_name'
- values = dict(id=identity, tenant_no='t', product_id='p', del_flag=0,
- create_by='creator', create_time=datetime(2020, 1, 1), **{attr: name})
- values.update(kwargs)
- row = model(**values)
- db.add(row)
- db.flush()
- return row
- def save(db, kind, values):
- getattr(crud, '_replace_product_' + kind)(db, product_id='p', tenant_no='t',
- user_id='editor', **{kind: values})
- db.flush()
- def item(kind, name='Original', **kwargs):
- return {'brandName' if kind == 'brands' else 'aliasName': name, **kwargs}
- @pytest.mark.parametrize('kind', ['brands', 'aliases'])
- def test_legacy_unique_name_reuses_identity(db, kind):
- old = seed(db, kind)
- save(db, kind, [item(kind)])
- assert old.del_flag == 0
- assert db.query(type(old)).count() == 1
- assert old.create_by == 'creator' and old.create_time == datetime(2020, 1, 1)
- @pytest.mark.parametrize('kind', ['brands', 'aliases'])
- @pytest.mark.parametrize('bad', ['missing', 'foreign_tenant', 'foreign_product', 'deleted', 'duplicate'])
- def test_bad_ids_rejected_before_mutation(db, kind, bad):
- old = seed(db, kind)
- foreign = {'foreign_tenant': {'tenant_no': 'other'}, 'foreign_product': {'product_id': 'other'},
- 'deleted': {'del_flag': 1}}
- if bad in foreign:
- seed(db, kind, 'bad', **foreign[bad])
- values = [item(kind, 'Changed', id='old'), item(kind, 'Other', id='old' if bad == 'duplicate' else 'bad')]
- with pytest.raises(ValueError):
- save(db, kind, values)
- assert old.del_flag == 0 and not db.dirty and not db.new
- @pytest.mark.parametrize('kind', ['brands', 'aliases'])
- def test_ambiguous_legacy_name_is_rejected(db, kind):
- old = seed(db, kind)
- seed(db, kind, 'second')
- with pytest.raises(ValueError):
- save(db, kind, [item(kind)])
- assert old.del_flag == 0 and not db.dirty
- @pytest.mark.parametrize('kind', ['brands', 'aliases'])
- def test_edit_add_remove_and_omission(db, kind):
- old = seed(db, kind)
- removed = seed(db, kind, 'removed', name='Remove')
- save(db, kind, None)
- assert old.del_flag == removed.del_flag == 0
- save(db, kind, [item(kind, 'Changed', id='old'), item(kind, 'New')])
- assert old.del_flag == 0 and removed.del_flag == 1
- rows = db.query(type(old)).filter_by(del_flag=0).all()
- assert len(rows) == 2 and len({row.id for row in rows}) == 2
- save(db, kind, [])
- assert db.query(type(old)).filter_by(del_flag=0).count() == 0
- def test_validate_both_collections_before_header_or_other_collection_mutation(db):
- parent = CrmProduct(id='p', tenant_no='t', product_no='P', product_name='Original', del_flag=0)
- db.add(parent)
- brand = seed(db, 'brands')
- seed(db, 'aliases', 'alias')
- with pytest.raises(ValueError):
- crud.update_product(db, 'p', product_name='Changed', brands=[],
- aliases=[item('aliases', id='foreign')])
- assert parent.product_name == 'Original' and brand.del_flag == 0
- assert not db.dirty and not db.new
- def test_existing_brand_omitted_status_preserves_it(db):
- old = seed(db, 'brands', status='off')
- save(db, 'brands', [item('brands', 'Renamed', id='old')])
- assert old.status == 'off'
- def test_brand_removal_guard_runs_before_any_mutation(db):
- from fastapi import HTTPException
- old = seed(db, 'brands')
- guarded = seed(db, 'brands', 'guarded', name='Guarded')
- db.add(PlInventory(id='inventory', tenant_no='t', product_id='p', brand_id='guarded',
- product_no='P', product_name='Product', stock_qty=1, del_flag=0))
- db.flush()
- with pytest.raises(HTTPException):
- save(db, 'brands', [])
- assert old.del_flag == guarded.del_flag == 0 and not db.dirty
- @pytest.mark.parametrize('kind', ['brands', 'aliases'])
- def test_create_rejects_source_ids_before_adding_parent(db, kind):
- with pytest.raises(ValueError):
- crud.create_product(db, tenant_no='t', product_no='P', product_name='New', barcode='123',
- **{kind: [item(kind, id='source')]})
- assert db.query(CrmProduct).count() == 0 and not db.new
- @pytest.mark.parametrize('kind', ['brands', 'aliases'])
- def test_explicit_same_name_and_new_row_have_distinct_identity(db, kind):
- old = seed(db, kind)
- save(db, kind, [item(kind, id='old'), item(kind)])
- assert old.del_flag == 0
- assert db.query(type(old)).filter_by(del_flag=0).count() == 2
- @pytest.mark.parametrize('kind', ['brands', 'aliases'])
- def test_repeated_anonymous_names_rejected_before_mutation(db, kind):
- old = seed(db, kind)
- with pytest.raises(ValueError):
- save(db, kind, [item(kind), item(kind)])
- assert old.del_flag == 0 and not db.dirty
- def test_omitted_product_images_stay_and_empty_string_clears(db):
- parent = CrmProduct(
- id='p', tenant_no='t', product_no='P', product_name='甲醇',
- images='https://a/1.jpg,https://a/2.jpg', del_flag=0,
- )
- db.add(parent)
- db.commit()
- crud.update_product(db, 'p', remark='只改备注')
- assert parent.images == 'https://a/1.jpg,https://a/2.jpg'
- crud.update_product(db, 'p', images='')
- assert parent.images == ''
|