Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions services/ingestion/app/epcis_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ class ExportRequest(BaseModel):
summary="Export in GS1 EPCIS 2.0 JSON-LD format",
description=(
"Generates a GS1 EPCIS 2.0 compliant JSON-LD document containing all "
"traceability events. Compatible with Walmart, Kroger, and Costco portals."
"traceability events. Compatible with Walmart, Kroger, and Costco portals. "
"Stored event hashes are included without cryptographic verification."
),
)
async def export_epcis(
Expand Down Expand Up @@ -269,8 +270,9 @@ async def export_epcis(
"generatedAt": now.isoformat(),
"eventsCount": len(event_list),
"dataSource": data_source,
"integrityVerified": data_source == "tenant",
"chainHashVerified": data_source == "tenant",
# Loading stored hashes does not verify event content or chain integrity.
"integrityVerified": False,
"chainHashVerified": False,
**({"regengine:message": "No data yet"} if data_source == "empty" else {}),
},
}
Expand All @@ -297,7 +299,7 @@ async def export_epcis(
+ '"'
),
"X-RegEngine-Events-Count": str(len(event_list)),
"X-RegEngine-Integrity-Verified": "true" if data_source == "tenant" else "false",
"X-RegEngine-Integrity-Verified": "false",
"X-RegEngine-Data-Source": data_source,
}
if validate:
Expand Down
40 changes: 37 additions & 3 deletions services/ingestion/tests/test_epcis_export_1342.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ def test_returns_tenant_events_when_db_has_rows(self, client: TestClient, monkey
body = response.json()
metadata = body["regengine:exportMetadata"]
assert metadata["dataSource"] == "tenant"
assert metadata["integrityVerified"] is True
assert metadata["chainHashVerified"] is True
assert metadata["integrityVerified"] is False
assert metadata["chainHashVerified"] is False
assert metadata["eventsCount"] == 1
assert "regengine:disclaimer" not in metadata
events = body["epcisBody"]["eventList"]
Expand All @@ -199,9 +199,43 @@ def test_returns_tenant_events_when_db_has_rows(self, client: TestClient, monkey
for key in ("type", "action", "bizStep", "eventTime"):
assert isinstance(event[key], str) and event[key]
assert response.headers["X-RegEngine-Data-Source"] == "tenant"
assert response.headers["X-RegEngine-Integrity-Verified"] == "true"
assert response.headers["X-RegEngine-Integrity-Verified"] == "false"
assert response.headers["X-RegEngine-Events-Count"] == "1"

@pytest.mark.parametrize("validate", [False, True])
def test_corrupted_stored_hash_remains_unverified(
self, client: TestClient, monkeypatch: pytest.MonkeyPatch, validate: bool
) -> None:
# A well-formed but wrong hash must not acquire a verification claim
# just because it was loaded from storage or passed structural checks.
corrupted_hash = "0" * 64
session = _FakeSession(execute_queue=[[_event_row(sha256_hash=corrupted_hash)]])
monkeypatch.setattr(epcis_export, "get_db_safe", lambda: session)

response = client.post(
"/api/v1/export/epcis",
params={"validate": str(validate).lower()},
json={"tenant_id": "t", "format": "epcis"},
)

assert response.status_code == 200
body = response.json()
metadata = body["regengine:exportMetadata"]
assert metadata["dataSource"] == "tenant"
assert metadata["eventsCount"] == 1
assert body["epcisBody"]["eventList"][0]["regengine:sha256"] == corrupted_hash
assert metadata["integrityVerified"] is False
assert metadata["chainHashVerified"] is False
assert response.headers["X-RegEngine-Integrity-Verified"] == "false"
assert session.closed
if validate:
assert body["regengine:validation"]["valid"] is True
assert body["regengine:validation"]["errors"] == []
assert response.headers["X-RegEngine-Schema-Valid"] == "true"
else:
assert "regengine:validation" not in body
assert "X-RegEngine-Schema-Valid" not in response.headers

def test_fills_defaults_when_row_fields_are_null(self, client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
row = dict(_event_row(event_timestamp=None)._mapping)
monkeypatch.setattr(epcis_export, "_query_tenant_events", lambda *a, **k: [row])
Expand Down
30 changes: 29 additions & 1 deletion services/ingestion/tests/test_legacy_router_tenant_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import Mock

import app.webhook_compat as webhook_compat
import pytest
from app import epcis_export, team_mgmt
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient

TENANT_A = "00000000-0000-0000-0000-0000000000aa"
Expand Down Expand Up @@ -40,6 +42,8 @@ def test_legacy_path_tenant_mismatch_rejected_before_handler(monkeypatch):

def test_legacy_json_body_tenant_mismatch_rejected_before_handler(monkeypatch):
_install_scoped_key(monkeypatch, TENANT_A)
query = Mock(side_effect=AssertionError("Unauthorized export must not query storage"))
monkeypatch.setattr(epcis_export, "_query_tenant_events", query)
app = FastAPI()
app.include_router(epcis_export.router)

Expand All @@ -51,3 +55,27 @@ def test_legacy_json_body_tenant_mismatch_rejected_before_handler(monkeypatch):

assert response.status_code == 409
assert response.json()["detail"]["code"] == "E_TENANT_MISMATCH"
query.assert_not_called()


@pytest.mark.parametrize("headers", [{}, {"X-RegEngine-API-Key": "invalid-test-key"}])
def test_export_auth_failure_rejected_before_storage(monkeypatch, headers):
async def _deny_key(*, request, x_regengine_api_key):
raise HTTPException(status_code=401, detail="Invalid API key")

monkeypatch.setattr(webhook_compat, "require_api_key", _deny_key)
query = Mock(side_effect=AssertionError("Unauthorized export must not query storage"))
monkeypatch.setattr(epcis_export, "_query_tenant_events", query)
app = FastAPI()
app.include_router(epcis_export.router)

response = TestClient(app).post(
"/api/v1/export/epcis",
headers=headers,
json={"tenant_id": TENANT_A, "format": "epcis"},
)

assert response.status_code == 401
assert "regengine:exportMetadata" not in response.json()
assert "X-RegEngine-Integrity-Verified" not in response.headers
query.assert_not_called()
Loading