| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- """SQLite-only vehicle personnel identity and partial-field regressions."""
- from datetime import datetime
- import pymysql
- import pytest
- from fastapi import HTTPException
- from sqlalchemy import create_engine, event
- from sqlalchemy.orm import Session
- from app.models.pl_models import PlPersonnel, PlVehiclePersonnel
- from app.schemas.pl.base_data import VehiclePersonnelRow
- from app.services.pl_personnel_service import save_vehicle_personnel, upsert_personnel
- @pytest.fixture
- def db(monkeypatch):
- def forbidden(*a, **k):
- raise AssertionError('Business database access forbidden')
- monkeypatch.setattr(pymysql, 'connect', forbidden)
- monkeypatch.setattr(pymysql.connections.Connection, 'connect', forbidden)
- engine = create_engine('sqlite://')
- PlPersonnel.__table__.create(engine)
- PlVehiclePersonnel.__table__.create(engine)
- with Session(engine, autoflush=False, expire_on_commit=False) as session:
- yield session
- engine.dispose()
- def person(db, identity='p', name='Person', phone='100', **extra):
- row = PlPersonnel(id=identity, tenant_no='t', personnel_name=name, phone_no=phone,
- create_by='creator', create_time=datetime(2020,1,1), **extra)
- db.add(row)
- return row
- def link(db, identity='link', pid='p', role='driver', **extra):
- row = PlVehiclePersonnel(id=identity, tenant_no='t', vehicle_id='v', personnel_id=pid,
- role=role, create_by='creator', create_time=datetime(2020,1,1), **extra)
- db.add(row)
- return row
- def body(**values):
- return VehiclePersonnelRow.model_validate({'personnelName':'Person', 'role':'driver', **values})
- def save(db, values):
- save_vehicle_personnel(db,'v',values,'t',user_id='editor',dept_id='dept')
- db.flush()
- @pytest.mark.parametrize('key', ['name','phone'])
- @pytest.mark.parametrize('standalone', [False,True])
- def test_ambiguous_legacy_identity_rejects_instead_of_replacing_link(db,key,standalone):
- person(db,'p1',phone=None if key=='name' else '100')
- person(db,'p2',phone=None if key=='name' else '100')
- old=link(db,pid='p2');db.commit()
- payload=body(**({'phoneNo':'100'} if key=='phone' else {}))
- with pytest.raises(HTTPException):
- if standalone:upsert_personnel(db,'t',payload,user_id='u',dept_id='d')
- else:save(db,[payload])
- assert not db.dirty and not db.new
- assert old.del_flag==0 and db.query(PlVehiclePersonnel).count()==1
- def test_unique_no_id_matches_existing_person_and_preserves_relation_identity(db):
- p=person(db);old=link(db);db.commit()
- save(db,[body(phoneNo='100')]);db.commit();save(db,[body(phoneNo='100')])
- assert db.query(PlPersonnel).count()==db.query(PlVehiclePersonnel).count()==1
- assert old.del_flag==0 and old.personnel_id==p.id
- assert old.create_by==p.create_by=='creator'
- assert old.create_time==p.create_time==datetime(2020,1,1)
- def test_omitted_fields_preserve_person_and_link_fields(db):
- p=person(db,age=42);old=link(db,remark='keep',is_default=1);db.commit()
- save(db,[body(personnelId='p')])
- assert p.phone_no=='100' and p.age==42
- assert old.remark=='keep' and old.is_default==1
- def test_explicit_null_and_zero_clear_fields(db):
- p=person(db,age=42);old=link(db,remark='keep',is_default=1);db.commit()
- save(db,[body(personnelId='p',phoneNo=None,age=None,remark=None,isDefault=0)])
- assert p.phone_no is None and p.age is None
- assert old.remark is None and old.is_default==0
- def test_new_person_can_have_two_roles_without_duplicate_person_creation(db):
- save(db,[body(phoneNo='100'),body(phoneNo='100',role='escort')]);db.commit()
- people=db.query(PlPersonnel).all();links=db.query(PlVehiclePersonnel).all()
- assert len(people)==1 and len(links)==2
- assert {row.personnel_id for row in links}=={people[0].id}
- assert {row.role for row in links}=={'driver','escort'}
- @pytest.mark.parametrize('reverse',[False,True])
- def test_explicit_identity_reserved_and_other_role_resolves_same_person(db,reverse):
- person(db);old=link(db);db.commit()
- rows=[body(personnelId='p'),body(phoneNo='100',role='escort')]
- save(db,rows[::-1] if reverse else rows)
- assert old.del_flag==0 and db.query(PlPersonnel).count()==1
- assert {row.personnel_id for row in db.query(PlVehiclePersonnel)}=={'p'}
- def test_explicit_reservation_does_not_guess_remaining_duplicate_name(db):
- person(db,'p1',phone=None);person(db,'p2',phone=None);link(db,pid='p1');db.commit()
- with pytest.raises(HTTPException):save(db,[body(personnelId='p1'),body()])
- assert not db.dirty and not db.new
- @pytest.mark.parametrize('field,values',[('phoneNo',('100','200')),('age',(20,30)),
- ('personnelName',('Person','Different'))])
- def test_same_person_conflicting_fields_across_roles_reject_before_mutation(db,field,values):
- p=person(db,age=42);link(db);db.commit()
- a=body(personnelId='p',**{field:values[0]})
- b=body(personnelId='p',role='escort',**{field:values[1]})
- with pytest.raises(HTTPException):save(db,[a,b])
- assert not db.dirty and not db.new and p.age==42 and p.phone_no=='100'
- @pytest.mark.parametrize('bad',['foreign','deleted','missing','duplicate'])
- def test_entire_batch_rejects_invalid_explicit_id_before_editing_first_person(db,bad):
- p=person(db);link(db)
- if bad in ('foreign','deleted'):
- other=person(db,'other',name='Other');other.tenant_no='other' if bad=='foreign' else 't'
- other.del_flag=1 if bad=='deleted' else 0
- db.commit()
- rows=[body(personnelId='p',phoneNo='new')]
- rows.append(body(personnelId='p' if bad=='duplicate' else 'other'))
- with pytest.raises(HTTPException):save(db,rows)
- assert not db.dirty and not db.new and p.phone_no=='100'
- def test_effective_defaults_cannot_create_two_default_drivers(db):
- person(db);person(db,'second',name='Second');old=link(db,is_default=1);db.commit()
- with pytest.raises(HTTPException):save(db,[body(personnelId='p'),
- body(personnelId='second',personnelName='Second',isDefault=1)])
- assert old.is_default==1 and not db.dirty and not db.new
- def test_add_edit_remove_and_omitted_collection_are_differential(db):
- p=person(db);old=link(db);person(db,'removed',name='Removed');removed=link(db,'removed-link','removed')
- db.commit();save_vehicle_personnel(db,'v',None,'t',user_id='u',dept_id='d')
- assert old.del_flag==removed.del_flag==0
- save(db,[body(personnelId='p',phoneNo='new'),body(personnelName='New',phoneNo='300',role='escort')])
- assert old.del_flag==0 and old.personnel_id==p.id and p.phone_no=='new'
- assert removed.del_flag==1 and db.query(PlVehiclePersonnel).filter_by(del_flag=0).count()==2
- save(db,[])
- assert db.query(PlVehiclePersonnel).filter_by(del_flag=0).count()==0
- assert db.query(PlPersonnel).count()==3
- def test_cross_role_partial_person_update_merges_without_clearing_phone(db):
- p=person(db,age=42);link(db);db.commit()
- save(db,[body(personnelId='p',phoneNo='200'),body(personnelId='p',role='escort')])
- assert p.phone_no=='200' and p.age==42
- def test_standalone_upsert_preserves_omitted_optional_fields(db):
- p=person(db,age=42);db.commit()
- identity=upsert_personnel(db,'t',body(personnelId='p'),user_id='u',dept_id='d')
- assert identity==p.id and p.phone_no=='100' and p.age==42
- def test_explicit_current_client_id_is_safe_with_duplicate_names(db):
- person(db,'p1',phone=None);p=person(db,'p2',phone=None);old=link(db,pid='p2');db.commit()
- save(db,[body(personnelId='p2',remark='')])
- assert old.del_flag==0 and old.personnel_id==p.id and old.remark==''
- assert db.query(PlVehiclePersonnel).count()==1
- def test_same_name_in_other_tenant_does_not_participate_in_matching(db):
- p=person(db);old=link(db);other=person(db,'other');other.tenant_no='other';db.commit()
- save(db,[body()])
- assert old.personnel_id==p.id and old.del_flag==0 and p.phone_no=='100'
- def test_empty_collection_only_removes_current_vehicle_links(db):
- person(db);old=link(db);other=link(db,'other-link');other.vehicle_id='other-vehicle';db.commit()
- save(db,[])
- assert old.del_flag==1 and other.del_flag==0
- def test_duplicate_historical_links_reject_before_person_update(db):
- p=person(db);link(db);link(db,'duplicate-link');db.commit()
- with pytest.raises(HTTPException):save(db,[body(personnelId='p',phoneNo='200')])
- assert p.phone_no=='100' and not db.dirty and not db.new
- @pytest.mark.parametrize('reverse',[False,True])
- def test_explicit_and_missing_id_same_role_reject_regardless_of_order(db,reverse):
- person(db);link(db);db.commit()
- rows=[body(),body(personnelId='p')]
- with pytest.raises(HTTPException):save(db,rows[::-1] if reverse else rows)
- assert not db.dirty and not db.new
- def test_explicit_new_name_can_be_reused_by_other_role_in_same_batch(db):
- p=person(db);link(db);db.commit()
- save(db,[body(personnelName='Renamed',role='escort'),body(personnelId='p',personnelName='Renamed')])
- assert p.personnel_name=='Renamed' and db.query(PlPersonnel).count()==1
- assert {row.personnel_id for row in db.query(PlVehiclePersonnel)}=={'p'}
|