feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
211
server/tests/test_financial_connector_operational_events.py
Normal file
211
server/tests/test_financial_connector_operational_events.py
Normal file
@@ -0,0 +1,211 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, func, select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
import app.models # noqa: F401 - 注册完整 metadata
|
||||
from app.db.base_class import Base
|
||||
from app.models.financial_connector import (
|
||||
FinancialConnectorConfig,
|
||||
FinancialConnectorOperationalEvent,
|
||||
)
|
||||
from app.schemas.financial_connector import FinancialEventEnvelope
|
||||
from app.services.financial_connector_auth import (
|
||||
FinancialConnectorAuthenticator,
|
||||
FinancialConnectorAuthError,
|
||||
FinancialConnectorSecretResolver,
|
||||
)
|
||||
from app.services.financial_connector_observability import (
|
||||
FinancialConnectorObservabilityService,
|
||||
)
|
||||
from app.services.financial_connector_operational_events import (
|
||||
FinancialConnectorOperationalContext,
|
||||
FinancialConnectorOperationalEventService,
|
||||
operational_event_candidate,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def db() -> Session:
|
||||
engine = create_engine(
|
||||
"sqlite+pysqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
Base.metadata.create_all(engine)
|
||||
factory = sessionmaker(bind=engine, expire_on_commit=False)
|
||||
with factory() as session:
|
||||
yield session
|
||||
Base.metadata.drop_all(engine)
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_operational_attempts_are_counted_but_candidate_retry_is_idempotent(
|
||||
db: Session,
|
||||
) -> None:
|
||||
config = _config(db)
|
||||
context = _context(config)
|
||||
as_of = datetime(2026, 7, 17, 10, 2, tzinfo=UTC)
|
||||
first = operational_event_candidate(
|
||||
context,
|
||||
event_type="replay",
|
||||
reason_code="duplicate_external_event",
|
||||
occurred_at=as_of - timedelta(minutes=2),
|
||||
)
|
||||
second = operational_event_candidate(
|
||||
context,
|
||||
event_type="replay",
|
||||
reason_code="duplicate_external_event",
|
||||
occurred_at=as_of - timedelta(minutes=1),
|
||||
)
|
||||
outside_window = operational_event_candidate(
|
||||
context,
|
||||
event_type="auth_failure",
|
||||
reason_code="signature_invalid",
|
||||
occurred_at=as_of - timedelta(hours=2),
|
||||
)
|
||||
service = FinancialConnectorOperationalEventService(db)
|
||||
|
||||
service.record(first)
|
||||
service.record(first)
|
||||
service.record(second)
|
||||
service.record(outside_window)
|
||||
db.commit()
|
||||
|
||||
assert first.idempotency_key != second.idempotency_key
|
||||
assert db.scalar(select(func.count(FinancialConnectorOperationalEvent.id))) == 3
|
||||
observed = FinancialConnectorObservabilityService(db, now=as_of).read_for_tenant(
|
||||
config.tenant_id,
|
||||
window_hours=1,
|
||||
)
|
||||
assert observed.window_started_at == as_of - timedelta(hours=1)
|
||||
assert observed.as_of == as_of
|
||||
assert observed.source_revision == "20260716_0022"
|
||||
assert observed.summary.retry_count == 2
|
||||
assert observed.summary.auth_failure_count == 0
|
||||
assert observed.summary.latest_replay_at == second.occurred_at
|
||||
|
||||
|
||||
def test_auth_failure_is_attributed_only_after_trusted_config_and_secret_resolution(
|
||||
db: Session,
|
||||
) -> None:
|
||||
config = _config(db)
|
||||
secret = "trusted-server-secret"
|
||||
now_epoch = 1_800_000_000
|
||||
envelope = FinancialEventEnvelope(
|
||||
tenant_id=config.tenant_id,
|
||||
external_event_id="raw-external-event-must-not-be-stored",
|
||||
event_type="payment_settled",
|
||||
occurred_at=datetime.fromtimestamp(now_epoch, UTC),
|
||||
correlation_id="raw-correlation-must-not-be-stored",
|
||||
payload={
|
||||
"claim_id": str(uuid.uuid4()),
|
||||
"claim_reference": "BX-RAW-MUST-NOT-BE-STORED",
|
||||
"amount": "66.00",
|
||||
"currency": "CNY",
|
||||
"external_payment_reference": "RAW-PAYMENT-MUST-NOT-BE-STORED",
|
||||
},
|
||||
)
|
||||
authenticator = FinancialConnectorAuthenticator(
|
||||
db,
|
||||
secrets=FinancialConnectorSecretResolver({config.secret_ref: secret}),
|
||||
now_epoch=now_epoch,
|
||||
)
|
||||
|
||||
with pytest.raises(FinancialConnectorAuthError) as forged_tenant:
|
||||
authenticator.verify(
|
||||
envelope,
|
||||
tenant_header="tenant-forged",
|
||||
provider_header=config.provider,
|
||||
key_version_header=config.key_version,
|
||||
timestamp_header=str(now_epoch),
|
||||
signature_header="sha256=" + "0" * 64,
|
||||
)
|
||||
assert forged_tenant.value.operational_context is None
|
||||
|
||||
with pytest.raises(FinancialConnectorAuthError) as forged_provider:
|
||||
authenticator.verify(
|
||||
envelope,
|
||||
tenant_header=config.tenant_id,
|
||||
provider_header="provider-forged",
|
||||
key_version_header=config.key_version,
|
||||
timestamp_header=str(now_epoch),
|
||||
signature_header="sha256=" + "0" * 64,
|
||||
)
|
||||
assert forged_provider.value.operational_context is None
|
||||
|
||||
with pytest.raises(FinancialConnectorAuthError) as trusted_failure:
|
||||
authenticator.verify(
|
||||
envelope,
|
||||
tenant_header=config.tenant_id,
|
||||
provider_header=config.provider,
|
||||
key_version_header=config.key_version,
|
||||
timestamp_header=str(now_epoch),
|
||||
signature_header="sha256=" + "0" * 64,
|
||||
)
|
||||
context = trusted_failure.value.operational_context
|
||||
assert context is not None
|
||||
assert context.request_fingerprint.startswith("hmac-sha256:")
|
||||
assert context.external_event_fingerprint.startswith("hmac-sha256:")
|
||||
|
||||
candidate = operational_event_candidate(
|
||||
context,
|
||||
event_type="auth_failure",
|
||||
reason_code=trusted_failure.value.code,
|
||||
occurred_at=datetime.fromtimestamp(now_epoch, UTC),
|
||||
)
|
||||
FinancialConnectorOperationalEventService(db).record(candidate)
|
||||
db.commit()
|
||||
row = db.scalar(select(FinancialConnectorOperationalEvent))
|
||||
assert row is not None
|
||||
stored = "|".join(
|
||||
(
|
||||
row.provider,
|
||||
row.reason_code,
|
||||
row.request_fingerprint,
|
||||
row.external_event_fingerprint,
|
||||
row.idempotency_key,
|
||||
)
|
||||
)
|
||||
for raw_value in (
|
||||
secret,
|
||||
envelope.external_event_id,
|
||||
envelope.correlation_id,
|
||||
envelope.payload["claim_reference"],
|
||||
envelope.payload["external_payment_reference"],
|
||||
):
|
||||
assert str(raw_value) not in stored
|
||||
|
||||
|
||||
def _config(db: Session) -> FinancialConnectorConfig:
|
||||
row = FinancialConnectorConfig(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id="tenant-operational",
|
||||
provider="operational-bank",
|
||||
environment="production",
|
||||
key_version="v1",
|
||||
secret_ref="connector/operational-test",
|
||||
allowed_event_types_json=["payment_settled"],
|
||||
clock_skew_seconds=300,
|
||||
status="active",
|
||||
created_by="operational-test",
|
||||
)
|
||||
db.add(row)
|
||||
db.flush()
|
||||
return row
|
||||
|
||||
|
||||
def _context(config: FinancialConnectorConfig) -> FinancialConnectorOperationalContext:
|
||||
return FinancialConnectorOperationalContext(
|
||||
tenant_id=config.tenant_id,
|
||||
config_id=config.id,
|
||||
provider=config.provider,
|
||||
environment=config.environment,
|
||||
request_fingerprint="hmac-sha256:" + "a" * 64,
|
||||
external_event_fingerprint="hmac-sha256:" + "b" * 64,
|
||||
)
|
||||
Reference in New Issue
Block a user