172 lines
6.7 KiB
Python
172 lines
6.7 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from lead_scoring.cli.webhook_contract import (
|
|
TEST_SIGNING_SECRET,
|
|
canonical_json,
|
|
generate_contract_files,
|
|
run_webhook_contract_cli,
|
|
sign_webhook,
|
|
)
|
|
from lead_scoring.schemas.review import (
|
|
REVIEW_EVENT_TYPE,
|
|
LeadScoringReviewedEvent,
|
|
ReviewDecision,
|
|
)
|
|
from lead_scoring.schemas.scoring_run import ScoringRunStatus
|
|
from lead_scoring.schemas.webhook import LeadScoringTerminalEvent, WebhookIngestErrorResponse
|
|
|
|
|
|
def test_contract_files_are_deterministic_and_validate_against_models():
|
|
files = generate_contract_files()
|
|
|
|
assert files == generate_contract_files()
|
|
assert set(files) == {
|
|
"contract.json",
|
|
"lead-scoring-terminal-event.schema.json",
|
|
"lead-scoring-reviewed-event.schema.json",
|
|
"webhook-ingest-response.schema.json",
|
|
"webhook-ingest-error-response.schema.json",
|
|
"fixtures/completed.json",
|
|
"fixtures/tenant-unavailable.json",
|
|
"fixtures/review_required.json",
|
|
"fixtures/worker_interrupted.json",
|
|
"fixtures/reviewed_approve.json",
|
|
"fixtures/reviewed_override.json",
|
|
"fixtures/reviewed_reevaluate.json",
|
|
"signature-vectors.json",
|
|
}
|
|
|
|
manifest = json.loads(files["contract.json"])
|
|
assert manifest["contract_pack_version"] == "0.1.0"
|
|
assert manifest["endpoint_path"] == "/api/v1/internal/lead-scoring/events"
|
|
assert manifest["event_types"] == [
|
|
"lead_scoring.run.terminal",
|
|
"lead_scoring.run.reviewed",
|
|
]
|
|
assert manifest["supported_event_types"] == [
|
|
"lead_scoring.run.terminal",
|
|
"lead_scoring.run.reviewed",
|
|
]
|
|
assert manifest["method"] == "POST"
|
|
assert manifest["replay_window_seconds"] == 300
|
|
assert manifest["signature_algorithm"] == "HMAC-SHA256"
|
|
assert manifest["signature_version"] == "v1"
|
|
|
|
terminal_schema = json.loads(files["lead-scoring-terminal-event.schema.json"])
|
|
assert "event_type" in terminal_schema["required"]
|
|
assert "event_version" in terminal_schema["required"]
|
|
|
|
reviewed_schema = json.loads(files["lead-scoring-reviewed-event.schema.json"])
|
|
assert "event_type" in reviewed_schema["required"]
|
|
assert "event_version" in reviewed_schema["required"]
|
|
|
|
completed = LeadScoringTerminalEvent.model_validate(
|
|
json.loads(files["fixtures/completed.json"])
|
|
)
|
|
review_required = LeadScoringTerminalEvent.model_validate(
|
|
json.loads(files["fixtures/review_required.json"])
|
|
)
|
|
worker_interrupted = LeadScoringTerminalEvent.model_validate(
|
|
json.loads(files["fixtures/worker_interrupted.json"])
|
|
)
|
|
|
|
assert completed.data.status is ScoringRunStatus.COMPLETED
|
|
assert review_required.data.status is ScoringRunStatus.COMPLETED
|
|
assert worker_interrupted.data.status is ScoringRunStatus.FAILED
|
|
assert worker_interrupted.data.error_code == "worker_interrupted"
|
|
assert worker_interrupted.data.retryable is True
|
|
|
|
reviewed_appr = LeadScoringReviewedEvent.model_validate(
|
|
json.loads(files["fixtures/reviewed_approve.json"])
|
|
)
|
|
assert reviewed_appr.event_type == REVIEW_EVENT_TYPE
|
|
assert reviewed_appr.data.decision is ReviewDecision.APPROVE
|
|
assert reviewed_appr.data.effective_score == 61
|
|
assert reviewed_appr.data.version == 2
|
|
assert reviewed_appr.data.reviewed_by == "agent_10"
|
|
|
|
reviewed_over = LeadScoringReviewedEvent.model_validate(
|
|
json.loads(files["fixtures/reviewed_override.json"])
|
|
)
|
|
assert reviewed_over.event_type == REVIEW_EVENT_TYPE
|
|
assert reviewed_over.data.decision is ReviewDecision.OVERRIDE
|
|
assert reviewed_over.data.calculated_score == 61
|
|
assert reviewed_over.data.effective_score == 90
|
|
assert reviewed_over.data.version == 2
|
|
assert reviewed_over.data.reviewed_by == "agent_10"
|
|
|
|
reviewed_reeval = LeadScoringReviewedEvent.model_validate(
|
|
json.loads(files["fixtures/reviewed_reevaluate.json"])
|
|
)
|
|
assert reviewed_reeval.event_type == REVIEW_EVENT_TYPE
|
|
assert reviewed_reeval.data.status == "superseded"
|
|
assert reviewed_reeval.data.decision is ReviewDecision.RE_EVALUATE
|
|
assert reviewed_reeval.data.calculated_score == 82
|
|
assert reviewed_reeval.data.effective_score == 82
|
|
assert reviewed_reeval.data.version == 2
|
|
assert reviewed_reeval.data.reviewed_by == "agent_10"
|
|
|
|
|
|
def test_tenant_unavailable_error_contract_is_exact():
|
|
files = generate_contract_files()
|
|
response_body = json.loads(files["fixtures/tenant-unavailable.json"])
|
|
response = WebhookIngestErrorResponse.model_validate(response_body)
|
|
|
|
assert response_body == {
|
|
"error_code": "subscriber_tenant_unavailable",
|
|
"detail": "Webhook destination is not available",
|
|
}
|
|
assert response.model_dump(mode="json") == response_body
|
|
|
|
schema = json.loads(files["webhook-ingest-error-response.schema.json"])
|
|
assert schema["additionalProperties"] is False
|
|
assert schema["required"] == ["error_code", "detail"]
|
|
|
|
with pytest.raises(ValidationError):
|
|
WebhookIngestErrorResponse.model_validate({**response_body, "tenant_id": "hidden"})
|
|
with pytest.raises(ValidationError):
|
|
WebhookIngestErrorResponse.model_validate(
|
|
{**response_body, "error_code": "tenant_not_registered"}
|
|
)
|
|
|
|
|
|
def test_signature_vectors_match_fixture_bytes():
|
|
files = generate_contract_files()
|
|
vectors = json.loads(files["signature-vectors.json"])
|
|
|
|
assert vectors["replay_window_seconds"] == 300
|
|
assert vectors["signature_version"] == "v1"
|
|
assert vectors["test_secret"] == TEST_SIGNING_SECRET
|
|
for vector in vectors["vectors"]:
|
|
body = files[vector["body_file"]].encode()
|
|
assert vector["signature"] == sign_webhook(
|
|
TEST_SIGNING_SECRET,
|
|
vector["event_id"],
|
|
vector["timestamp"],
|
|
body,
|
|
)
|
|
|
|
|
|
def test_webhook_contract_cli_generates_and_checks_contract_pack(tmp_path: Path):
|
|
output_dir = tmp_path / "contract"
|
|
|
|
assert run_webhook_contract_cli(["--output", str(output_dir)]) == 0
|
|
assert run_webhook_contract_cli(["--output", str(output_dir), "--check"]) == 0
|
|
|
|
|
|
def test_webhook_contract_cli_rejects_stale_or_unexpected_artifacts(tmp_path: Path):
|
|
output_dir = tmp_path / "contract"
|
|
assert run_webhook_contract_cli(["--output", str(output_dir)]) == 0
|
|
|
|
fixture = output_dir / "fixtures" / "completed.json"
|
|
fixture.write_text(canonical_json({"stale": True}), encoding="utf-8")
|
|
assert run_webhook_contract_cli(["--output", str(output_dir), "--check"]) == 1
|
|
|
|
assert run_webhook_contract_cli(["--output", str(output_dir)]) == 0
|
|
(output_dir / "unexpected.json").write_text("{}\n", encoding="utf-8")
|
|
assert run_webhook_contract_cli(["--output", str(output_dir), "--check"]) == 1
|