|
15 | 15 |
|
16 | 16 | import asyncio |
17 | 17 | import base64 |
| 18 | +import contextlib as _contextlib |
18 | 19 | import json |
| 20 | +import os |
19 | 21 | from typing import Any |
20 | 22 | from unittest.mock import patch |
21 | 23 |
|
@@ -595,6 +597,202 @@ def test_handle_django_returns_402_on_discovery_leg(monkeypatch: pytest.MonkeyPa |
595 | 597 | # ───────────────────────────────────────────────────────────────────────────── |
596 | 598 |
|
597 | 599 |
|
| 600 | +# ───────────────────────────────────────────────────────────────────────────── |
| 601 | +# buildSignedUcpResponse / buildSignedJwksResponse / bootstrapUcpSigningKey |
| 602 | +# ───────────────────────────────────────────────────────────────────────────── |
| 603 | + |
| 604 | + |
| 605 | +@_contextlib.contextmanager |
| 606 | +def _env_key(jwk_dict: dict[str, Any]) -> Any: |
| 607 | + """Yield with UCP_SIGNING_KEY_JWK_PRIVATE set to ``jwk_dict``, restoring on exit.""" |
| 608 | + from agentscore_commerce.identity.ucp_jwks import _reset_ucp_signing_key_cache |
| 609 | + |
| 610 | + _reset_ucp_signing_key_cache() |
| 611 | + prev = os.environ.get("UCP_SIGNING_KEY_JWK_PRIVATE") |
| 612 | + os.environ["UCP_SIGNING_KEY_JWK_PRIVATE"] = json.dumps(jwk_dict) |
| 613 | + try: |
| 614 | + yield |
| 615 | + finally: |
| 616 | + if prev is None: |
| 617 | + os.environ.pop("UCP_SIGNING_KEY_JWK_PRIVATE", None) |
| 618 | + else: |
| 619 | + os.environ["UCP_SIGNING_KEY_JWK_PRIVATE"] = prev |
| 620 | + _reset_ucp_signing_key_cache() |
| 621 | + |
| 622 | + |
| 623 | +def test_bootstrap_ucp_signing_key_throws_on_malformed_env() -> None: |
| 624 | + from agentscore_commerce.discovery import bootstrap_ucp_signing_key |
| 625 | + from agentscore_commerce.identity.ucp_jwks import _reset_ucp_signing_key_cache |
| 626 | + |
| 627 | + _reset_ucp_signing_key_cache() |
| 628 | + prev = os.environ.get("UCP_SIGNING_KEY_JWK_PRIVATE") |
| 629 | + os.environ["UCP_SIGNING_KEY_JWK_PRIVATE"] = "not-json" |
| 630 | + try: |
| 631 | + with pytest.raises((ValueError, Exception)): |
| 632 | + bootstrap_ucp_signing_key() |
| 633 | + finally: |
| 634 | + if prev is None: |
| 635 | + os.environ.pop("UCP_SIGNING_KEY_JWK_PRIVATE", None) |
| 636 | + else: |
| 637 | + os.environ["UCP_SIGNING_KEY_JWK_PRIVATE"] = prev |
| 638 | + _reset_ucp_signing_key_cache() |
| 639 | + |
| 640 | + |
| 641 | +def test_bootstrap_ucp_signing_key_succeeds_with_valid_env() -> None: |
| 642 | + from agentscore_commerce.discovery import bootstrap_ucp_signing_key |
| 643 | + from agentscore_commerce.identity.ucp_jwks import generate_ucp_signing_key |
| 644 | + |
| 645 | + key = generate_ucp_signing_key(kid="bootstrap-test") |
| 646 | + private_jwk = key.private_key.as_dict(private=True) |
| 647 | + with _env_key(private_jwk): |
| 648 | + bootstrap_ucp_signing_key() # should not raise |
| 649 | + |
| 650 | + |
| 651 | +def test_build_signed_jwks_response_emits_jwk_set_json() -> None: |
| 652 | + from agentscore_commerce.discovery import build_signed_jwks_response |
| 653 | + from agentscore_commerce.identity.ucp_jwks import generate_ucp_signing_key |
| 654 | + |
| 655 | + key = generate_ucp_signing_key(kid="jwks-test") |
| 656 | + private_jwk = key.private_key.as_dict(private=True) |
| 657 | + with _env_key(private_jwk): |
| 658 | + resp = build_signed_jwks_response(request_headers={"X-Request-Id": "req-jwks"}) |
| 659 | + assert resp.status == 200 |
| 660 | + assert resp.media_type == "application/jwk-set+json" |
| 661 | + assert "max-age=300" in resp.headers["Cache-Control"] |
| 662 | + assert resp.headers["X-Request-ID"] == "req-jwks" |
| 663 | + body = json.loads(resp.content) |
| 664 | + assert len(body["keys"]) == 1 |
| 665 | + |
| 666 | + |
| 667 | +def test_build_signed_ucp_response_misconfigured_when_no_rails() -> None: |
| 668 | + from agentscore_commerce.checkout import Checkout, PricingResult |
| 669 | + from agentscore_commerce.discovery import build_signed_ucp_response |
| 670 | + |
| 671 | + async def _pricing(ctx: Any) -> PricingResult: |
| 672 | + return PricingResult(amount_usd=1.0) |
| 673 | + |
| 674 | + checkout = Checkout(rails={}, url="https://x/purchase", compute_pricing=_pricing) |
| 675 | + resp = build_signed_ucp_response( |
| 676 | + checkout=checkout, |
| 677 | + name="X", |
| 678 | + well_known_ucp_url="https://x/.well-known/ucp", |
| 679 | + services={}, |
| 680 | + request_headers={"X-Request-Id": "req-misc"}, |
| 681 | + ) |
| 682 | + assert resp.status == 503 |
| 683 | + assert "max-age=60" in resp.headers["Cache-Control"] |
| 684 | + assert resp.headers["X-Request-ID"] == "req-misc" |
| 685 | + body = json.loads(resp.content) |
| 686 | + assert body["error"]["code"] == "ucp_misconfigured" |
| 687 | + |
| 688 | + |
| 689 | +def test_build_signed_ucp_response_happy_path_signs_profile() -> None: |
| 690 | + from agentscore_commerce.checkout import Checkout, PricingResult |
| 691 | + from agentscore_commerce.discovery import build_signed_ucp_response |
| 692 | + from agentscore_commerce.identity.ucp_jwks import generate_ucp_signing_key |
| 693 | + |
| 694 | + async def _pricing(ctx: Any) -> PricingResult: |
| 695 | + return PricingResult(amount_usd=1.0) |
| 696 | + |
| 697 | + key = generate_ucp_signing_key(kid="ucp-test") |
| 698 | + private_jwk = key.private_key.as_dict(private=True) |
| 699 | + checkout = Checkout( |
| 700 | + rails={ |
| 701 | + "tempo": TempoRailSpec(recipient="0x" + "00" * 19 + "dE" + "aD"), |
| 702 | + "base": X402BaseRailSpec(recipient="0x" + "00" * 19 + "dE" + "aD"), |
| 703 | + }, |
| 704 | + url="https://x/purchase", |
| 705 | + compute_pricing=_pricing, |
| 706 | + ) |
| 707 | + with _env_key(private_jwk): |
| 708 | + resp = build_signed_ucp_response( |
| 709 | + checkout=checkout, |
| 710 | + name="AgentScore Store", |
| 711 | + well_known_ucp_url="https://x/.well-known/ucp", |
| 712 | + services={"dev.ucp.shopping": []}, |
| 713 | + signing_kid="ucp-test", |
| 714 | + request_headers={"X-Request-Id": "req-ucp"}, |
| 715 | + ) |
| 716 | + assert resp.status == 200 |
| 717 | + assert resp.headers["X-Request-ID"] == "req-ucp" |
| 718 | + assert "max-age=60" in resp.headers["Cache-Control"] |
| 719 | + body = json.loads(resp.content) |
| 720 | + assert body["ucp"]["name"] == "AgentScore Store" |
| 721 | + assert "signature" in body |
| 722 | + assert body["ucp"]["payment_handlers"] |
| 723 | + |
| 724 | + |
| 725 | +def test_build_signed_ucp_response_includes_solana_stripe_tempo_session() -> None: |
| 726 | + from agentscore_commerce.checkout import Checkout, PricingResult |
| 727 | + from agentscore_commerce.discovery import build_signed_ucp_response |
| 728 | + from agentscore_commerce.identity.ucp_jwks import generate_ucp_signing_key |
| 729 | + from agentscore_commerce.payment import SolanaMppRailSpec, StripeRailSpec, TempoSessionRailSpec |
| 730 | + |
| 731 | + async def _pricing(ctx: Any) -> PricingResult: |
| 732 | + return PricingResult(amount_usd=1.0) |
| 733 | + |
| 734 | + key = generate_ucp_signing_key(kid="ucp-multi") |
| 735 | + private_jwk = key.private_key.as_dict(private=True) |
| 736 | + checkout = Checkout( |
| 737 | + rails={ |
| 738 | + "solana": SolanaMppRailSpec(recipient="SoLaNaReCiPiEnT"), |
| 739 | + "stripe": StripeRailSpec(profile_id="profile_abc"), |
| 740 | + "tempo_session": TempoSessionRailSpec( |
| 741 | + recipient="0x" + "00" * 20, |
| 742 | + escrow_contract="0x" + "11" * 20, |
| 743 | + store=object(), |
| 744 | + ), |
| 745 | + }, |
| 746 | + url="https://x/purchase", |
| 747 | + compute_pricing=_pricing, |
| 748 | + ) |
| 749 | + with _env_key(private_jwk): |
| 750 | + resp = build_signed_ucp_response( |
| 751 | + checkout=checkout, |
| 752 | + name="Multi-Rail", |
| 753 | + well_known_ucp_url="https://x/.well-known/ucp", |
| 754 | + services={}, |
| 755 | + signing_kid="ucp-multi", |
| 756 | + ) |
| 757 | + assert resp.status == 200 |
| 758 | + body = json.loads(resp.content) |
| 759 | + keys = list(body["ucp"]["payment_handlers"].keys()) |
| 760 | + assert any("mpp" in k or "stripe" in k for k in keys) |
| 761 | + |
| 762 | + |
| 763 | +def test_default_a2a_services_returns_canonical_a2a_binding() -> None: |
| 764 | + from agentscore_commerce.discovery.well_known import default_a2a_services |
| 765 | + |
| 766 | + services = default_a2a_services(agent_card_url="https://x/.well-known/agent-card.json") |
| 767 | + assert "dev.ucp.shopping" in services |
| 768 | + binding = services["dev.ucp.shopping"][0] |
| 769 | + assert binding.transport == "a2a" |
| 770 | + assert binding.endpoint == "https://x/.well-known/agent-card.json" |
| 771 | + |
| 772 | + |
| 773 | +def test_well_known_cors_preflight_headers_without_request() -> None: |
| 774 | + from agentscore_commerce.discovery import well_known_cors_preflight_headers |
| 775 | + |
| 776 | + headers = well_known_cors_preflight_headers() |
| 777 | + assert headers["Access-Control-Allow-Origin"] == "*" |
| 778 | + assert "GET" in headers["Access-Control-Allow-Methods"] |
| 779 | + assert "Access-Control-Allow-Headers" not in headers |
| 780 | + |
| 781 | + |
| 782 | +def test_well_known_cors_preflight_headers_echoes_acrh() -> None: |
| 783 | + from agentscore_commerce.discovery import well_known_cors_preflight_headers |
| 784 | + |
| 785 | + headers = well_known_cors_preflight_headers( |
| 786 | + {"Access-Control-Request-Headers": "x-foo, x-bar"}, |
| 787 | + ) |
| 788 | + assert headers["Access-Control-Allow-Headers"] == "x-foo, x-bar" |
| 789 | + |
| 790 | + |
| 791 | +# ───────────────────────────────────────────────────────────────────────────── |
| 792 | +# load_solana_fee_payer |
| 793 | +# ───────────────────────────────────────────────────────────────────────────── |
| 794 | + |
| 795 | + |
598 | 796 | def test_load_solana_fee_payer_returns_none_on_empty() -> None: |
599 | 797 | from agentscore_commerce.payment.solana import load_solana_fee_payer |
600 | 798 |
|
@@ -801,6 +999,42 @@ def test_x_payment_info_from_checkout_lists_protocols_per_rail() -> None: |
801 | 999 | assert any(p.get("mpp", {}).get("method") == "tempo" for p in block["protocols"]) |
802 | 1000 |
|
803 | 1001 |
|
| 1002 | +def test_x_payment_info_from_checkout_covers_all_rail_types() -> None: |
| 1003 | + from agentscore_commerce.checkout import Checkout, PricingResult |
| 1004 | + from agentscore_commerce.discovery import ( |
| 1005 | + XPaymentInfoFixedPrice, |
| 1006 | + x_payment_info_from_checkout, |
| 1007 | + ) |
| 1008 | + from agentscore_commerce.payment import SolanaMppRailSpec, StripeRailSpec |
| 1009 | + |
| 1010 | + async def _pricing(ctx: Any) -> PricingResult: |
| 1011 | + return PricingResult(amount_usd=1.0) |
| 1012 | + |
| 1013 | + checkout = Checkout( |
| 1014 | + rails={ |
| 1015 | + "tempo": TempoRailSpec(recipient="0x" + "00" * 20), |
| 1016 | + "base": X402BaseRailSpec(recipient="0x" + "00" * 20), |
| 1017 | + "stripe": StripeRailSpec(profile_id="profile_abc"), |
| 1018 | + "solana": SolanaMppRailSpec(recipient="SoLaNaReCiPiEnT", token="EPjFWdd5..."), |
| 1019 | + }, |
| 1020 | + url="https://x/purchase", |
| 1021 | + compute_pricing=_pricing, |
| 1022 | + ) |
| 1023 | + ext = x_payment_info_from_checkout( |
| 1024 | + checkout=checkout, |
| 1025 | + price=XPaymentInfoFixedPrice(currency="USD", amount="1.00"), |
| 1026 | + ) |
| 1027 | + protos = ext["x-payment-info"]["protocols"] |
| 1028 | + methods = [p.get("mpp", {}).get("method") or "x402" for p in protos] |
| 1029 | + assert "stripe" in methods |
| 1030 | + assert "tempo" in methods |
| 1031 | + assert "solana" in methods |
| 1032 | + assert "x402" in methods |
| 1033 | + # Solana entry should include the `currency` from token |
| 1034 | + solana_entry = next(p["mpp"] for p in protos if p.get("mpp", {}).get("method") == "solana") |
| 1035 | + assert solana_entry["currency"] == "EPjFWdd5..." |
| 1036 | + |
| 1037 | + |
804 | 1038 | def test_x_payment_info_from_checkout_merges_protocol_extras() -> None: |
805 | 1039 | from agentscore_commerce.discovery import ( |
806 | 1040 | XPaymentInfoFixedPrice, |
|
0 commit comments