CI (SIAX Cloud) / quality (push) Successful in 51s
Security Scan (SIAX Cloud) / security (push) Successful in 26s
CI (SIAX Cloud) / security (push) Successful in 28s
CI (SIAX Cloud) / integration (push) Successful in 1m1s
CI (SIAX Cloud) / integration (pull_request) Successful in 30s
CI (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / quality (pull_request) Successful in 48s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 22s
Tenant isolation (security-critical): every app/api/0pt/** route reading
tenant_id from client-controlled query/body never cross-checked it against
the token middleware.ts already verified (x-0pt-verified-tenant). Adds a
shared assertTenantMatchesVerified guard and wires it into all 20+ affected
routes. Also closes two full-leak-by-omission bugs found in this pass
(documents/capture and lakehouse/mappings GETs returned every tenant's rows
when tenant_id was simply omitted) and two "no tenant check at all" reads
(rate-policies/[id], lakehouse/mappings/[id]) reachable by ID alone.
Duplicate-delivery idempotency (correctness-critical): POST /api/orders now
honors Idempotency-Key (sql/006, replay-or-409-on-conflict, Postgres +
in-memory); the Python orchestrator's POST /orders/{id}/run — the actual
delivery trigger — is now idempotent under an asyncio lock, closing a real
concurrent-request race, not just the sequential-retry case.
Engine-duplication resolution: adaptive_router.py's docstring wrongly
invited a bypass ("see b0ts for scraping") contradicting the product's own
compliance stance; corrected, and given real (additive, not escalating)
block-signal tracking so a rate-limited/blocked source is excluded from the
plan instead of hammered. Also fixes a previously-undiscovered production
bug: the Python engine's only call into the TS quality bridge
(X-Internal-Token) was unconditionally 401'd by middleware.ts, which never
checked that credential — the advisory quality bridge was unreachable in any
environment enforcing auth.
Restore drill: executed for real against throwaway Docker Postgres
containers (not simulated). Found and fixed two real blockers along the way:
sql/004 referenced opt_datasets, which no migration ever created (chain was
never actually runnable end to end); lib/db.ts hardcoded ssl:"require",
making any non-TLS Postgres (incl. compose.dev.yaml's own local-dev
container) unreachable. After both fixes: full migration chain applies
cleanly, pg_dump/pg_restore round-trip is byte-identical (row counts +
content checksums), and real application code (lib/orders-store.ts,
SourceRegistryService's Postgres repository) reads the restored data back
correctly, including cross-tenant isolation still holding.
82 new tests (55 TS + 27 Python), all passing; full existing suite (291 TS +
55 Python) still green; typecheck clean on every changed/added file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
115 lines
5.3 KiB
Python
115 lines
5.3 KiB
Python
"""
|
|
P014: integration proof that a source signaling a block (HTTP 401/403/429/451)
|
|
during a real run_job() call (a) never crashes or fabricates a result, (b) is
|
|
recorded on the process-wide adaptive_router so the engine backs off that
|
|
source, and (c) the pipeline still delivers from the next compliant source in
|
|
the plan -- exactly the "source-racing several authorized sources" behavior
|
|
the siax-0pt skill documents, now with an honest memory of blocks instead of
|
|
silently re-trying a rate-limited source with none.
|
|
|
|
Uses the real default_router (module-level singleton pipeline.py actually
|
|
uses), cleared before and after every test in this file so this suite never
|
|
leaks state into -- or picks up state from -- any other test module that
|
|
imports workers.pipeline.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import httpx
|
|
import pytest
|
|
import respx
|
|
|
|
from workers.adaptive_router import default_router
|
|
from workers.pipeline import run_job
|
|
|
|
COMPANIES_HOUSE_URL = "https://api.company-information.service.gov.uk/company/00000006"
|
|
GLEIF_URL = "https://api.gleif.org/api/v1/lei-records/00000006"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_router():
|
|
default_router.clear()
|
|
yield
|
|
default_router.clear()
|
|
|
|
|
|
class TestSourceBlockedDuringARealRun:
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_a_429_from_one_source_is_recorded_as_blocked_and_the_run_still_delivers_from_the_next_source(
|
|
self, monkeypatch
|
|
):
|
|
monkeypatch.setenv("COMPANIES_HOUSE_API_KEY", "test-key")
|
|
respx.get(COMPANIES_HOUSE_URL).mock(return_value=httpx.Response(429, json={"error": "rate limited"}))
|
|
respx.get(GLEIF_URL).mock(
|
|
return_value=httpx.Response(
|
|
200,
|
|
json={"data": {"attributes": {"entity": {"legalName": {"name": "Fallback Ltd"}, "status": "ACTIVE", "legalAddress": {}}}}},
|
|
)
|
|
)
|
|
|
|
assert default_router.is_blocked("GB", "companies_house") is False # sanity: clean slate
|
|
|
|
result = await run_job("order_block_1", {"markets": ["GB"], "confidence": 50, "seeds": {"GB": ["00000006"]}})
|
|
|
|
assert result["state"] == "done"
|
|
assert result["metrics"]["rows"] == 1
|
|
assert result["records"][0]["name"] == "Fallback Ltd"
|
|
assert result["records"][0]["source"] == "gleif" # companies_house was blocked, gleif answered
|
|
|
|
# THE regression this test guards: the block was actually recorded,
|
|
# not silently discarded the way a bare `except Exception: res = None`
|
|
# would (that's still the fallback for genuine errors, but a block is
|
|
# now distinguishable from one).
|
|
assert default_router.is_blocked("GB", "companies_house") is True
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_a_blocked_source_is_excluded_from_the_plan_on_the_very_next_key_in_the_same_run(self, monkeypatch):
|
|
monkeypatch.setenv("COMPANIES_HOUSE_API_KEY", "test-key")
|
|
gb_second_url = "https://api.company-information.service.gov.uk/company/11111111"
|
|
gleif_second_url = "https://api.gleif.org/api/v1/lei-records/11111111"
|
|
|
|
companies_house_calls = respx.get(COMPANIES_HOUSE_URL).mock(return_value=httpx.Response(429, json={}))
|
|
# No mock registered for gb_second_url at all -- if the pipeline tried
|
|
# companies_house AGAIN for the second key after it was just blocked,
|
|
# respx would raise on the unmocked request and this test would fail
|
|
# with a connection error, not a clean assertion. That absence IS the
|
|
# assertion: the plan excluded the just-blocked source.
|
|
respx.get(GLEIF_URL).mock(
|
|
return_value=httpx.Response(200, json={"data": {"attributes": {"entity": {"legalName": {"name": "First"}, "legalAddress": {}}}}})
|
|
)
|
|
respx.get(gleif_second_url).mock(
|
|
return_value=httpx.Response(200, json={"data": {"attributes": {"entity": {"legalName": {"name": "Second"}, "legalAddress": {}}}}})
|
|
)
|
|
|
|
result = await run_job(
|
|
"order_block_2",
|
|
{"markets": ["GB"], "confidence": 50, "seeds": {"GB": ["00000006", "11111111"]}},
|
|
)
|
|
|
|
assert result["state"] == "done"
|
|
assert result["metrics"]["rows"] == 2
|
|
names = {r["name"] for r in result["records"]}
|
|
assert names == {"First", "Second"}
|
|
# companies_house was hit exactly once (for the first key, where it
|
|
# got blocked) -- never retried for the second key in this run.
|
|
assert companies_house_calls.call_count == 1
|
|
|
|
@pytest.mark.asyncio
|
|
@respx.mock
|
|
async def test_a_plain_5xx_is_not_treated_as_a_block(self, monkeypatch):
|
|
# A genuine server error is a different signal from a compliance
|
|
# block -- it should NOT be recorded via record_blocked (that would
|
|
# incorrectly suppress a source that's just having a bad moment, not
|
|
# one that told the engine to stop).
|
|
monkeypatch.setenv("COMPANIES_HOUSE_API_KEY", "test-key")
|
|
respx.get(COMPANIES_HOUSE_URL).mock(return_value=httpx.Response(500, json={}))
|
|
respx.get(GLEIF_URL).mock(
|
|
return_value=httpx.Response(200, json={"data": {"attributes": {"entity": {"legalName": {"name": "X"}, "legalAddress": {}}}}})
|
|
)
|
|
|
|
result = await run_job("order_block_3", {"markets": ["GB"], "confidence": 50, "seeds": {"GB": ["00000006"]}})
|
|
|
|
assert result["state"] == "done"
|
|
assert default_router.is_blocked("GB", "companies_house") is False
|