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