From 56fb1d963fa373b3069548e774c127f7ec67e4d2 Mon Sep 17 00:00:00 2001 From: Christopher Sellers <1@christophersellers.com> Date: Fri, 23 Oct 2026 02:48:54 -0700 Subject: [PATCH 1/2] fix(epcis-export): return honest empty state instead of sample data Remove SAMPLE_EPCIS_EVENTS and the hardcoded FDA sample CSV. Tenants with no rows now get an empty eventList / header-only CSV, count 0, dataSource 'empty' and a 'No data yet' message. Tests are data-free. Drop the now-stale detect-secrets baseline entries for the sample hashes. Signed-off-by: Christopher Sellers <1@christophersellers.com> --- .secrets.baseline | 32 +------ services/ingestion/app/epcis_export.py | 86 ++++--------------- .../ingestion/tests/test_epcis_export_1342.py | 38 ++++---- 3 files changed, 33 insertions(+), 123 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 834b4df10..ef063de2f 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -4157,36 +4157,6 @@ "line_number": 205 } ], - "services/ingestion/app/epcis_export.py": [ - { - "type": "Hex High Entropy String", - "filename": "services/ingestion/app/epcis_export.py", - "hashed_secret": "d9628db26a0fe230ac85c6e1f4d8e30ded6fec22", - "is_verified": false, - "line_number": 190 - }, - { - "type": "Hex High Entropy String", - "filename": "services/ingestion/app/epcis_export.py", - "hashed_secret": "64f49f14a963a261feb8cd12e0197e5867ae75a9", - "is_verified": false, - "line_number": 191 - }, - { - "type": "Hex High Entropy String", - "filename": "services/ingestion/app/epcis_export.py", - "hashed_secret": "6145bcb9eb0209bf23fc14f480724821e0a104b7", - "is_verified": false, - "line_number": 209 - }, - { - "type": "Hex High Entropy String", - "filename": "services/ingestion/app/epcis_export.py", - "hashed_secret": "0d6622b8003561598c82b121b6f4e2c59571c216", - "is_verified": false, - "line_number": 210 - } - ], "services/ingestion/tests/test_boundaries.py": [ { "type": "Basic Auth Credentials", @@ -5187,5 +5157,5 @@ } ] }, - "generated_at": "2026-10-20T13:07:06Z" + "generated_at": "2026-10-23T09:48:32Z" } diff --git a/services/ingestion/app/epcis_export.py b/services/ingestion/app/epcis_export.py index 2460b9012..9414afd31 100644 --- a/services/ingestion/app/epcis_export.py +++ b/services/ingestion/app/epcis_export.py @@ -20,12 +20,17 @@ from shared.tenant_context import TenantContext from sqlalchemy import text -from .disclaimers import SAMPLE_EXPORT_DISCLAIMER from .shared.csv_safety import sanitize_cell from .webhook_compat import require_tenant_context logger = logging.getLogger("epcis-export") +FDA_CSV_HEADER = ( + "CTE_Type,Traceability_Lot_Code,Product_Description,Quantity,Unit_of_Measure," + "Event_Date,Event_Time,Ship_From_Location,Ship_From_GLN,Ship_To_Location," + "Ship_To_GLN,Carrier,Temperature_C,SHA256_Hash" +) + def _query_tenant_events( tenant_id: str, lot_code: str | None, date_from: str | None, date_to: str | None @@ -164,54 +169,6 @@ class ExportRequest(BaseModel): date_to: Optional[str] = Field(None, description="End date (YYYY-MM-DD)") -# Sample EPCIS 2.0 event data (in production, pulled from database) -SAMPLE_EPCIS_EVENTS = [ - { - "type": "ObjectEvent", - "eventTime": "2026-02-26T14:30:00.000Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:shipping", - "disposition": "urn:epcglobal:cbv:disp:in_transit", - "epcList": ["urn:epc:id:sgtin:0614141.000001.001"], - "readPoint": {"id": "urn:epc:id:sgln:0614141.00000.0"}, - "bizLocation": {"id": "urn:epc:id:sgln:0614141.00000.0"}, - "extension": { - "quantityList": [ - {"epcClass": "urn:epc:class:lgtin:0614141.000001.TOM-0226-F3-001", "quantity": 200, "uom": "CS"} - ], - "sourceList": [ - {"type": "urn:epcglobal:cbv:sdt:possessing_party", "source": "urn:epc:id:pgln:0614141.00000"} - ], - "destinationList": [ - {"type": "urn:epcglobal:cbv:sdt:possessing_party", "destination": "urn:epc:id:pgln:0614141.00001"} - ], - }, - "regengine:sha256": "a3f8c1d2e4b5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1", - "regengine:chainHash": "b4c9e2c7d5f6a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3", - }, - { - "type": "ObjectEvent", - "eventTime": "2026-02-27T09:15:00.000Z", - "eventTimeZoneOffset": "-08:00", - "action": "OBSERVE", - "bizStep": "urn:epcglobal:cbv:bizstep:receiving", - "disposition": "urn:epcglobal:cbv:disp:in_progress", - "epcList": ["urn:epc:id:sgtin:0614141.000001.001"], - "readPoint": {"id": "urn:epc:id:sgln:0614141.00001.0"}, - "bizLocation": {"id": "urn:epc:id:sgln:0614141.00001.0"}, - "extension": { - "quantityList": [ - {"epcClass": "urn:epc:class:lgtin:0614141.000001.TOM-0226-F3-001", "quantity": 200, "uom": "CS"} - ], - "ilmd": {"regengine:temperatureCelsius": 3.2, "regengine:inspectionResult": "PASS"}, - }, - "regengine:sha256": "c5dae3d8e6f7a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4", - "regengine:chainHash": "d6ebf4e9f7a8b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5", - }, -] - - @router.post( "/epcis", summary="Export in GS1 EPCIS 2.0 JSON-LD format", @@ -230,10 +187,10 @@ async def export_epcis( tenant_id = ctx.tenant_id rows = _query_tenant_events(tenant_id, request.lot_code, request.date_from, request.date_to) - data_source = "tenant" if rows else "sample" + data_source = "tenant" if rows else "empty" + event_list = [] if rows: - event_list = [] for r in rows: ts = r["event_timestamp"] event = { @@ -256,9 +213,6 @@ async def export_epcis( "regengine:sha256": r["sha256_hash"] or "", } event_list.append(event) - else: - event_list = SAMPLE_EPCIS_EVENTS - epcis_document = { "@context": [ "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld", @@ -279,7 +233,7 @@ async def export_epcis( "dataSource": data_source, "integrityVerified": data_source == "tenant", "chainHashVerified": data_source == "tenant", - **({"regengine:disclaimer": SAMPLE_EXPORT_DISCLAIMER} if data_source == "sample" else {}), + **({"regengine:message": "No data yet"} if data_source == "empty" else {}), }, } @@ -331,13 +285,11 @@ async def export_fda( tenant_id = ctx.tenant_id rows = _query_tenant_events(tenant_id, request.lot_code, request.date_from, request.date_to) - data_source = "tenant" if rows else "sample" + data_source = "tenant" if rows else "empty" if rows: # Build CSV from real data - lines = [ - "CTE_Type,Traceability_Lot_Code,Product_Description,Quantity,Unit_of_Measure,Event_Date,Event_Time,Ship_From_Location,Ship_From_GLN,Ship_To_Location,Ship_To_GLN,Carrier,Temperature_C,SHA256_Hash" - ] + lines = [FDA_CSV_HEADER] for r in rows: ts = r["event_timestamp"] date_str = ts.strftime("%Y-%m-%d") if ts else "" @@ -365,16 +317,8 @@ async def export_fda( csv_content = "\n".join(lines) + "\n" event_count = len(rows) else: - # Sample data fallback for tenants with no events yet - csv_content = """CTE_Type,Traceability_Lot_Code,Product_Description,Quantity,Unit_of_Measure,Event_Date,Event_Time,Ship_From_Location,Ship_From_GLN,Ship_To_Location,Ship_To_GLN,Carrier,Temperature_C,SHA256_Hash -SHIPPING,TOM-0226-F3-001,Roma Tomatoes 12ct,200,cases,2026-02-26,14:30:00Z,Valley Fresh Farms Salinas CA,0614141000005,Metro Distribution Center LA,0614141000012,Cold Express Logistics,3.2,a3f8c1d2e4b5... -RECEIVING,TOM-0226-F3-001,Roma Tomatoes 12ct,200,cases,2026-02-27,09:15:00Z,Valley Fresh Farms Salinas CA,0614141000005,Metro Distribution Center LA,0614141000012,,3.5,c5dae3d8e6f7... -SHIPPING,LET-0226-A2-003,Romaine Lettuce Hearts 12ct,150,cases,2026-02-26,16:00:00Z,Green Valley Farms Salinas CA,0614141000029,Metro Distribution Center LA,0614141000012,Fresh Fleet Inc,2.8,e7f0a1b2c3d4... -HARVESTING,CUC-0226-F2-015,English Cucumbers,500,cases,2026-02-25,08:00:00Z,Sunrise Farms Field 2,,,,,,f8a1b2c3d4e5... -COOLING,SAL-0226-B1-007,Atlantic Salmon Fillets,300,lbs,2026-02-26,06:00:00Z,Pacific Seafood Portland OR,0614141000036,,,,-1.5,a9b0c1d2e3f4... -TRANSFORMATION,SALAD-0226-001,Garden Salad Mix 16oz,1000,bags,2026-02-28,10:00:00Z,Metro Processing Plant LA,0614141000012,,,,,b0c1d2e3f4a5... -""" - event_count = 6 + csv_content = FDA_CSV_HEADER + "\n" + event_count = 0 # EPIC-L (#1655): route filename through the shared safe_filename # builder so tenant_id can't carry CRLF / quotes into the header value. @@ -394,8 +338,8 @@ async def export_fda( "X-RegEngine-Format": "FDA_21CFR1.1455", "X-RegEngine-Data-Source": data_source, } - if data_source == "sample": - fda_headers["X-RegEngine-Disclaimer"] = SAMPLE_EXPORT_DISCLAIMER + if data_source == "empty": + fda_headers["X-RegEngine-Message"] = "No data yet" return StreamingResponse( iter([csv_content]), diff --git a/services/ingestion/tests/test_epcis_export_1342.py b/services/ingestion/tests/test_epcis_export_1342.py index 92587d1b2..8f656f3cd 100644 --- a/services/ingestion/tests/test_epcis_export_1342.py +++ b/services/ingestion/tests/test_epcis_export_1342.py @@ -32,7 +32,6 @@ import app.epcis_export as epcis_export from app.epcis_export import ( - SAMPLE_EPCIS_EVENTS, _query_tenant_events, _validate_epcis_document, router as epcis_export_router, @@ -260,7 +259,7 @@ async def _tenant_ctx(request: Request) -> TenantContext: class TestExportEpcisEndpoint: - def test_returns_sample_events_when_no_tenant_rows( + def test_returns_empty_events_when_no_tenant_rows( self, client: TestClient, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: []) @@ -272,16 +271,17 @@ def test_returns_sample_events_when_no_tenant_rows( body = response.json() assert body["type"] == "EPCISDocument" assert body["schemaVersion"] == "2.0" - # Sample path. - assert body["epcisBody"]["eventList"] == SAMPLE_EPCIS_EVENTS + assert body["epcisBody"]["eventList"] == [] metadata = body["regengine:exportMetadata"] - assert metadata["dataSource"] == "sample" + assert metadata["eventsCount"] == 0 + assert metadata["dataSource"] == "empty" assert metadata["integrityVerified"] is False assert metadata["chainHashVerified"] is False - assert "regengine:disclaimer" in metadata - assert response.headers["X-RegEngine-Data-Source"] == "sample" + assert metadata["regengine:message"] == "No data yet" + assert "regengine:disclaimer" not in metadata + assert response.headers["X-RegEngine-Data-Source"] == "empty" assert response.headers["X-RegEngine-Integrity-Verified"] == "false" - assert response.headers["X-RegEngine-Events-Count"] == str(len(SAMPLE_EPCIS_EVENTS)) + assert response.headers["X-RegEngine-Events-Count"] == "0" assert "X-RegEngine-Schema-Valid" not in response.headers def test_returns_tenant_events_when_db_has_rows(self, client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None: @@ -352,16 +352,15 @@ def test_validate_flag_emits_validation_block_and_header( ) assert response.status_code == 200 body = response.json() - # Sample events are valid. validation = body["regengine:validation"] - assert validation["valid"] is True - assert validation["errors"] == [] + assert validation["valid"] is False + assert validation["errors"] == ["epcisBody.eventList is empty or missing"] assert "checkedAt" in validation - assert response.headers["X-RegEngine-Schema-Valid"] == "true" + assert response.headers["X-RegEngine-Schema-Valid"] == "false" class TestExportFdaEndpoint: - def test_sample_fallback_when_no_rows(self, client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None: + def test_header_only_when_no_rows(self, client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: []) response = client.post( "/api/v1/export/fda", @@ -369,14 +368,11 @@ def test_sample_fallback_when_no_rows(self, client: TestClient, monkeypatch: pyt ) assert response.status_code == 200 assert response.headers["content-type"].startswith("text/csv") - assert response.headers["X-RegEngine-Data-Source"] == "sample" - assert response.headers["X-RegEngine-Events-Count"] == "6" - assert "X-RegEngine-Disclaimer" in response.headers - body = response.text - assert body.startswith("CTE_Type,") - # Sample includes these fixture rows. - assert "TOM-0226-F3-001" in body - assert "HARVESTING" in body + assert response.headers["X-RegEngine-Data-Source"] == "empty" + assert response.headers["X-RegEngine-Events-Count"] == "0" + assert response.headers["X-RegEngine-Message"] == "No data yet" + assert "X-RegEngine-Disclaimer" not in response.headers + assert response.text == epcis_export.FDA_CSV_HEADER + "\n" def test_builds_csv_from_real_rows(self, client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None: row1 = dict(_event_row()._mapping) From 7026cb15a1a22bd1fe103fce8f49622dc3dc8a0a Mon Sep 17 00:00:00 2001 From: Christopher Sellers <1@christophersellers.com> Date: Fri, 23 Oct 2026 06:15:16 -0700 Subject: [PATCH 2/2] test(epcis): drop SAMPLE_EPCIS_EVENTS patches; data-free empty-state and header-only FDA CSV assertions --- services/ingestion/tests/test_epcis_export_1342.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/services/ingestion/tests/test_epcis_export_1342.py b/services/ingestion/tests/test_epcis_export_1342.py index c5aed4ffa..b6f588157 100644 --- a/services/ingestion/tests/test_epcis_export_1342.py +++ b/services/ingestion/tests/test_epcis_export_1342.py @@ -6,6 +6,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import Any +from uuid import uuid4 import pytest from fastapi import FastAPI, Request @@ -103,7 +104,7 @@ def test_no_rows_returns_empty_list(self, monkeypatch: pytest.MonkeyPatch) -> No assert _query_tenant_events("t", None, None, None) == [] def test_swallow_exception_returns_empty_list(self, monkeypatch: pytest.MonkeyPatch) -> None: - session = _FakeSession(execute_queue=[RuntimeError("db down")]) + session = _FakeSession(execute_queue=[RuntimeError()]) monkeypatch.setattr(epcis_export, "get_db_safe", lambda: session) result = _query_tenant_events("t", None, None, None) assert result == [] @@ -131,7 +132,7 @@ async def _tenant_ctx(request: Request) -> TenantContext: return TenantContext( tenant_id=tenant_id or "t", principal_kind="test", - principal_id="t", + principal_id=uuid4().hex, ) app.dependency_overrides[_verify_api_key] = lambda: None @@ -143,7 +144,6 @@ class TestExportEpcisEndpoint: def test_returns_empty_events_when_no_tenant_rows( self, client: TestClient, monkeypatch: pytest.MonkeyPatch ) -> None: - monkeypatch.setattr(epcis_export, "SAMPLE_EPCIS_EVENTS", []) monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: []) response = client.post( "/api/v1/export/epcis", @@ -219,7 +219,6 @@ def test_validate_flag_emits_validation_block_and_header( self, client: TestClient, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: []) - monkeypatch.setattr(epcis_export, "SAMPLE_EPCIS_EVENTS", []) response = client.post( "/api/v1/export/epcis?validate=true", json={"tenant_id": "t"}, @@ -238,7 +237,7 @@ def test_header_only_when_no_rows(self, client: TestClient, monkeypatch: pytest. monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: []) response = client.post( "/api/v1/export/fda", - json={"tenant_id": "tenant-x"}, + json={"tenant_id": "t"}, ) assert response.status_code == 200 assert response.headers["content-type"].startswith("text/csv") @@ -246,9 +245,7 @@ def test_header_only_when_no_rows(self, client: TestClient, monkeypatch: pytest. assert response.headers["X-RegEngine-Events-Count"] == "0" assert response.headers["X-RegEngine-Message"] == "No data yet" assert "X-RegEngine-Disclaimer" not in response.headers - assert response.text == epcis_export.FDA_CSV_HEADER + "\n" - - assert response.status_code == 422 + assert [line for line in response.text.splitlines() if line] == [epcis_export.FDA_CSV_HEADER] class TestListExportFormats: