Repository navigation
fix(suppliers): do not cascade facility rows that arrive during removal #3615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
298c578
52e955d
5cfd41f
aa0e068
bb6bb8d
c57711f
995086a
ef99d3f
ee850b9
9b166bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -646,3 +646,100 @@ def _deny_owner(): | |
| with TestClient(app) as member_client: | ||
| resp = member_client.delete(f"/v1/supplier/facilities/{uuid4()}") | ||
| assert resp.status_code == 403 | ||
|
|
||
| def test_delete_empty_facility(self, client: TestClient, db_session: Session, ids: TestIds): | ||
| facility = _make_facility(db_session, ids) | ||
| resp = client.delete(f"/v1/supplier/facilities/{facility.id}") | ||
| assert resp.status_code == 200 | ||
| assert resp.json()["status"] == "deleted" | ||
| db_session.expire_all() | ||
| assert db_session.get(SupplierFacilityModel, facility.id) is None | ||
|
|
||
| def test_linked_lot_returns_409(self, client: TestClient, db_session: Session, ids: TestIds): | ||
| facility = _make_facility(db_session, ids) | ||
| db_session.add( | ||
| SupplierTraceabilityLotModel( | ||
| tenant_id=ids.tenant_id, | ||
| supplier_user_id=ids.user_id, | ||
| facility_id=facility.id, | ||
| tlc_code=f"tlc-{uuid4().hex}", | ||
| product_description=f"desc-{uuid4().hex}", | ||
| status="active", | ||
| ) | ||
| ) | ||
| db_session.commit() | ||
|
|
||
| resp = client.delete(f"/v1/supplier/facilities/{facility.id}") | ||
| assert resp.status_code == 409 | ||
| db_session.expire_all() | ||
| assert db_session.get(SupplierFacilityModel, facility.id) is not None | ||
|
|
||
| def test_stale_empty_count_does_not_cascade_lot( | ||
| self, client: TestClient, db_session: Session, ids: TestIds, monkeypatch: pytest.MonkeyPatch | ||
| ): | ||
| """A lot committed after the pre-check must not be deleted with the facility. | ||
|
|
||
| Lots use ON DELETE CASCADE. The pre-check and the DELETE used to be | ||
| separate statements, so a lot that landed in between was cascaded away. | ||
| """ | ||
| import app.supplier_facilities_routes as fac_routes | ||
|
|
||
| facility = _make_facility(db_session, ids) | ||
| lot = SupplierTraceabilityLotModel( | ||
| tenant_id=ids.tenant_id, | ||
| supplier_user_id=ids.user_id, | ||
| facility_id=facility.id, | ||
| tlc_code=f"tlc-{uuid4().hex}", | ||
| product_description=f"desc-{uuid4().hex}", | ||
| status="active", | ||
| ) | ||
| db_session.add(lot) | ||
| db_session.commit() | ||
| monkeypatch.setattr(fac_routes, "_linked_record_counts", lambda *_args, **_kwargs: (0, 0, 0)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Start the linked-row transaction before deletion. The proposed deletion-first ordering is inverted. If deletion acquires Start a PostgreSQL transaction that inserts and flushes a linked lot or CTE event, then keep it uncommitted. Start deletion in a second session and synchronize at
|
||
|
|
||
| resp = client.delete(f"/v1/supplier/facilities/{facility.id}") | ||
| assert resp.status_code == 409 | ||
| db_session.expire_all() | ||
| assert db_session.get(SupplierFacilityModel, facility.id) is not None | ||
| assert db_session.get(SupplierTraceabilityLotModel, lot.id) is not None | ||
|
|
||
| def test_stale_empty_count_does_not_cascade_cte_event( | ||
| self, client: TestClient, db_session: Session, ids: TestIds, monkeypatch: pytest.MonkeyPatch | ||
| ): | ||
| """An event filed on this facility must survive even when its lot lives elsewhere.""" | ||
| import app.supplier_facilities_routes as fac_routes | ||
|
|
||
| target = _make_facility(db_session, ids, name=f"name-{uuid4().hex}") | ||
| other = _make_facility(db_session, ids, name=f"name-{uuid4().hex}") | ||
| lot = SupplierTraceabilityLotModel( | ||
| tenant_id=ids.tenant_id, | ||
| supplier_user_id=ids.user_id, | ||
| facility_id=other.id, | ||
| tlc_code=f"tlc-{uuid4().hex}", | ||
| product_description=f"desc-{uuid4().hex}", | ||
| status="active", | ||
| ) | ||
| db_session.add(lot) | ||
| db_session.commit() | ||
| event = SupplierCTEEventModel( | ||
| tenant_id=ids.tenant_id, | ||
| supplier_user_id=ids.user_id, | ||
| facility_id=target.id, | ||
| lot_id=lot.id, | ||
| cte_type="shipping", | ||
| event_time=datetime.now(timezone.utc), | ||
| kde_data={}, | ||
| payload_sha256="", | ||
| merkle_hash="", | ||
| sequence_number=1, | ||
| obligation_ids=[], | ||
| ) | ||
| db_session.add(event) | ||
| db_session.commit() | ||
| monkeypatch.setattr(fac_routes, "_linked_record_counts", lambda *_args, **_kwargs: (0, 0, 0)) | ||
|
|
||
| resp = client.delete(f"/v1/supplier/facilities/{target.id}") | ||
| assert resp.status_code == 409 | ||
| db_session.expire_all() | ||
| assert db_session.get(SupplierFacilityModel, target.id) is not None | ||
| assert db_session.get(SupplierCTEEventModel, event.id) is not None | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔎 Supported by static analysis
Script executed:
Repository: regengine/RegEngine
Length of output: 14400
Add unauthenticated and replay negative-path tests for facility deletion.
The
clientfixture overridesget_session,get_current_user, andrequire_owner. No test in this file sends a DELETE without those overrides, so an authentication regression on this route would not fail these tests. The repository test guidance requires unauthenticated and replay negative paths for security-relevant changes. This PR changes a tenant-scoped destructive route.Add these two tests:
get_sessionand the tenant context. Do not overrideget_current_userorrequire_owner. Send a DELETE for an existing facility. Assert the authentication-failure status, then assert that the facility still exists.404. Assert that an unrelated facility and its linked lot remain.