From e8df7925088f831ab6aaf37ccb8fbad7e926f71a Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:08:29 -0400 Subject: [PATCH 01/29] feat(paymentcrypto): init --- moto/backend_index.py | 1 + moto/paymentcryptography/__init__.py | 1 + moto/paymentcryptography/exceptions.py | 3 +++ moto/paymentcryptography/models.py | 16 ++++++++++++ moto/paymentcryptography/responses.py | 25 +++++++++++++++++++ moto/paymentcryptography/urls.py | 9 +++++++ tests/test_paymentcryptography/__init__.py | 0 .../test_paymentcryptography.py | 7 ++++++ tests/test_paymentcryptography/test_server.py | 13 ++++++++++ 9 files changed, 75 insertions(+) create mode 100644 moto/paymentcryptography/__init__.py create mode 100644 moto/paymentcryptography/exceptions.py create mode 100644 moto/paymentcryptography/models.py create mode 100644 moto/paymentcryptography/responses.py create mode 100644 moto/paymentcryptography/urls.py create mode 100644 tests/test_paymentcryptography/__init__.py create mode 100644 tests/test_paymentcryptography/test_paymentcryptography.py create mode 100644 tests/test_paymentcryptography/test_server.py diff --git a/moto/backend_index.py b/moto/backend_index.py index 61de465ef95c..220a5e38fba2 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -166,6 +166,7 @@ ("opensearchserverless", re.compile("https?://aoss\\.(.+)\\.amazonaws\\.com")), ("organizations", re.compile("https?://organizations\\.(.+)\\.amazonaws\\.com")), ("osis", re.compile("https?://osis\\.(.+)\\.amazonaws\\.com")), + ("paymentcryptography", re.compile("https?://payment-cryptography\\.(.+)\\.amazonaws\\.com")), ("personalize", re.compile("https?://personalize\\.(.+)\\.amazonaws\\.com")), ("pinpoint", re.compile("https?://pinpoint\\.(.+)\\.amazonaws\\.com")), ("pipes", re.compile("https?://pipes\\.(.+)\\.amazonaws\\.com")), diff --git a/moto/paymentcryptography/__init__.py b/moto/paymentcryptography/__init__.py new file mode 100644 index 000000000000..37cb2da8acc9 --- /dev/null +++ b/moto/paymentcryptography/__init__.py @@ -0,0 +1 @@ +from .models import paymentcryptography_backends #noqa: F401 \ No newline at end of file diff --git a/moto/paymentcryptography/exceptions.py b/moto/paymentcryptography/exceptions.py new file mode 100644 index 000000000000..991371891847 --- /dev/null +++ b/moto/paymentcryptography/exceptions.py @@ -0,0 +1,3 @@ +"""Exceptions raised by the paymentcryptography service.""" +from moto.core.exceptions import JsonRESTError + diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py new file mode 100644 index 000000000000..0215270ceb5a --- /dev/null +++ b/moto/paymentcryptography/models.py @@ -0,0 +1,16 @@ +"""PaymentCryptographyControlPlaneBackend class with methods for supported APIs.""" + +from moto.core.base_backend import BaseBackend, BackendDict +from moto.core.common_models import BaseModel + + +class PaymentCryptographyControlPlaneBackend(BaseBackend): + """Implementation of PaymentCryptographyControlPlane APIs.""" + + def __init__(self, region_name, account_id): + super().__init__(region_name, account_id) + + # add methods from here + + +paymentcryptography_backends = BackendDict(PaymentCryptographyControlPlaneBackend, "payment-cryptography") \ No newline at end of file diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py new file mode 100644 index 000000000000..447dd5490c27 --- /dev/null +++ b/moto/paymentcryptography/responses.py @@ -0,0 +1,25 @@ +"""Handles incoming paymentcryptography requests, invokes methods, returns responses.""" +import json + +from moto.core.responses import BaseResponse +from .models import paymentcryptography_backends + + +class PaymentCryptographyControlPlaneResponse(BaseResponse): + """Handler for PaymentCryptographyControlPlane requests and responses.""" + + def __init__(self): + super().__init__(service_name="paymentcryptography") + + @property + def paymentcryptography_backend(self): + """Return backend instance specific for this region.""" + # TODO + # paymentcryptography_backends is not yet typed + # Please modify moto/backends.py to add the appropriate type annotations for this service + return paymentcryptography_backends[self.current_account][self.region] + + # add methods from here + + +# add templates from here diff --git a/moto/paymentcryptography/urls.py b/moto/paymentcryptography/urls.py new file mode 100644 index 000000000000..9b5846592b6d --- /dev/null +++ b/moto/paymentcryptography/urls.py @@ -0,0 +1,9 @@ +"""paymentcryptography base URL and path.""" +from .responses import PaymentCryptographyControlPlaneResponse + +url_bases = [ + r"https?://controlplane.payment-cryptography\.(.+)\.amazonaws\.com", +] + +url_paths = { +} diff --git a/tests/test_paymentcryptography/__init__.py b/tests/test_paymentcryptography/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py new file mode 100644 index 000000000000..e7b243505d6f --- /dev/null +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -0,0 +1,7 @@ +"""Unit tests for paymentcryptography-supported APIs.""" +import boto3 + +from moto import mock_aws + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html \ No newline at end of file diff --git a/tests/test_paymentcryptography/test_server.py b/tests/test_paymentcryptography/test_server.py new file mode 100644 index 000000000000..9557f707903b --- /dev/null +++ b/tests/test_paymentcryptography/test_server.py @@ -0,0 +1,13 @@ +"""Test different server responses.""" + +import moto.server as server + + +def test_paymentcryptography_list(): + backend = server.create_backend_app("payment-cryptography") + test_client = backend.test_client() + + resp = test_client.get("/") + + assert resp.status_code == 200 + assert "?" in str(resp.data) From 2b877007eb37cc68c50828d5a8133ad3a18b1b92 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Sun, 12 Jul 2026 06:45:31 -0400 Subject: [PATCH 02/29] feat(paymentcrypto): fix backend url --- moto/backend_index.py | 2 +- moto/paymentcryptography/urls.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/moto/backend_index.py b/moto/backend_index.py index 220a5e38fba2..d5b75b67dceb 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -166,7 +166,7 @@ ("opensearchserverless", re.compile("https?://aoss\\.(.+)\\.amazonaws\\.com")), ("organizations", re.compile("https?://organizations\\.(.+)\\.amazonaws\\.com")), ("osis", re.compile("https?://osis\\.(.+)\\.amazonaws\\.com")), - ("paymentcryptography", re.compile("https?://payment-cryptography\\.(.+)\\.amazonaws\\.com")), + ("paymentcryptography", re.compile("https?://controlplane\\.payment-cryptography\\.(.+)\\.amazonaws\\.com")), ("personalize", re.compile("https?://personalize\\.(.+)\\.amazonaws\\.com")), ("pinpoint", re.compile("https?://pinpoint\\.(.+)\\.amazonaws\\.com")), ("pipes", re.compile("https?://pipes\\.(.+)\\.amazonaws\\.com")), diff --git a/moto/paymentcryptography/urls.py b/moto/paymentcryptography/urls.py index 9b5846592b6d..994256175b77 100644 --- a/moto/paymentcryptography/urls.py +++ b/moto/paymentcryptography/urls.py @@ -2,7 +2,7 @@ from .responses import PaymentCryptographyControlPlaneResponse url_bases = [ - r"https?://controlplane.payment-cryptography\.(.+)\.amazonaws\.com", + r"https?://controlplane\.payment-cryptography\.(.+)\.amazonaws\.com", ] url_paths = { From 45b1f97d7603ed8cbc7f6ff717478695642acd21 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:10:09 -0400 Subject: [PATCH 03/29] feat(payment-crypto): add tests --- .../test_paymentcryptography.py | 679 +++++++++++++++++- 1 file changed, 677 insertions(+), 2 deletions(-) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index e7b243505d6f..53b88f8c6095 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -1,7 +1,682 @@ """Unit tests for paymentcryptography-supported APIs.""" +from datetime import datetime + import boto3 +import json +import pytest +from botocore.exceptions import ClientError from moto import mock_aws -# See our Development Tips on writing tests for hints on how to write good tests: -# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html \ No newline at end of file + +KEY_ATTRIBUTES_1 = { + "KeyUsage": "TR31_P0_PIN_ENCRYPTION_KEY", + "KeyClass": "SYMMETRIC", + "KeyAlgorithm": "TDES_3KEY", + "KeyModesOfUse": { + "Encrypt": True, + "Decrypt": True, + "Wrap": True, + "Unwrap": True, + "Generate": True, + "Sign": True, + "Verify": True, + "DeriveKey": True, + "NoRestrictions": False, + }, +} + + +@mock_aws +def test_create_key(): + client = boto3.client("paymentcryptography", region_name="us-east-1") + + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", + MultiRegionKeyType="PRIMARY", + )["Key"] + + assert key["KeyArn"].startswith("arn:aws:payment-cryptography:us-east-1:1234567890:key/") + assert key["KeyAttributes"] == KEY_ATTRIBUTES_1 + assert key["Exportable"] is True + assert key["Enabled"] is True + assert key["KeyCheckValue"] is not None + assert key["KeyCheckValueAlgorithm"] is not None + assert key["KeyState"] == "CREATE_COMPLETE" + assert key["KeyOrigin"] == "AWS_PAYMENT_CRYPTOGRAPHY" + assert key["DeriveKeyUsage"] == "TR31_P0_PIN_ENCRYPTION_KEY" + assert isinstance(key["CreateTimestamp"], datetime) + + +# @mock_aws +# def test_create_key_with_replica_regions(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# ReplicationRegions=[ +# "us-west-2", +# "eu-west-1", +# ] +# )["Key"] + +# assert key["MultiRegionKeyType"] == "PRIMARY" +# assert key["PrimaryRegion"] == "us-east-1" +# assert key["UsingDefaultReplicationRegions"] is False +# assert key["ReplicationStatus"] == { +# "us-west-2": {"Status": "SYNCHRONIZED"}, +# "eu-west-1": {"Status": "SYNCHRONIZED"}, +# } + + +# @mock_aws +# def test_get_key(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# key_arn = key["KeyArn"] + +# retrieved_key = client.get_key(KeyArn=key_arn)["Key"] + +# assert retrieved_key["KeyArn"] == key_arn +# assert key["KeyAttributes"] == KEY_ATTRIBUTES_1 +# assert key["Exportable"] is True +# assert key["Enabled"] is True +# assert key["KeyCheckValue"] is not None +# assert key["KeyCheckValueAlgorithm"] is not None +# assert key["KeyState"] == "CREATE_COMPLETE" +# assert key["KeyOrigin"] == "AWS_PAYMENT_CRYPTOGRAPHY" +# assert key["DeriveKeyUsage"] == "TR31_P0_PIN_ENCRYPTION_KEY" +# assert isinstance(key["CreateTimestamp"], datetime) + + +# @mock_aws +# def test_delete_key(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# key_arn = key["KeyArn"] + +# # Delete the key +# client.delete_key(KeyArn=key_arn) + +# # Check that the key no longer exists +# with pytest.raises(ClientError) as exc: +# client.get_key(KeyArn=key_arn) +# err = exc.value.response["Error"] +# assert err["Code"] == "ResourceNotFoundException" + + +# @mock_aws +# def test_get_key_replica(): +# primary_client = boto3.client("paymentcryptography", region_name="us-east-1") +# replica_client = boto3.client("paymentcryptography", region_name="us-west-2") +# secondary_client = boto3.client("paymentcryptography", region_name="eu-west-1") + +# primary_arn = primary_client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# ReplicationRegions=[ +# "us-west-2", +# "eu-west-1", +# ] +# )["Key"]["KeyArn"] + +# primary_key = primary_client.get_key(KeyArn=primary_arn)["Key"] + +# # The replica shares the primary's key id, only the region differs. +# key_id = primary_arn.split(":key/")[1] +# replica_arn = f"arn:aws:payment-cryptography:us-west-2:1234567890:key/{key_id}" +# replica_key = replica_client.get_key(KeyArn=replica_arn)["Key"] +# secondary_arn = f"arn:aws:payment-cryptography:eu-west-1:1234567890:key/{key_id}" +# secondary_key = secondary_client.get_key(KeyArn=secondary_arn)["Key"] + + +# assert primary_key["MultiRegionKeyType"] == "PRIMARY" +# assert primary_key["PrimaryRegion"] == "us-east-1" +# assert primary_key["UsingDefaultReplicationRegions"] is True +# assert primary_key["ReplicationStatus"] == { +# "us-west-2": {"Status": "SYNCHRONIZED"}, +# "eu-west-1": {"Status": "SYNCHRONIZED"}, +# } +# assert replica_key["MultiRegionKeyType"] == "REPLICA" +# assert replica_key["PrimaryRegion"] == "us-east-1" +# # Check that replica does not have the keys for UsingDefaultReplicationRegions and ReplicationStatus +# assert "UsingDefaultReplicationRegions" not in replica_key +# assert "ReplicationStatus" not in replica_key +# assert secondary_key["MultiRegionKeyType"] == "REPLICA" +# assert secondary_key["PrimaryRegion"] == "us-east-1" +# # Check that secondary replica does not have the keys for UsingDefaultReplicationRegions and ReplicationStatus +# assert "UsingDefaultReplicationRegions" not in secondary_key +# assert "ReplicationStatus" not in secondary_key + + +# @mock_aws +# def test_list_keys(): +# primary_client = boto3.client("paymentcryptography", region_name="us-east-1") +# secondary_client = boto3.client("paymentcryptography", region_name="us-west-2") + +# # Create multiple keys +# for _ in range(5): +# primary_client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# ) + +# keys_list = primary_client.list_keys()["Keys"] +# assert len(keys_list) == 5 +# assert keys_list[0]["KeyArn"].startswith("arn:aws:payment-cryptography:us-east-1:1234567890:key/") +# assert keys_list[0]["KeyState"] == "CREATE_COMPLETE" + +# # Create keys with replicas +# for _ in range(2): +# primary_client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# ReplicationRegions=[ +# "us-west-2", +# "eu-west-1", +# ] +# ) + +# keys_list_primary = primary_client.list_keys()["Keys"] +# keys_list_secondary = secondary_client.list_keys()["Keys"] +# assert len(keys_list_primary) == 7 # 5 + 2 +# assert len(keys_list_secondary) == 2 # Only the replicas created in us-west-2 +# assert keys_list_primary[5]["MultiRegionKeyType"] == "PRIMARY" +# assert keys_list_secondary[0]["MultiRegionKeyType"] == "REPLICA" + + +# @mock_aws +# def test_add_key_replication_regions(): +# primary_client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key = primary_client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# )["Key"] + +# key_arn = key["KeyArn"] +# key_id = key_arn.split(":key/")[1] + +# # Add replication regions to the key +# primary_client.add_key_replication_regions( +# KeyArn=key_arn, +# ReplicationRegions=[ +# "us-west-2", +# "eu-west-1", +# ] +# ) + +# # Retrieve the key and check the replication status +# primary_key = primary_client.get_key(KeyArn=key_arn)["Key"] +# assert primary_key["MultiRegionKeyType"] == "PRIMARY" +# assert primary_key["PrimaryRegion"] == "us-east-1" +# assert primary_key["UsingDefaultReplicationRegions"] is False +# assert primary_key["ReplicationStatus"] == { +# "us-west-2": {"Status": "SYNCHRONIZED"}, +# "eu-west-1": {"Status": "SYNCHRONIZED"}, +# } + +# replica_client = boto3.client("paymentcryptography", region_name="us-west-2") +# replica_key = replica_client.get_key(KeyArn=f"arn:aws:payment-cryptography:us-west-2:1234567890:key/{key_id}")["Key"] +# assert replica_key["MultiRegionKeyType"] == "REPLICA" +# assert replica_key["PrimaryRegion"] == "us-east-1" + +# replica_client_eu = boto3.client("paymentcryptography", region_name="eu-west-1") +# replica_key_eu = replica_client_eu.get_key(KeyArn=f"arn:aws:payment-cryptography:eu-west-1:1234567890:key/{key_id}")["Key"] +# assert replica_key_eu["MultiRegionKeyType"] == "REPLICA" +# assert replica_key_eu["PrimaryRegion"] == "us-east-1" + + +# @mock_aws +# def test_enable_default_key_replication_regions(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# # Enable default replication regions +# key_replication_regions = client.enable_default_key_replication_regions( +# ReplicationRegions=[ +# "ap-southeast-1", +# ] +# ) + +# # Include default and newly added replication region +# assert key_replication_regions["EnabledReplicationRegions"] == [ +# "us-west-2", +# "eu-west-1", +# "ap-southeast-1", +# ] + + +# @mock_aws +# def test_get_default_key_replication_regions(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# default_regions = client.get_default_key_replication_regions()["EnabledReplicationRegions"] +# assert "us-west-2" in default_regions +# assert "eu-west-1" in default_regions + + +# @mock_aws +# def test_disable_default_key_replication_regions(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# # Disable default replication regions +# key_replication_regions = client.disable_default_key_replication_regions( +# ReplicationRegions=[ +# "us-west-2", +# ] +# ) + +# # Check that the specified region is removed from the enabled replication regions +# assert "us-west-2" not in key_replication_regions["EnabledReplicationRegions"] + + +# @mock_aws +# def test_tag_and_untag_key(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# key_arn = key["KeyArn"] + +# # Tag the key with additional tags +# client.tag_resource( +# ResourceArn=key_arn, +# Tags=[ +# {"Key": "foo", "Value": "1"}, +# {"Key": "bar", "Value": "2"}, +# ] +# ) + +# # Retrieve the key and check the tags with list_tags_for_resource +# retrieved_key = client.list_tags_for_resource(ResourceArn=key_arn)["Tags"] +# assert len(retrieved_key) == 3 # Original tag + 2 new tags +# assert {"Key": "Environment", "Value": "Test"} in retrieved_key +# assert {"Key": "foo", "Value": "1"} in retrieved_key +# assert {"Key": "bar", "Value": "2"} in retrieved_key + +# # Untag the key +# client.untag_resource( +# ResourceArn=key_arn, +# TagKeys=["foo", "bar"] +# ) + +# # Retrieve the key and check the tags again +# retrieved_key_after_untag = client.list_tags_for_resource(ResourceArn=key_arn)["Tags"] +# assert len(retrieved_key_after_untag) == 1 # Only the original tag should remain +# assert {"Key": "Environment", "Value": "Test"} in retrieved_key_after_untag + + +# @mock_aws +# def test_create_alias(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# key_arn = key["KeyArn"] + +# # Create an alias for the key +# alias_name = "alias/my-key-alias" +# client.create_alias( +# AliasName=alias_name, +# KeyArn=key_arn +# ) + +# # Retrieve the alias and check its properties +# alias = client.list_aliases()["Aliases"] +# assert alias["AliasName"] == alias_name +# assert alias["KeyArn"] == key_arn + + +# @mock_aws +# def test_get_alias(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# key_arn = key["KeyArn"] + +# # Create an alias for the key +# alias_name = "alias/my-key-alias" +# client.create_alias( +# AliasName=alias_name, +# KeyArn=key_arn +# ) + +# retrieved_alias = client.get_alias(AliasName=alias_name) +# assert retrieved_alias["AliasName"] == alias_name +# assert retrieved_alias["KeyArn"] == key_arn + + +# @mock_aws +# def test_update_alias(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# # Create an alias for the key +# alias_name = "alias/my-key-alias" +# client.create_alias( +# AliasName=alias_name, +# KeyArn=key["KeyArn"] +# ) + +# # Check alias for the key +# retrieved_alias = client.get_alias(AliasName=alias_name) +# assert retrieved_alias["AliasName"] == alias_name + +# # Update the alias +# new_alias_name = "alias/my-updated-key-alias" + +# client.update_alias( +# AliasName=new_alias_name, +# KeyArn=key["KeyArn"], +# ) + +# # Check key for the updated alias +# updated_alias = client.get_alias(AliasName=new_alias_name) +# assert updated_alias["AliasName"] == new_alias_name + + + +# @mock_aws +# def test_delete_alias(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# key_arn = key["KeyArn"] + +# # Create an alias for the key +# alias_name = "alias/my-key-alias" +# client.create_alias( +# AliasName=alias_name, +# KeyArn=key_arn +# ) + +# # Delete the alias +# client.delete_alias(AliasName=alias_name) + +# # Check that the alias no longer exists +# aliases_list = client.list_aliases()["Aliases"] +# assert all(alias["AliasName"] != alias_name for alias in aliases_list) + + +# @mock_aws +# def test_list_aliases(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key1 = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# key2 = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# # Create aliases for the keys +# alias_name1 = "alias/my-key-alias-1" +# alias_name2 = "alias/my-key-alias-2" +# client.create_alias( +# AliasName=alias_name1, +# KeyArn=key1["KeyArn"] +# ) +# client.create_alias( +# AliasName=alias_name2, +# KeyArn=key2["KeyArn"] +# ) + +# aliases_list = client.list_aliases()["Aliases"] +# assert len(aliases_list) == 2 +# assert any(alias["AliasName"] == alias_name1 for alias in aliases_list) +# assert any(alias["AliasName"] == alias_name2 for alias in aliases_list) + + +# @mock_aws +# def test_put_resource_policy(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# key_arn = key["KeyArn"] + +# # Put a resource policy for the key +# policy = { +# "Version": "2012-10-17", +# "Statement": [ +# { +# "Effect": "Allow", +# "Principal": {"AWS": "*"}, +# "Action": "paymentcryptography:GetKey", +# "Resource": key_arn, +# } +# ], +# } +# client.put_resource_policy( +# ResourceArn=key_arn, +# Policy=json.dumps(policy) +# ) + +# # Retrieve the resource policy and check its content +# retrieved_policy = client.get_resource_policy(ResourceArn=key_arn)["Policy"] +# assert json.loads(retrieved_policy) == policy + + +# @mock_aws +# def test_delete_resource_policy(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# key_arn = key["KeyArn"] + +# # Put a resource policy for the key +# policy = { +# "Version": "2012-10-17", +# "Statement": [ +# { +# "Effect": "Allow", +# "Principal": {"AWS": "*"}, +# "Action": "paymentcryptography:GetKey", +# "Resource": key_arn, +# } +# ], +# } +# client.put_resource_policy( +# ResourceArn=key_arn, +# Policy=json.dumps(policy) +# ) + +# # Delete the resource policy +# client.delete_resource_policy(ResourceArn=key_arn) + +# # Check that the resource policy no longer exists +# with pytest.raises(ClientError) as exc: +# client.get_resource_policy(ResourceArn=key_arn) +# err = exc.value.response["Error"] +# assert err["Code"] == "ResourceNotFoundException" + + +# @mock_aws +# def test_start_key_usage(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# # Initially disabled +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = False, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# key_arn = key["KeyArn"] + +# # Start key usage +# client.start_key_usage(KeyArn=key_arn) + +# # Retrieve the key and check its state +# updated_key = client.get_key(KeyArn=key_arn)["Key"] +# assert updated_key["KeyState"] == "ENABLED" + +# @mock_aws +# def test_stop_key_usage(): +# client = boto3.client("paymentcryptography", region_name="us-east-1") + +# # Initially enabled +# key = client.create_key( +# KeyAttributes=KEY_ATTRIBUTES_1, +# Exportable=True, +# Enabled = True, +# Tags = [ +# {"Key": "Environment", "Value": "Test"}, +# ], +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", +# MultiRegionKeyType="PRIMARY", +# )["Key"] + +# key_arn = key["KeyArn"] + +# # Stop key usage +# client.stop_key_usage(KeyArn=key_arn) + +# # Retrieve the key and check its state +# updated_key = client.get_key(KeyArn=key_arn)["Key"] +# assert updated_key["KeyState"] == "DISABLED" From 05c63fd8cf126b42b1c82327e481f55c7e8117ce Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:00:48 -0400 Subject: [PATCH 04/29] feat(payment-crypto): add key class to model --- moto/paymentcryptography/models.py | 71 ++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 0215270ceb5a..e06bec49d3ff 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -1,16 +1,81 @@ """PaymentCryptographyControlPlaneBackend class with methods for supported APIs.""" +import string +import random +from datetime import datetime, timezone +from typing import Any, Dict, List, Optional + + from moto.core.base_backend import BaseBackend, BackendDict from moto.core.common_models import BaseModel +def _random_key_id() -> str: + chars = string.ascii_lowercase + string.digits + return "".join(random.choices(chars, k=16)) + + +def _key_check_value() -> str: + # return a fake but plausible key check value based on the algorithm + kcv = "".join(random.choices("0123456789ABCDEF", k=6)) + return kcv + +class Key(BaseModel): + def __init__( + self, + account_id: str, + region_name: str, + key_attributes: Optional[Dict[str, Any]], + key_check_value_algorithm: str, + exportable: bool, + enabled: bool, + tags: list[dict[str, str]], + derive_key_usage: str, + replication_regions: list[str] + ): + self.key_id = _random_key_id() + self.key_arn = f"arn:aws:payment-cryptography:{region_name}:{account_id}:key/{self.key_id}" + self.key_attributes = key_attributes + + self.key_check_value = _key_check_value() + self.key_check_value_algorithm = key_check_value_algorithm + + self.exportable = exportable + self.enabled = enabled + self.key_state = "CREATE_COMPLETE" + + # when a key is created, the origin is AWS_PAYMENT_CRYPTOGRAPHY. + # If a key is imported, the origin is EXTERNAL. + self.key_origin = "AWS_PAYMENT_CRYPTOGRAPHY" + self.create_time = datetime.now(timezone.utc) + self.usage_start_timestamp = self.create_time if enabled else None + self.usage_stop_timestamp = None + self.delete_pending_timestamp = None + self.delete_timestamp = None + self.derive_key_usage = derive_key_usage + + if replication_regions is not None: + self.multi_region_key_type = "PRIMARY" + self.primary_region = region_name + self.replication_status = dict() + for region in replication_regions: + self.replication_status[region] = { + "replication_status": "SYNCHRONIZED", + "status_message": "Key is synchronized across regions.", + } + self.using_default_replication_regions = False + + + self.tags = tags + self.derive_key_usage = derive_key_usage + self.replication_regions = replication_regions + + class PaymentCryptographyControlPlaneBackend(BaseBackend): """Implementation of PaymentCryptographyControlPlane APIs.""" def __init__(self, region_name, account_id): super().__init__(region_name, account_id) - # add methods from here - -paymentcryptography_backends = BackendDict(PaymentCryptographyControlPlaneBackend, "payment-cryptography") \ No newline at end of file +paymentcryptography_backends = BackendDict(PaymentCryptographyControlPlaneBackend, "payment-cryptography") From 46b50c7a9c1f6742af12df37fc1e333b0555bf8d Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:14:16 -0400 Subject: [PATCH 05/29] feat(payment-crypto): scaffold add create_key, list_keys, list_tags_for_resource --- moto/backend_index.py | 7 +- moto/paymentcryptography/__init__.py | 2 +- moto/paymentcryptography/models.py | 79 ++++++++++++++++--- moto/paymentcryptography/responses.py | 50 +++++++++++- moto/paymentcryptography/urls.py | 2 + .../test_paymentcryptography.py | 38 +++++++-- 6 files changed, 155 insertions(+), 23 deletions(-) diff --git a/moto/backend_index.py b/moto/backend_index.py index d5b75b67dceb..f4a0a7eac430 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -166,7 +166,12 @@ ("opensearchserverless", re.compile("https?://aoss\\.(.+)\\.amazonaws\\.com")), ("organizations", re.compile("https?://organizations\\.(.+)\\.amazonaws\\.com")), ("osis", re.compile("https?://osis\\.(.+)\\.amazonaws\\.com")), - ("paymentcryptography", re.compile("https?://controlplane\\.payment-cryptography\\.(.+)\\.amazonaws\\.com")), + ( + "paymentcryptography", + re.compile( + "https?://controlplane\\.payment-cryptography\\.(.+)\\.amazonaws\\.com" + ), + ), ("personalize", re.compile("https?://personalize\\.(.+)\\.amazonaws\\.com")), ("pinpoint", re.compile("https?://pinpoint\\.(.+)\\.amazonaws\\.com")), ("pipes", re.compile("https?://pipes\\.(.+)\\.amazonaws\\.com")), diff --git a/moto/paymentcryptography/__init__.py b/moto/paymentcryptography/__init__.py index 37cb2da8acc9..a20fccf91713 100644 --- a/moto/paymentcryptography/__init__.py +++ b/moto/paymentcryptography/__init__.py @@ -1 +1 @@ -from .models import paymentcryptography_backends #noqa: F401 \ No newline at end of file +from .models import paymentcryptography_backends # noqa: F401 diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index e06bec49d3ff..015dbe8f0364 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -1,12 +1,11 @@ """PaymentCryptographyControlPlaneBackend class with methods for supported APIs.""" -import string import random +import string from datetime import datetime, timezone -from typing import Any, Dict, List, Optional +from typing import Any, Optional - -from moto.core.base_backend import BaseBackend, BackendDict +from moto.core.base_backend import BackendDict, BaseBackend from moto.core.common_models import BaseModel @@ -20,21 +19,24 @@ def _key_check_value() -> str: kcv = "".join(random.choices("0123456789ABCDEF", k=6)) return kcv + class Key(BaseModel): def __init__( self, account_id: str, region_name: str, - key_attributes: Optional[Dict[str, Any]], + key_attributes: Optional[dict[str, Any]], key_check_value_algorithm: str, exportable: bool, enabled: bool, tags: list[dict[str, str]], derive_key_usage: str, - replication_regions: list[str] + replication_regions: list[str], ): self.key_id = _random_key_id() - self.key_arn = f"arn:aws:payment-cryptography:{region_name}:{account_id}:key/{self.key_id}" + self.key_arn = ( + f"arn:aws:payment-cryptography:{region_name}:{account_id}:key/{self.key_id}" + ) self.key_attributes = key_attributes self.key_check_value = _key_check_value() @@ -65,10 +67,7 @@ def __init__( } self.using_default_replication_regions = False - - self.tags = tags - self.derive_key_usage = derive_key_usage - self.replication_regions = replication_regions + self.tags = tags or [] class PaymentCryptographyControlPlaneBackend(BaseBackend): @@ -77,5 +76,61 @@ class PaymentCryptographyControlPlaneBackend(BaseBackend): def __init__(self, region_name, account_id): super().__init__(region_name, account_id) + def create_key( + self, + key_attributes: Optional[dict[str, Any]], + key_check_value_algorithm: str, + exportable: bool, + enabled: bool, + tags: list[dict[str, str]], + derive_key_usage: str, + replication_regions: list[str], + ) -> dict[str, Any]: + + key = Key( + key_attributes = self.key_attributes, + + ) + + return { + + } + + + + + + + return key_arn + + def list_keys(self, key_state, next_token, max_results): + # implement here + return keys, next_token + + def list_tags_for_resource(self, resource_arn, next_token, max_results): + # implement here + return tags, next_token + -paymentcryptography_backends = BackendDict(PaymentCryptographyControlPlaneBackend, "payment-cryptography") +paymentcryptography_backends = BackendDict( + PaymentCryptographyControlPlaneBackend, + "payment-cryptography", + additional_regions=[ + "us-east-1", + "us-east-2", + "us-west-2", + "ca-central-1", + "sa-east-1", + "eu-west-1", + "eu-west-2", + "eu-west-3", + "eu-central-1", + "ap-southeast-1", + "ap-southeast-2", + "ap-northeast-1", + "ap-northeast-3", + "ap-south-1", + "ap-south-2", + "af-south-1" + ], +) diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 447dd5490c27..4943f2b7c93c 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -1,7 +1,9 @@ """Handles incoming paymentcryptography requests, invokes methods, returns responses.""" + import json from moto.core.responses import BaseResponse + from .models import paymentcryptography_backends @@ -21,5 +23,51 @@ def paymentcryptography_backend(self): # add methods from here + def create_key(self): + params = self._get_params() + key_attributes = params.get("KeyAttributes") + key_check_value_algorithm = params.get("KeyCheckValueAlgorithm") + exportable = params.get("Exportable") + enabled = params.get("Enabled") + tags = params.get("Tags") + derive_key_usage = params.get("DeriveKeyUsage") + replication_regions = params.get("ReplicationRegions") + key = self.paymentcryptography_backend.create_key( + key_attributes=key_attributes, + key_check_value_algorithm=key_check_value_algorithm, + exportable=exportable, + enabled=enabled, + tags=tags, + derive_key_usage=derive_key_usage, + replication_regions=replication_regions, + ) + # TODO: adjust response + return json.dumps(dict(key=key)) + + def list_keys(self): + params = self._get_params() + key_state = params.get("KeyState") + next_token = params.get("NextToken") + max_results = params.get("MaxResults") + keys, next_token = self.paymentcryptography_backend.list_keys( + key_state=key_state, + next_token=next_token, + max_results=max_results, + ) + # TODO: adjust response + return json.dumps(dict(keys=keys, nextToken=next_token)) + + # add templates from here -# add templates from here + def list_tags_for_resource(self): + params = self._get_params() + resource_arn = params.get("ResourceArn") + next_token = params.get("NextToken") + max_results = params.get("MaxResults") + tags, next_token = self.paymentcryptography_backend.list_tags_for_resource( + resource_arn=resource_arn, + next_token=next_token, + max_results=max_results, + ) + # TODO: adjust response + return json.dumps(dict(tags=tags, nextToken=next_token)) diff --git a/moto/paymentcryptography/urls.py b/moto/paymentcryptography/urls.py index 994256175b77..bd4e2be47605 100644 --- a/moto/paymentcryptography/urls.py +++ b/moto/paymentcryptography/urls.py @@ -1,4 +1,5 @@ """paymentcryptography base URL and path.""" + from .responses import PaymentCryptographyControlPlaneResponse url_bases = [ @@ -6,4 +7,5 @@ ] url_paths = { + "{0}/$": PaymentCryptographyControlPlaneResponse.dispatch, } diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 53b88f8c6095..572ebc91cf62 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -1,14 +1,11 @@ """Unit tests for paymentcryptography-supported APIs.""" + from datetime import datetime import boto3 -import json -import pytest -from botocore.exceptions import ClientError from moto import mock_aws - KEY_ATTRIBUTES_1 = { "KeyUsage": "TR31_P0_PIN_ENCRYPTION_KEY", "KeyClass": "SYMMETRIC", @@ -34,15 +31,17 @@ def test_create_key(): key = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", MultiRegionKeyType="PRIMARY", )["Key"] - assert key["KeyArn"].startswith("arn:aws:payment-cryptography:us-east-1:1234567890:key/") + assert key["KeyArn"].startswith( + "arn:aws:payment-cryptography:us-east-1:1234567890:key/" + ) assert key["KeyAttributes"] == KEY_ATTRIBUTES_1 assert key["Exportable"] is True assert key["Enabled"] is True @@ -471,7 +470,6 @@ def test_create_key(): # assert updated_alias["AliasName"] == new_alias_name - # @mock_aws # def test_delete_alias(): # client = boto3.client("paymentcryptography", region_name="us-east-1") @@ -680,3 +678,27 @@ def test_create_key(): # # Retrieve the key and check its state # updated_key = client.get_key(KeyArn=key_arn)["Key"] # assert updated_key["KeyState"] == "DISABLED" + + +# @mock_aws +# def test_create_key(): +# client = boto3.client("payment-cryptography", region_name="ap-southeast-1") +# resp = client.create_key() + +# raise Exception("NotYetImplemented") + + +# @mock_aws +# def test_list_keys(): +# client = boto3.client("payment-cryptography", region_name="eu-west-1") +# resp = client.list_keys() + +# raise Exception("NotYetImplemented") + + +# @mock_aws +# def test_list_tags_for_resource(): +# client = boto3.client("payment-cryptography", region_name="us-east-2") +# resp = client.list_tags_for_resource() + +# raise Exception("NotYetImplemented") From 7cb1a84664cf89ac04b9346d3274bcd10b9d7f15 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:06:17 -0400 Subject: [PATCH 06/29] feat(payment-crypto): update create_key and test --- moto/paymentcryptography/models.py | 48 +++++++++---- moto/paymentcryptography/responses.py | 26 ++++--- .../test_paymentcryptography.py | 70 +++++++++---------- 3 files changed, 86 insertions(+), 58 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 015dbe8f0364..68e09cf9531c 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -2,7 +2,7 @@ import random import string -from datetime import datetime, timezone +from moto.core.utils import unix_time from typing import Any, Optional from moto.core.base_backend import BackendDict, BaseBackend @@ -27,8 +27,8 @@ def __init__( region_name: str, key_attributes: Optional[dict[str, Any]], key_check_value_algorithm: str, - exportable: bool, enabled: bool, + exportable: bool, tags: list[dict[str, str]], derive_key_usage: str, replication_regions: list[str], @@ -37,6 +37,7 @@ def __init__( self.key_arn = ( f"arn:aws:payment-cryptography:{region_name}:{account_id}:key/{self.key_id}" ) + now = unix_time() self.key_attributes = key_attributes self.key_check_value = _key_check_value() @@ -49,8 +50,8 @@ def __init__( # when a key is created, the origin is AWS_PAYMENT_CRYPTOGRAPHY. # If a key is imported, the origin is EXTERNAL. self.key_origin = "AWS_PAYMENT_CRYPTOGRAPHY" - self.create_time = datetime.now(timezone.utc) - self.usage_start_timestamp = self.create_time if enabled else None + self.create_timestamp = now + self.usage_start_timestamp = now if enabled else None self.usage_stop_timestamp = None self.delete_pending_timestamp = None self.delete_timestamp = None @@ -75,32 +76,48 @@ class PaymentCryptographyControlPlaneBackend(BaseBackend): def __init__(self, region_name, account_id): super().__init__(region_name, account_id) + self.keys: dict[str, Key] = {} def create_key( self, key_attributes: Optional[dict[str, Any]], key_check_value_algorithm: str, - exportable: bool, enabled: bool, + exportable: bool, tags: list[dict[str, str]], derive_key_usage: str, replication_regions: list[str], ) -> dict[str, Any]: key = Key( - key_attributes = self.key_attributes, - + account_id=self.account_id, + region_name=self.region_name, + key_attributes=key_attributes, + key_check_value_algorithm=key_check_value_algorithm, # TODO: match kcv to key_check_value_algorithm + exportable=exportable, + enabled=enabled, + tags=tags, + derive_key_usage=derive_key_usage, + replication_regions=replication_regions, ) - return { - - } - - - - + self.keys[key.key_arn] = key + if tags: + self.tag_resource(resource_arn=key.key_arn, tags=tags) + return { + "KeyArn": key.key_arn, + "KeyAttributes": key.key_attributes, + "KeyCheckValue": key.key_check_value, + "KeyCheckValueAlgorithm": key.key_check_value_algorithm, + "Enabled": key.enabled, + "Exportable": key.exportable, + "KeyState": key.key_state, + "KeyOrigin": key.key_origin, + "CreateTimestamp": key.create_timestamp, + "DeriveKeyUsage": key.derive_key_usage + } return key_arn def list_keys(self, key_state, next_token, max_results): @@ -111,6 +128,9 @@ def list_tags_for_resource(self, resource_arn, next_token, max_results): # implement here return tags, next_token + def tag_resource(self, resource_arn, tags: list[dict[str, str]]): + self.tagger.tag_resource(resource_arn, tags) + paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 4943f2b7c93c..10ff5f6cc460 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -4,7 +4,7 @@ from moto.core.responses import BaseResponse -from .models import paymentcryptography_backends +from .models import PaymentCryptographyControlPlaneBackend, paymentcryptography_backends class PaymentCryptographyControlPlaneResponse(BaseResponse): @@ -14,16 +14,12 @@ def __init__(self): super().__init__(service_name="paymentcryptography") @property - def paymentcryptography_backend(self): + def paymentcryptography_backend(self) -> PaymentCryptographyControlPlaneBackend: """Return backend instance specific for this region.""" - # TODO - # paymentcryptography_backends is not yet typed - # Please modify moto/backends.py to add the appropriate type annotations for this service return paymentcryptography_backends[self.current_account][self.region] - # add methods from here - def create_key(self): + def create_key(self) -> str: params = self._get_params() key_attributes = params.get("KeyAttributes") key_check_value_algorithm = params.get("KeyCheckValueAlgorithm") @@ -41,8 +37,9 @@ def create_key(self): derive_key_usage=derive_key_usage, replication_regions=replication_regions, ) - # TODO: adjust response - return json.dumps(dict(key=key)) + + + return json.dumps(dict(Key=key)) def list_keys(self): params = self._get_params() @@ -71,3 +68,14 @@ def list_tags_for_resource(self): ) # TODO: adjust response return json.dumps(dict(tags=tags, nextToken=next_token)) + + def tag_resource(self): + params = self._get_params() + resource_arn = params.get("ResourceArn") + tags = params.get("Tags") + self.paymentcryptography_backend.tag_resource( + resource_arn=resource_arn, + tags=tags, + ) + # TODO: adjust response + return json.dumps(dict()) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 572ebc91cf62..207217e3ad64 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -5,6 +5,7 @@ import boto3 from moto import mock_aws +from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID KEY_ATTRIBUTES_1 = { "KeyUsage": "TR31_P0_PIN_ENCRYPTION_KEY", @@ -26,7 +27,7 @@ @mock_aws def test_create_key(): - client = boto3.client("paymentcryptography", region_name="us-east-1") + client = boto3.client("payment-cryptography", region_name="us-east-1") key = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, @@ -36,11 +37,10 @@ def test_create_key(): {"Key": "Environment", "Value": "Test"}, ], DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", - MultiRegionKeyType="PRIMARY", )["Key"] assert key["KeyArn"].startswith( - "arn:aws:payment-cryptography:us-east-1:1234567890:key/" + f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/" ) assert key["KeyAttributes"] == KEY_ATTRIBUTES_1 assert key["Exportable"] is True @@ -55,7 +55,7 @@ def test_create_key(): # @mock_aws # def test_create_key_with_replica_regions(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # key = client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -83,7 +83,7 @@ def test_create_key(): # @mock_aws # def test_get_key(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # key = client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -114,7 +114,7 @@ def test_create_key(): # @mock_aws # def test_delete_key(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # key = client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -141,9 +141,9 @@ def test_create_key(): # @mock_aws # def test_get_key_replica(): -# primary_client = boto3.client("paymentcryptography", region_name="us-east-1") -# replica_client = boto3.client("paymentcryptography", region_name="us-west-2") -# secondary_client = boto3.client("paymentcryptography", region_name="eu-west-1") +# primary_client = boto3.client("payment-cryptography", region_name="us-east-1") +# replica_client = boto3.client("payment-cryptography", region_name="us-west-2") +# secondary_client = boto3.client("payment-cryptography", region_name="eu-west-1") # primary_arn = primary_client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -191,8 +191,8 @@ def test_create_key(): # @mock_aws # def test_list_keys(): -# primary_client = boto3.client("paymentcryptography", region_name="us-east-1") -# secondary_client = boto3.client("paymentcryptography", region_name="us-west-2") +# primary_client = boto3.client("payment-cryptography", region_name="us-east-1") +# secondary_client = boto3.client("payment-cryptography", region_name="us-west-2") # # Create multiple keys # for _ in range(5): @@ -239,7 +239,7 @@ def test_create_key(): # @mock_aws # def test_add_key_replication_regions(): -# primary_client = boto3.client("paymentcryptography", region_name="us-east-1") +# primary_client = boto3.client("payment-cryptography", region_name="us-east-1") # key = primary_client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -273,12 +273,12 @@ def test_create_key(): # "eu-west-1": {"Status": "SYNCHRONIZED"}, # } -# replica_client = boto3.client("paymentcryptography", region_name="us-west-2") +# replica_client = boto3.client("payment-cryptography", region_name="us-west-2") # replica_key = replica_client.get_key(KeyArn=f"arn:aws:payment-cryptography:us-west-2:1234567890:key/{key_id}")["Key"] # assert replica_key["MultiRegionKeyType"] == "REPLICA" # assert replica_key["PrimaryRegion"] == "us-east-1" -# replica_client_eu = boto3.client("paymentcryptography", region_name="eu-west-1") +# replica_client_eu = boto3.client("payment-cryptography", region_name="eu-west-1") # replica_key_eu = replica_client_eu.get_key(KeyArn=f"arn:aws:payment-cryptography:eu-west-1:1234567890:key/{key_id}")["Key"] # assert replica_key_eu["MultiRegionKeyType"] == "REPLICA" # assert replica_key_eu["PrimaryRegion"] == "us-east-1" @@ -286,7 +286,7 @@ def test_create_key(): # @mock_aws # def test_enable_default_key_replication_regions(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # # Enable default replication regions # key_replication_regions = client.enable_default_key_replication_regions( @@ -305,7 +305,7 @@ def test_create_key(): # @mock_aws # def test_get_default_key_replication_regions(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # default_regions = client.get_default_key_replication_regions()["EnabledReplicationRegions"] # assert "us-west-2" in default_regions @@ -314,7 +314,7 @@ def test_create_key(): # @mock_aws # def test_disable_default_key_replication_regions(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # # Disable default replication regions # key_replication_regions = client.disable_default_key_replication_regions( @@ -329,7 +329,7 @@ def test_create_key(): # @mock_aws # def test_tag_and_untag_key(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # key = client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -374,7 +374,7 @@ def test_create_key(): # @mock_aws # def test_create_alias(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # key = client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -404,7 +404,7 @@ def test_create_key(): # @mock_aws # def test_get_alias(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # key = client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -433,7 +433,7 @@ def test_create_key(): # @mock_aws # def test_update_alias(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # key = client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -472,7 +472,7 @@ def test_create_key(): # @mock_aws # def test_delete_alias(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # key = client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -504,7 +504,7 @@ def test_create_key(): # @mock_aws # def test_list_aliases(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # key1 = client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -548,7 +548,7 @@ def test_create_key(): # @mock_aws # def test_put_resource_policy(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # key = client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -587,7 +587,7 @@ def test_create_key(): # @mock_aws # def test_delete_resource_policy(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # key = client.create_key( # KeyAttributes=KEY_ATTRIBUTES_1, @@ -631,7 +631,7 @@ def test_create_key(): # @mock_aws # def test_start_key_usage(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # # Initially disabled # key = client.create_key( @@ -656,7 +656,7 @@ def test_create_key(): # @mock_aws # def test_stop_key_usage(): -# client = boto3.client("paymentcryptography", region_name="us-east-1") +# client = boto3.client("payment-cryptography", region_name="us-east-1") # # Initially enabled # key = client.create_key( @@ -680,14 +680,6 @@ def test_create_key(): # assert updated_key["KeyState"] == "DISABLED" -# @mock_aws -# def test_create_key(): -# client = boto3.client("payment-cryptography", region_name="ap-southeast-1") -# resp = client.create_key() - -# raise Exception("NotYetImplemented") - - # @mock_aws # def test_list_keys(): # client = boto3.client("payment-cryptography", region_name="eu-west-1") @@ -702,3 +694,11 @@ def test_create_key(): # resp = client.list_tags_for_resource() # raise Exception("NotYetImplemented") + + +# @mock_aws +# def test_tag_resource(): +# client = boto3.client("payment-cryptography", region_name="ap-southeast-1") +# resp = client.tag_resource() + +# raise Exception("NotYetImplemented") From 7315db8030dbf13919d07c762bfccd8de8699329 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:15:33 -0400 Subject: [PATCH 07/29] feat(payment-crypto): fix models and responses for create_key --- moto/paymentcryptography/models.py | 22 +++++++++++++++++-- moto/paymentcryptography/responses.py | 31 +++++++++++++-------------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 68e09cf9531c..55eecb67af37 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -7,6 +7,7 @@ from moto.core.base_backend import BackendDict, BaseBackend from moto.core.common_models import BaseModel +from moto.utilities.tagging_service import TaggingService def _random_key_id() -> str: @@ -19,6 +20,17 @@ def _key_check_value() -> str: kcv = "".join(random.choices("0123456789ABCDEF", k=6)) return kcv +def _key_check_value_algorithm(key_algorithm: str) -> str: + # TDES keys use ANSI X9.24, AES keys use CMAC, HMAC keys use the HMAC hash, + # and asymmetric (RSA/ECC) keys use SHA-1. + if key_algorithm.startswith("TDES"): + return "ANSI_X9_24" + if key_algorithm.startswith("AES"): + return "CMAC" + if key_algorithm.startswith("HMAC"): + return "HMAC" + return "SHA_1" + class Key(BaseModel): def __init__( @@ -39,9 +51,13 @@ def __init__( ) now = unix_time() self.key_attributes = key_attributes + key_attributes_key_algorithm = key_attributes.get("KeyAlgorithm") self.key_check_value = _key_check_value() - self.key_check_value_algorithm = key_check_value_algorithm + if key_check_value_algorithm: + self.key_check_value_algorithm = key_check_value_algorithm + else: + self.key_check_value_algorithm = _key_check_value_algorithm(key_attributes_key_algorithm) self.exportable = exportable self.enabled = enabled @@ -77,6 +93,9 @@ class PaymentCryptographyControlPlaneBackend(BaseBackend): def __init__(self, region_name, account_id): super().__init__(region_name, account_id) self.keys: dict[str, Key] = {} + self.tagger = TaggingService( + tag_name="Tags", key_name="Key", value_name="Value" + ) def create_key( self, @@ -118,7 +137,6 @@ def create_key( "CreateTimestamp": key.create_timestamp, "DeriveKeyUsage": key.derive_key_usage } - return key_arn def list_keys(self, key_state, next_token, max_results): # implement here diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 10ff5f6cc460..81f67aeb5802 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -20,14 +20,13 @@ def paymentcryptography_backend(self) -> PaymentCryptographyControlPlaneBackend: def create_key(self) -> str: - params = self._get_params() - key_attributes = params.get("KeyAttributes") - key_check_value_algorithm = params.get("KeyCheckValueAlgorithm") - exportable = params.get("Exportable") - enabled = params.get("Enabled") - tags = params.get("Tags") - derive_key_usage = params.get("DeriveKeyUsage") - replication_regions = params.get("ReplicationRegions") + key_attributes = self._get_param("KeyAttributes") + key_check_value_algorithm = self._get_param("KeyCheckValueAlgorithm") + exportable = self._get_param("Exportable") + enabled = self._get_param("Enabled") + tags = self._get_param("Tags") + derive_key_usage = self._get_param("DeriveKeyUsage") + replication_regions = self._get_param("ReplicationRegions") key = self.paymentcryptography_backend.create_key( key_attributes=key_attributes, key_check_value_algorithm=key_check_value_algorithm, @@ -43,9 +42,9 @@ def create_key(self) -> str: def list_keys(self): params = self._get_params() - key_state = params.get("KeyState") - next_token = params.get("NextToken") - max_results = params.get("MaxResults") + key_state = self._get_param("KeyState") + next_token = self._get_param("NextToken") + max_results = self._get_param("MaxResults") keys, next_token = self.paymentcryptography_backend.list_keys( key_state=key_state, next_token=next_token, @@ -58,9 +57,9 @@ def list_keys(self): def list_tags_for_resource(self): params = self._get_params() - resource_arn = params.get("ResourceArn") - next_token = params.get("NextToken") - max_results = params.get("MaxResults") + resource_arn = self._get_param("ResourceArn") + next_token = self._get_param("NextToken") + max_results = self._get_param("MaxResults") tags, next_token = self.paymentcryptography_backend.list_tags_for_resource( resource_arn=resource_arn, next_token=next_token, @@ -71,8 +70,8 @@ def list_tags_for_resource(self): def tag_resource(self): params = self._get_params() - resource_arn = params.get("ResourceArn") - tags = params.get("Tags") + resource_arn = self._get_param("ResourceArn") + tags = self._get_param("Tags") self.paymentcryptography_backend.tag_resource( resource_arn=resource_arn, tags=tags, From da41e0a79a7f32358b04e3c5ee1428ebd7a80c85 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:40:39 -0400 Subject: [PATCH 08/29] feat(paymentcrypto): add create_key and get_key implementation and tests --- moto/paymentcryptography/exceptions.py | 4 ++ moto/paymentcryptography/models.py | 67 +++++++++++++++++--------- moto/paymentcryptography/responses.py | 12 +++-- 3 files changed, 56 insertions(+), 27 deletions(-) diff --git a/moto/paymentcryptography/exceptions.py b/moto/paymentcryptography/exceptions.py index 991371891847..9ec7b5512140 100644 --- a/moto/paymentcryptography/exceptions.py +++ b/moto/paymentcryptography/exceptions.py @@ -1,3 +1,7 @@ """Exceptions raised by the paymentcryptography service.""" from moto.core.exceptions import JsonRESTError + +class ResourceNotFoundException(JsonRESTError): + def __init__(self, msg: str): + super().__init__("ResourceNotFoundException", f"{msg}") diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 55eecb67af37..d59826d2729b 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -9,6 +9,8 @@ from moto.core.common_models import BaseModel from moto.utilities.tagging_service import TaggingService +from .exceptions import ResourceNotFoundException + def _random_key_id() -> str: chars = string.ascii_lowercase + string.digits @@ -38,12 +40,12 @@ def __init__( account_id: str, region_name: str, key_attributes: Optional[dict[str, Any]], - key_check_value_algorithm: str, + key_check_value_algorithm: Optional[str], enabled: bool, exportable: bool, tags: list[dict[str, str]], derive_key_usage: str, - replication_regions: list[str], + replication_regions: Optional[list[str]], ): self.key_id = _random_key_id() self.key_arn = ( @@ -73,19 +75,41 @@ def __init__( self.delete_timestamp = None self.derive_key_usage = derive_key_usage + self.multi_region_key_type: Optional[str] = None + self.primary_region: Optional[str] = None + self.replication_status: Optional[dict] = None + self.using_default_replication_regions: Optional[bool] = None + if replication_regions is not None: - self.multi_region_key_type = "PRIMARY" self.primary_region = region_name - self.replication_status = dict() - for region in replication_regions: - self.replication_status[region] = { - "replication_status": "SYNCHRONIZED", - "status_message": "Key is synchronized across regions.", - } + self.multi_region_key_type = "PRIMARY" + self.replication_status = { + region: {"Status": "SYNCHRONIZED"} for region in replication_regions + } self.using_default_replication_regions = False + # TODO: add missing mpa details + self.tags = tags or [] + def to_dict(self) -> dict[str, Any]: + return { + "KeyArn": self.key_arn, + "KeyAttributes": self.key_attributes, + "KeyCheckValue": self.key_check_value, + "KeyCheckValueAlgorithm": self.key_check_value_algorithm, + "Enabled": self.enabled, + "Exportable": self.exportable, + "KeyState": self.key_state, + "KeyOrigin": self.key_origin, + "CreateTimestamp": self.create_timestamp, + "DeriveKeyUsage": self.derive_key_usage, + "MultiRegionKeyType": self.multi_region_key_type, + "PrimaryRegion": self.primary_region, + "ReplicationStatus": self.replication_status, + "UsingDefaultReplicationRegions": self.using_default_replication_regions + } + class PaymentCryptographyControlPlaneBackend(BaseBackend): """Implementation of PaymentCryptographyControlPlane APIs.""" @@ -125,22 +149,10 @@ def create_key( if tags: self.tag_resource(resource_arn=key.key_arn, tags=tags) - return { - "KeyArn": key.key_arn, - "KeyAttributes": key.key_attributes, - "KeyCheckValue": key.key_check_value, - "KeyCheckValueAlgorithm": key.key_check_value_algorithm, - "Enabled": key.enabled, - "Exportable": key.exportable, - "KeyState": key.key_state, - "KeyOrigin": key.key_origin, - "CreateTimestamp": key.create_timestamp, - "DeriveKeyUsage": key.derive_key_usage - } + return key.to_dict() - def list_keys(self, key_state, next_token, max_results): - # implement here - return keys, next_token + def list_keys(self, next_token, max_results): + return self.keys, next_token def list_tags_for_resource(self, resource_arn, next_token, max_results): # implement here @@ -149,6 +161,13 @@ def list_tags_for_resource(self, resource_arn, next_token, max_results): def tag_resource(self, resource_arn, tags: list[dict[str, str]]): self.tagger.tag_resource(resource_arn, tags) + def get_key(self, key_identifier): + if key_identifier not in self.keys: + raise ResourceNotFoundException(key_identifier) + + key = self.keys.get(key_identifier) + return key.to_dict() + paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 81f67aeb5802..43d3bf145fc5 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -11,7 +11,7 @@ class PaymentCryptographyControlPlaneResponse(BaseResponse): """Handler for PaymentCryptographyControlPlane requests and responses.""" def __init__(self): - super().__init__(service_name="paymentcryptography") + super().__init__(service_name="payment-cryptography") @property def paymentcryptography_backend(self) -> PaymentCryptographyControlPlaneBackend: @@ -41,7 +41,6 @@ def create_key(self) -> str: return json.dumps(dict(Key=key)) def list_keys(self): - params = self._get_params() key_state = self._get_param("KeyState") next_token = self._get_param("NextToken") max_results = self._get_param("MaxResults") @@ -50,7 +49,7 @@ def list_keys(self): next_token=next_token, max_results=max_results, ) - # TODO: adjust response + return json.dumps(dict(keys=keys, nextToken=next_token)) # add templates from here @@ -78,3 +77,10 @@ def tag_resource(self): ) # TODO: adjust response return json.dumps(dict()) + + def get_key(self): + key_identifier = self._get_param("KeyIdentifier") + key = self.paymentcryptography_backend.get_key( + key_identifier=key_identifier, + ) + return json.dumps(dict(key=key)) From f99cfd8f03c28f4e1d3e75b45337fbe1786eab5a Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:57:57 -0400 Subject: [PATCH 09/29] feat(paymentcrypto): update test --- .../test_paymentcryptography.py | 104 +++++++++--------- tests/test_paymentcryptography/test_server.py | 13 --- 2 files changed, 53 insertions(+), 64 deletions(-) delete mode 100644 tests/test_paymentcryptography/test_server.py diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 207217e3ad64..db2e7b4ebc42 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -53,63 +53,62 @@ def test_create_key(): assert isinstance(key["CreateTimestamp"], datetime) -# @mock_aws -# def test_create_key_with_replica_regions(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") - -# key = client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# ReplicationRegions=[ -# "us-west-2", -# "eu-west-1", -# ] -# )["Key"] - -# assert key["MultiRegionKeyType"] == "PRIMARY" -# assert key["PrimaryRegion"] == "us-east-1" -# assert key["UsingDefaultReplicationRegions"] is False -# assert key["ReplicationStatus"] == { -# "us-west-2": {"Status": "SYNCHRONIZED"}, -# "eu-west-1": {"Status": "SYNCHRONIZED"}, -# } +@mock_aws +def test_create_key_with_replica_regions(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", + ReplicationRegions=[ + "us-west-2", + "eu-west-1", + ] + )["Key"] -# @mock_aws -# def test_get_key(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") + assert key["MultiRegionKeyType"] == "PRIMARY" + assert key["PrimaryRegion"] == "us-east-1" + assert key["UsingDefaultReplicationRegions"] is False + assert key["ReplicationStatus"] == { + "us-west-2": {"Status": "SYNCHRONIZED"}, + "eu-west-1": {"Status": "SYNCHRONIZED"}, + } -# key = client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# )["Key"] -# key_arn = key["KeyArn"] +@mock_aws +def test_get_key(): + client = boto3.client("payment-cryptography", region_name="us-east-1") -# retrieved_key = client.get_key(KeyArn=key_arn)["Key"] + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + ) -# assert retrieved_key["KeyArn"] == key_arn -# assert key["KeyAttributes"] == KEY_ATTRIBUTES_1 -# assert key["Exportable"] is True -# assert key["Enabled"] is True -# assert key["KeyCheckValue"] is not None -# assert key["KeyCheckValueAlgorithm"] is not None -# assert key["KeyState"] == "CREATE_COMPLETE" -# assert key["KeyOrigin"] == "AWS_PAYMENT_CRYPTOGRAPHY" -# assert key["DeriveKeyUsage"] == "TR31_P0_PIN_ENCRYPTION_KEY" -# assert isinstance(key["CreateTimestamp"], datetime) + key_arn = key["KeyArn"] + retrieved_key = client.get_key(KeyIdentifier=key_arn) + # retrieved_key = retrieved_json.get("Key") + + assert retrieved_key + assert retrieved_key["KeyArn"] == key_arn + assert retrieved_key["KeyAttributes"] == KEY_ATTRIBUTES_1 + assert retrieved_key["Exportable"] is True + assert retrieved_key["Enabled"] is True + assert retrieved_key["KeyCheckValue"] is not None + assert retrieved_key["KeyCheckValueAlgorithm"] is not None + assert retrieved_key["KeyState"] == "CREATE_COMPLETE" + assert retrieved_key["KeyOrigin"] == "AWS_PAYMENT_CRYPTOGRAPHY" + assert retrieved_key["DeriveKeyUsage"] == "TR31_P0_PIN_ENCRYPTION_KEY" + assert isinstance(key["CreateTimestamp"], datetime) # @mock_aws @@ -702,3 +701,6 @@ def test_create_key(): # resp = client.tag_resource() # raise Exception("NotYetImplemented") + + + diff --git a/tests/test_paymentcryptography/test_server.py b/tests/test_paymentcryptography/test_server.py deleted file mode 100644 index 9557f707903b..000000000000 --- a/tests/test_paymentcryptography/test_server.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Test different server responses.""" - -import moto.server as server - - -def test_paymentcryptography_list(): - backend = server.create_backend_app("payment-cryptography") - test_client = backend.test_client() - - resp = test_client.get("/") - - assert resp.status_code == 200 - assert "?" in str(resp.data) From 5bcf61809f830968f742c38efb866d0ae40be62e Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:02:57 -0400 Subject: [PATCH 10/29] feat(paymentcrypto): fix get_key test and response --- moto/paymentcryptography/responses.py | 2 +- .../test_paymentcryptography.py | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 43d3bf145fc5..d4a7107305f3 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -83,4 +83,4 @@ def get_key(self): key = self.paymentcryptography_backend.get_key( key_identifier=key_identifier, ) - return json.dumps(dict(key=key)) + return json.dumps(dict(Key=key)) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index db2e7b4ebc42..24f93e928106 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -92,13 +92,12 @@ def test_get_key(): {"Key": "Environment", "Value": "Test"}, ], DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" - ) + )["Key"] key_arn = key["KeyArn"] - retrieved_key = client.get_key(KeyIdentifier=key_arn) + retrieved_key = client.get_key(KeyIdentifier=key_arn)["Key"] # retrieved_key = retrieved_json.get("Key") - assert retrieved_key assert retrieved_key["KeyArn"] == key_arn assert retrieved_key["KeyAttributes"] == KEY_ATTRIBUTES_1 assert retrieved_key["Exportable"] is True @@ -122,8 +121,7 @@ def test_get_key(): # Tags = [ # {"Key": "Environment", "Value": "Test"}, # ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", +# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" # )["Key"] # key_arn = key["KeyArn"] @@ -701,6 +699,3 @@ def test_get_key(): # resp = client.tag_resource() # raise Exception("NotYetImplemented") - - - From dcb806d7c58996e51845aae01253e12395f3607a Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:31:06 -0400 Subject: [PATCH 11/29] feat(paymentcrypto): single region list_key --- moto/paymentcryptography/models.py | 7 +++-- moto/paymentcryptography/responses.py | 2 +- .../test_paymentcryptography.py | 29 +++++++++++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index d59826d2729b..5ccc740300b9 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -151,8 +151,11 @@ def create_key( return key.to_dict() - def list_keys(self, next_token, max_results): - return self.keys, next_token + def list_keys(self, key_state, next_token, max_results): + keys = list(self.keys.values()) + if key_state: + keys = [key for key in keys if key.key_state == key_state] + return [key.to_dict() for key in keys], next_token def list_tags_for_resource(self, resource_arn, next_token, max_results): # implement here diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index d4a7107305f3..5ba1668ac854 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -50,7 +50,7 @@ def list_keys(self): max_results=max_results, ) - return json.dumps(dict(keys=keys, nextToken=next_token)) + return json.dumps(dict(Keys=keys, NextToken=next_token)) # add templates from here diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 24f93e928106..d68202fb69ff 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -677,13 +677,32 @@ def test_get_key(): # assert updated_key["KeyState"] == "DISABLED" -# @mock_aws -# def test_list_keys(): -# client = boto3.client("payment-cryptography", region_name="eu-west-1") -# resp = client.list_keys() +@mock_aws +def test_list_keys_single_region(): + client = boto3.client("payment-cryptography", region_name="us-east-1") -# raise Exception("NotYetImplemented") + client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled=True, + Tags=[ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", + ) + + list_key_resp = client.list_keys()["Keys"] + key = list_key_resp[0] + assert len(list_key_resp) == 1 + assert key["KeyArn"].startswith( + f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/" + ) + assert key["KeyAttributes"] == KEY_ATTRIBUTES_1 + assert key["Exportable"] is True + assert key["Enabled"] is True + assert key["KeyCheckValue"] is not None + assert key["KeyState"] == "CREATE_COMPLETE" # @mock_aws # def test_list_tags_for_resource(): From 431a874157f185a0d942f713f51dc8907ddf364b Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:43:43 -0400 Subject: [PATCH 12/29] feat(paymentcrypto): add list_tags_for_resource --- moto/paymentcryptography/models.py | 2 +- moto/paymentcryptography/responses.py | 4 +--- .../test_paymentcryptography.py | 22 ++++++++++++++----- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 5ccc740300b9..a0b8797a80ca 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -158,7 +158,7 @@ def list_keys(self, key_state, next_token, max_results): return [key.to_dict() for key in keys], next_token def list_tags_for_resource(self, resource_arn, next_token, max_results): - # implement here + tags = self.tagger.list_tags_for_resource(resource_arn)["Tags"] return tags, next_token def tag_resource(self, resource_arn, tags: list[dict[str, str]]): diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 5ba1668ac854..3bb0351d2e3e 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -55,7 +55,6 @@ def list_keys(self): # add templates from here def list_tags_for_resource(self): - params = self._get_params() resource_arn = self._get_param("ResourceArn") next_token = self._get_param("NextToken") max_results = self._get_param("MaxResults") @@ -64,8 +63,7 @@ def list_tags_for_resource(self): next_token=next_token, max_results=max_results, ) - # TODO: adjust response - return json.dumps(dict(tags=tags, nextToken=next_token)) + return json.dumps(dict(Tags=tags, NextToken=next_token)) def tag_resource(self): params = self._get_params() diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index d68202fb69ff..244d29be8abd 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -704,12 +704,24 @@ def test_list_keys_single_region(): assert key["KeyCheckValue"] is not None assert key["KeyState"] == "CREATE_COMPLETE" -# @mock_aws -# def test_list_tags_for_resource(): -# client = boto3.client("payment-cryptography", region_name="us-east-2") -# resp = client.list_tags_for_resource() +@mock_aws +def test_list_tags_for_resource(): + client = boto3.client("payment-cryptography", region_name="us-east-1") -# raise Exception("NotYetImplemented") + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled=True, + Tags=[ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", + )["Key"] + + list_tags_resp = client.list_tags_for_resource(ResourceArn=key["KeyArn"]) + assert list_tags_resp['Tags'] == [ + {"Key": "Environment", "Value": "Test"}, + ] # @mock_aws From 914178a847e4e551182c626cfc72fd9e46486f7b Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:55:56 -0400 Subject: [PATCH 13/29] feat(paymentcrypto): add tag and untag resource --- moto/paymentcryptography/models.py | 3 + moto/paymentcryptography/responses.py | 11 +- .../test_paymentcryptography.py | 111 ++++++++++-------- 3 files changed, 74 insertions(+), 51 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index a0b8797a80ca..366c7e58d9de 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -171,6 +171,9 @@ def get_key(self, key_identifier): key = self.keys.get(key_identifier) return key.to_dict() + def untag_resource(self, resource_arn, tag_keys): + self.tagger.untag_resource_using_names(resource_arn, tag_keys) + paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 3bb0351d2e3e..de908b184006 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -73,7 +73,6 @@ def tag_resource(self): resource_arn=resource_arn, tags=tags, ) - # TODO: adjust response return json.dumps(dict()) def get_key(self): @@ -82,3 +81,13 @@ def get_key(self): key_identifier=key_identifier, ) return json.dumps(dict(Key=key)) + + def untag_resource(self): + resource_arn = self._get_param("ResourceArn") + tag_keys = self._get_param("TagKeys") + self.paymentcryptography_backend.untag_resource( + resource_arn=resource_arn, + tag_keys=tag_keys, + ) + + return json.dumps(dict()) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 244d29be8abd..ba038efc63e1 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -324,51 +324,6 @@ def test_get_key(): # assert "us-west-2" not in key_replication_regions["EnabledReplicationRegions"] -# @mock_aws -# def test_tag_and_untag_key(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") - -# key = client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# )["Key"] - -# key_arn = key["KeyArn"] - -# # Tag the key with additional tags -# client.tag_resource( -# ResourceArn=key_arn, -# Tags=[ -# {"Key": "foo", "Value": "1"}, -# {"Key": "bar", "Value": "2"}, -# ] -# ) - -# # Retrieve the key and check the tags with list_tags_for_resource -# retrieved_key = client.list_tags_for_resource(ResourceArn=key_arn)["Tags"] -# assert len(retrieved_key) == 3 # Original tag + 2 new tags -# assert {"Key": "Environment", "Value": "Test"} in retrieved_key -# assert {"Key": "foo", "Value": "1"} in retrieved_key -# assert {"Key": "bar", "Value": "2"} in retrieved_key - -# # Untag the key -# client.untag_resource( -# ResourceArn=key_arn, -# TagKeys=["foo", "bar"] -# ) - -# # Retrieve the key and check the tags again -# retrieved_key_after_untag = client.list_tags_for_resource(ResourceArn=key_arn)["Tags"] -# assert len(retrieved_key_after_untag) == 1 # Only the original tag should remain -# assert {"Key": "Environment", "Value": "Test"} in retrieved_key_after_untag - - # @mock_aws # def test_create_alias(): # client = boto3.client("payment-cryptography", region_name="us-east-1") @@ -723,10 +678,66 @@ def test_list_tags_for_resource(): {"Key": "Environment", "Value": "Test"}, ] +@mock_aws +def test_tag_resource(): + client = boto3.client("payment-cryptography", region_name="us-east-1") -# @mock_aws -# def test_tag_resource(): -# client = boto3.client("payment-cryptography", region_name="ap-southeast-1") -# resp = client.tag_resource() + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + )["Key"] + + key_arn = key["KeyArn"] + + # Tag the key with additional tags + client.tag_resource( + ResourceArn=key_arn, + Tags=[ + {"Key": "foo", "Value": "1"}, + {"Key": "bar", "Value": "2"}, + ] + ) + + # Retrieve the key and check the tags with list_tags_for_resource + retrieved_key = client.list_tags_for_resource(ResourceArn=key_arn)["Tags"] + assert len(retrieved_key) == 3 # Original tag + 2 new tags + assert {"Key": "Environment", "Value": "Test"} in retrieved_key + assert {"Key": "foo", "Value": "1"} in retrieved_key + assert {"Key": "bar", "Value": "2"} in retrieved_key + + + + +@mock_aws +def test_untag_resource(): + client = boto3.client("payment-cryptography", region_name="ap-southeast-1") + + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + {"Key": "foo", "Value": "1"}, + {"Key": "bar", "Value": "2"} + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + )["Key"] + + key_arn = key["KeyArn"] + + # Untag the key + client.untag_resource( + ResourceArn=key_arn, + TagKeys=["foo", "bar"] + ) -# raise Exception("NotYetImplemented") + # Retrieve the key and check the tags + retrieved_key_after_untag = client.list_tags_for_resource(ResourceArn=key_arn)["Tags"] + assert len(retrieved_key_after_untag) == 1 + assert {"Key": "Environment", "Value": "Test"} in retrieved_key_after_untag From 8d631cb600e953cfd5b4f29c86ee443231642081 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:01:18 -0400 Subject: [PATCH 14/29] feat(paymentcrypto): add delete_key --- moto/paymentcryptography/models.py | 19 +++++- moto/paymentcryptography/responses.py | 9 +++ .../test_paymentcryptography.py | 66 +++++++++++-------- 3 files changed, 67 insertions(+), 27 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 366c7e58d9de..5bcb03ab77ff 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -93,7 +93,7 @@ def __init__( self.tags = tags or [] def to_dict(self) -> dict[str, Any]: - return { + result = { "KeyArn": self.key_arn, "KeyAttributes": self.key_attributes, "KeyCheckValue": self.key_check_value, @@ -109,6 +109,11 @@ def to_dict(self) -> dict[str, Any]: "ReplicationStatus": self.replication_status, "UsingDefaultReplicationRegions": self.using_default_replication_regions } + if self.delete_pending_timestamp is not None: + result["DeletePendingTimestamp"] = self.delete_pending_timestamp + if self.delete_timestamp is not None: + result["DeletePendingTimestamp"] = self.delete_timestamp + return result class PaymentCryptographyControlPlaneBackend(BaseBackend): @@ -174,6 +179,18 @@ def get_key(self, key_identifier): def untag_resource(self, resource_arn, tag_keys): self.tagger.untag_resource_using_names(resource_arn, tag_keys) + def delete_key(self, key_identifier, delete_key_in_days): + if key_identifier not in self.keys: + raise ResourceNotFoundException(key_identifier) + + key = self.keys[key_identifier] + days = delete_key_in_days if delete_key_in_days is not None else 7 + key.key_state = "DELETE_PENDING" + key.enabled = False + key.usage_stop_timestamp = unix_time() + key.delete_pending_timestamp = unix_time() + days *86400 + return key.to_dict() + paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index de908b184006..7f0c3e35df09 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -91,3 +91,12 @@ def untag_resource(self): ) return json.dumps(dict()) + + def delete_key(self): + key_identifier = self._get_param("KeyIdentifier") + delete_key_in_days = self._get_param("DeleteKeyInDays") + key = self.paymentcryptography_backend.delete_key( + key_identifier=key_identifier, + delete_key_in_days=delete_key_in_days, + ) + return json.dumps(dict(Key=key)) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index ba038efc63e1..9f82a608497b 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -3,6 +3,8 @@ from datetime import datetime import boto3 +import pytest +from botocore.exceptions import ClientError from moto import mock_aws from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID @@ -110,32 +112,6 @@ def test_get_key(): assert isinstance(key["CreateTimestamp"], datetime) -# @mock_aws -# def test_delete_key(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") - -# key = client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" -# )["Key"] - -# key_arn = key["KeyArn"] - -# # Delete the key -# client.delete_key(KeyArn=key_arn) - -# # Check that the key no longer exists -# with pytest.raises(ClientError) as exc: -# client.get_key(KeyArn=key_arn) -# err = exc.value.response["Error"] -# assert err["Code"] == "ResourceNotFoundException" - - # @mock_aws # def test_get_key_replica(): # primary_client = boto3.client("payment-cryptography", region_name="us-east-1") @@ -741,3 +717,41 @@ def test_untag_resource(): retrieved_key_after_untag = client.list_tags_for_resource(ResourceArn=key_arn)["Tags"] assert len(retrieved_key_after_untag) == 1 assert {"Key": "Environment", "Value": "Test"} in retrieved_key_after_untag + + +@mock_aws +def test_delete_key(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled=True, + Tags=[ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + )["Key"] + + key_arn = key["KeyArn"] + + # Delete the key + deleted = client.delete_key(KeyIdentifier=key_arn)["Key"] + assert deleted["KeyState"] == "DELETE_PENDING" + assert deleted["Enabled"] is False + assert isinstance(deleted["DeletePendingTimestamp"], datetime) + + retrieved = client.get_key(KeyIdentifier=key_arn)["Key"] + assert retrieved["KeyState"] == "DELETE_PENDING" + assert retrieved["Enabled"] is False + + +@mock_aws +def test_delete_key_not_found(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist" + with pytest.raises(ClientError) as exc: + client.delete_key(KeyIdentifier=missing_arn) + assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException" + From 50f2f20dd419e94685cc5a75d5015d50c6aede44 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:40:00 -0400 Subject: [PATCH 15/29] feat(paymentcrypto): add resource_policy actions --- moto/paymentcryptography/models.py | 23 ++ moto/paymentcryptography/responses.py | 23 ++ .../test_paymentcryptography.py | 213 ++++++++++-------- 3 files changed, 159 insertions(+), 100 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 5bcb03ab77ff..61ffcc58322e 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -122,6 +122,7 @@ class PaymentCryptographyControlPlaneBackend(BaseBackend): def __init__(self, region_name, account_id): super().__init__(region_name, account_id) self.keys: dict[str, Key] = {} + self.resource_policies: dict[str, str] = {} self.tagger = TaggingService( tag_name="Tags", key_name="Key", value_name="Value" ) @@ -191,6 +192,28 @@ def delete_key(self, key_identifier, delete_key_in_days): key.delete_pending_timestamp = unix_time() + days *86400 return key.to_dict() + def put_resource_policy(self, resource_arn, policy): + if resource_arn not in self.keys: + raise ResourceNotFoundException(resource_arn) + self.resource_policies[resource_arn] = policy + return { + "Policy": policy, + "ResourceArn": resource_arn, + } + + def get_resource_policy(self, resource_arn): + if resource_arn not in self.keys: + raise ResourceNotFoundException(resource_arn) + return { + "Policy": self.resource_policies.get(resource_arn, "{}"), + "ResourceArn": resource_arn, + } + + def delete_resource_policy(self, resource_arn): + if resource_arn not in self.keys: + raise ResourceNotFoundException(resource_arn) + self.resource_policies.pop(resource_arn, None) + paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 7f0c3e35df09..13300407365f 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -100,3 +100,26 @@ def delete_key(self): delete_key_in_days=delete_key_in_days, ) return json.dumps(dict(Key=key)) + + def put_resource_policy(self): + resource_arn = self._get_param("ResourceArn") + policy = self._get_param("Policy") + result = self.paymentcryptography_backend.put_resource_policy( + resource_arn=resource_arn, + policy=policy, + ) + return json.dumps(result) + + def get_resource_policy(self): + resource_arn = self._get_param("ResourceArn") + policy = self.paymentcryptography_backend.get_resource_policy( + resource_arn=resource_arn, + ) + return json.dumps(policy) + + def delete_resource_policy(self): + resource_arn = self._get_param("ResourceArn") + self.paymentcryptography_backend.delete_resource_policy( + resource_arn=resource_arn, + ) + return json.dumps(dict()) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 9f82a608497b..467ecefb8ed0 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -3,8 +3,9 @@ from datetime import datetime import boto3 -import pytest from botocore.exceptions import ClientError +import json +import pytest from moto import mock_aws from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID @@ -26,6 +27,26 @@ }, } +def _sample_policy(key_arn): + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowCrossAccountKeyUsage", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam:123456789012:role/PaymentProcessingRole" + }, + "Action": [ + "payment-cryptography:GetKey", + "payment-cryptography:EncryptData", + "payment-cryptography:DecryptData", + ], + "Resource": key_arn, + } + ] + } + return json.dumps(policy) @mock_aws def test_create_key(): @@ -112,104 +133,6 @@ def test_get_key(): assert isinstance(key["CreateTimestamp"], datetime) -# @mock_aws -# def test_get_key_replica(): -# primary_client = boto3.client("payment-cryptography", region_name="us-east-1") -# replica_client = boto3.client("payment-cryptography", region_name="us-west-2") -# secondary_client = boto3.client("payment-cryptography", region_name="eu-west-1") - -# primary_arn = primary_client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# ReplicationRegions=[ -# "us-west-2", -# "eu-west-1", -# ] -# )["Key"]["KeyArn"] - -# primary_key = primary_client.get_key(KeyArn=primary_arn)["Key"] - -# # The replica shares the primary's key id, only the region differs. -# key_id = primary_arn.split(":key/")[1] -# replica_arn = f"arn:aws:payment-cryptography:us-west-2:1234567890:key/{key_id}" -# replica_key = replica_client.get_key(KeyArn=replica_arn)["Key"] -# secondary_arn = f"arn:aws:payment-cryptography:eu-west-1:1234567890:key/{key_id}" -# secondary_key = secondary_client.get_key(KeyArn=secondary_arn)["Key"] - - -# assert primary_key["MultiRegionKeyType"] == "PRIMARY" -# assert primary_key["PrimaryRegion"] == "us-east-1" -# assert primary_key["UsingDefaultReplicationRegions"] is True -# assert primary_key["ReplicationStatus"] == { -# "us-west-2": {"Status": "SYNCHRONIZED"}, -# "eu-west-1": {"Status": "SYNCHRONIZED"}, -# } -# assert replica_key["MultiRegionKeyType"] == "REPLICA" -# assert replica_key["PrimaryRegion"] == "us-east-1" -# # Check that replica does not have the keys for UsingDefaultReplicationRegions and ReplicationStatus -# assert "UsingDefaultReplicationRegions" not in replica_key -# assert "ReplicationStatus" not in replica_key -# assert secondary_key["MultiRegionKeyType"] == "REPLICA" -# assert secondary_key["PrimaryRegion"] == "us-east-1" -# # Check that secondary replica does not have the keys for UsingDefaultReplicationRegions and ReplicationStatus -# assert "UsingDefaultReplicationRegions" not in secondary_key -# assert "ReplicationStatus" not in secondary_key - - -# @mock_aws -# def test_list_keys(): -# primary_client = boto3.client("payment-cryptography", region_name="us-east-1") -# secondary_client = boto3.client("payment-cryptography", region_name="us-west-2") - -# # Create multiple keys -# for _ in range(5): -# primary_client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# ) - -# keys_list = primary_client.list_keys()["Keys"] -# assert len(keys_list) == 5 -# assert keys_list[0]["KeyArn"].startswith("arn:aws:payment-cryptography:us-east-1:1234567890:key/") -# assert keys_list[0]["KeyState"] == "CREATE_COMPLETE" - -# # Create keys with replicas -# for _ in range(2): -# primary_client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# ReplicationRegions=[ -# "us-west-2", -# "eu-west-1", -# ] -# ) - -# keys_list_primary = primary_client.list_keys()["Keys"] -# keys_list_secondary = secondary_client.list_keys()["Keys"] -# assert len(keys_list_primary) == 7 # 5 + 2 -# assert len(keys_list_secondary) == 2 # Only the replicas created in us-west-2 -# assert keys_list_primary[5]["MultiRegionKeyType"] == "PRIMARY" -# assert keys_list_secondary[0]["MultiRegionKeyType"] == "REPLICA" - - # @mock_aws # def test_add_key_replication_regions(): # primary_client = boto3.client("payment-cryptography", region_name="us-east-1") @@ -750,8 +673,98 @@ def test_delete_key(): def test_delete_key_not_found(): client = boto3.client("payment-cryptography", region_name="us-east-1") - missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist" + missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" with pytest.raises(ClientError) as exc: client.delete_key(KeyIdentifier=missing_arn) assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException" + +@mock_aws +def test_put_resource_policy(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled=True, + Tags=[ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", + )["Key"] + + key_arn = key["KeyArn"] + + put_resp = client.put_resource_policy(ResourceArn=key_arn, Policy=_sample_policy(key_arn)) + + assert put_resp["ResourceArn"] == key_arn + assert json.loads(put_resp["Policy"]) == json.loads(_sample_policy(key_arn)) + + get_resp = client.get_resource_policy(ResourceArn=key_arn) + assert get_resp["ResourceArn"] == key_arn + assert json.loads(get_resp["Policy"]) == json.loads(_sample_policy(key_arn)) + + +@mock_aws +def test_put_resource_policy_key_not_found(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + + with pytest.raises(ClientError) as exc: + client.put_resource_policy(ResourceArn=missing_arn, Policy="{}") + assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_get_resource_policy_when_none_set(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled=True, + Tags=[ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", + )["Key"] + + key_arn = key["KeyArn"] + + # The key exists but has no attached policy. Return an empty response + get_resp = client.get_resource_policy(ResourceArn=key_arn) + assert get_resp["ResourceArn"] == key_arn + assert json.loads(get_resp["Policy"]) == {} + +@mock_aws +def test_get_resource_policy_key_not_found(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + + with pytest.raises(ClientError) as exc: + client.get_resource_policy(ResourceArn=missing_arn) + assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_delete_resource_policy(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled=True, + Tags=[ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", + )["Key"] + + key_arn = key["KeyArn"] + + client.put_resource_policy(ResourceArn=key_arn, Policy=_sample_policy(key_arn)) + + client.delete_resource_policy(ResourceArn=key_arn) + get_resp = client.get_resource_policy(ResourceArn=key_arn) + assert get_resp["ResourceArn"] == key_arn + assert json.loads(get_resp["Policy"]) == {} From e4f7f0155adcfb19d3fa7a099ad0c2322e34ea67 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:16:57 -0400 Subject: [PATCH 16/29] feat(paymentcrypto): add replicaiton region --- moto/paymentcryptography/models.py | 39 ++++++++ moto/paymentcryptography/responses.py | 11 ++- .../test_paymentcryptography.py | 94 +++++++++---------- 3 files changed, 96 insertions(+), 48 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 61ffcc58322e..664c080405fb 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -214,6 +214,45 @@ def delete_resource_policy(self, resource_arn): raise ResourceNotFoundException(resource_arn) self.resource_policies.pop(resource_arn, None) + def add_key_replication_regions(self, key_identifier, replication_regions): + if key_identifier not in self.keys: + raise ResourceNotFoundException(key_identifier) + + key = self.keys[key_identifier] + + if key.multi_region_key_type is None: + key.multi_region_key_type = "PRIMARY" + key.primary_region = self.region_name + key.replication_status = {} + key.using_default_replication_regions = False + + for region in replication_regions: + if region not in key.replication_status: + key.replication_status[region] = {"Status": "SYNCHRONIZED"} + + replica_backend = paymentcryptography_backends[self.account_id][region] + replica_key = Key( + account_id=self.account_id, + region_name=region, + key_attributes=key.key_attributes, + key_check_value_algorithm=key.key_check_value_algorithm, + exportable=key.exportable, + enabled=key.enabled, + tags=key.tags, + derive_key_usage=key.derive_key_usage, + replication_regions=None, + ) + replica_key.key_id = key.key_id + replica_key.key_arn = f"arn:aws:payment-cryptography:{region}:{self.account_id}:key/{key.key_id}" + replica_key.key_check_value = key.key_check_value + replica_key.multi_region_key_type = "REPLICA" + replica_key.primary_region = self.region_name + replica_key.replication_status = None + replica_key.using_default_replication_regions = None + replica_backend.keys[replica_key.key_arn] = replica_key + + return key.to_dict() + paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 13300407365f..e477ca007eff 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -66,7 +66,6 @@ def list_tags_for_resource(self): return json.dumps(dict(Tags=tags, NextToken=next_token)) def tag_resource(self): - params = self._get_params() resource_arn = self._get_param("ResourceArn") tags = self._get_param("Tags") self.paymentcryptography_backend.tag_resource( @@ -123,3 +122,13 @@ def delete_resource_policy(self): resource_arn=resource_arn, ) return json.dumps(dict()) + + def add_key_replication_regions(self): + key_identifier =self._get_param("KeyIdentifier") + replication_regions =self._get_param("ReplicationRegions") + key = self.paymentcryptography_backend.add_key_replication_regions( + key_identifier=key_identifier, + replication_regions=replication_regions, + ) + + return json.dumps(dict(Key=key)) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 467ecefb8ed0..6f8bf57519a0 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -133,53 +133,6 @@ def test_get_key(): assert isinstance(key["CreateTimestamp"], datetime) -# @mock_aws -# def test_add_key_replication_regions(): -# primary_client = boto3.client("payment-cryptography", region_name="us-east-1") - -# key = primary_client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# )["Key"] - -# key_arn = key["KeyArn"] -# key_id = key_arn.split(":key/")[1] - -# # Add replication regions to the key -# primary_client.add_key_replication_regions( -# KeyArn=key_arn, -# ReplicationRegions=[ -# "us-west-2", -# "eu-west-1", -# ] -# ) - -# # Retrieve the key and check the replication status -# primary_key = primary_client.get_key(KeyArn=key_arn)["Key"] -# assert primary_key["MultiRegionKeyType"] == "PRIMARY" -# assert primary_key["PrimaryRegion"] == "us-east-1" -# assert primary_key["UsingDefaultReplicationRegions"] is False -# assert primary_key["ReplicationStatus"] == { -# "us-west-2": {"Status": "SYNCHRONIZED"}, -# "eu-west-1": {"Status": "SYNCHRONIZED"}, -# } - -# replica_client = boto3.client("payment-cryptography", region_name="us-west-2") -# replica_key = replica_client.get_key(KeyArn=f"arn:aws:payment-cryptography:us-west-2:1234567890:key/{key_id}")["Key"] -# assert replica_key["MultiRegionKeyType"] == "REPLICA" -# assert replica_key["PrimaryRegion"] == "us-east-1" - -# replica_client_eu = boto3.client("payment-cryptography", region_name="eu-west-1") -# replica_key_eu = replica_client_eu.get_key(KeyArn=f"arn:aws:payment-cryptography:eu-west-1:1234567890:key/{key_id}")["Key"] -# assert replica_key_eu["MultiRegionKeyType"] == "REPLICA" -# assert replica_key_eu["PrimaryRegion"] == "us-east-1" - - # @mock_aws # def test_enable_default_key_replication_regions(): # client = boto3.client("payment-cryptography", region_name="us-east-1") @@ -768,3 +721,50 @@ def test_delete_resource_policy(): get_resp = client.get_resource_policy(ResourceArn=key_arn) assert get_resp["ResourceArn"] == key_arn assert json.loads(get_resp["Policy"]) == {} + + +@mock_aws +def test_add_key_replication_regions(): + primary_client = boto3.client("payment-cryptography", region_name="us-east-1") + + key = primary_client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", + )["Key"] + + key_arn = key["KeyArn"] + key_id = key_arn.split(":key/")[1] + + # Add replication regions to the key + primary_key = primary_client.add_key_replication_regions( + KeyIdentifier=key_arn, + ReplicationRegions=[ + "us-west-2", + "eu-west-1", + ] + )["Key"] + + assert primary_key["MultiRegionKeyType"] == "PRIMARY" + assert primary_key["PrimaryRegion"] == "us-east-1" + assert primary_key["UsingDefaultReplicationRegions"] is False + assert primary_key["ReplicationStatus"] == { + "us-west-2": {"Status": "SYNCHRONIZED"}, + "eu-west-1": {"Status": "SYNCHRONIZED"}, + } + + replica_client = boto3.client("payment-cryptography", region_name="us-west-2") + replica_arn = f"arn:aws:payment-cryptography:us-west-2:{ACCOUNT_ID}:key/{key_id}" + replica_key = replica_client.get_key(KeyIdentifier=replica_arn)["Key"] + assert replica_key["MultiRegionKeyType"] == "REPLICA" + assert replica_key["PrimaryRegion"] == "us-east-1" + + replica_client_eu = boto3.client("payment-cryptography", region_name="eu-west-1") + replica_arn_eu = f"arn:aws:payment-cryptography:eu-west-1:{ACCOUNT_ID}:key/{key_id}" + replica_key_eu = replica_client_eu.get_key(KeyIdentifier=replica_arn_eu)["Key"] + assert replica_key_eu["MultiRegionKeyType"] == "REPLICA" + assert replica_key_eu["PrimaryRegion"] == "us-east-1" From 8c3cdac4086f744c1c4ad6c054301143ee042fb2 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:35:27 -0400 Subject: [PATCH 17/29] feat(paymentcrypto): update the way replicas are created --- moto/paymentcryptography/models.py | 30 +++++++++++-------- .../test_paymentcryptography.py | 25 +++++++++------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 664c080405fb..54ffeb7e6329 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -155,6 +155,9 @@ def create_key( if tags: self.tag_resource(resource_arn=key.key_arn, tags=tags) + if replication_regions: + self._create_replicas(key, replication_regions) + return key.to_dict() def list_keys(self, key_state, next_token, max_results): @@ -214,18 +217,8 @@ def delete_resource_policy(self, resource_arn): raise ResourceNotFoundException(resource_arn) self.resource_policies.pop(resource_arn, None) - def add_key_replication_regions(self, key_identifier, replication_regions): - if key_identifier not in self.keys: - raise ResourceNotFoundException(key_identifier) - - key = self.keys[key_identifier] - - if key.multi_region_key_type is None: - key.multi_region_key_type = "PRIMARY" - key.primary_region = self.region_name - key.replication_status = {} - key.using_default_replication_regions = False + def _create_replicas(self, key: "Key", replication_regions: list[str]) -> None: for region in replication_regions: if region not in key.replication_status: key.replication_status[region] = {"Status": "SYNCHRONIZED"} @@ -246,11 +239,24 @@ def add_key_replication_regions(self, key_identifier, replication_regions): replica_key.key_arn = f"arn:aws:payment-cryptography:{region}:{self.account_id}:key/{key.key_id}" replica_key.key_check_value = key.key_check_value replica_key.multi_region_key_type = "REPLICA" - replica_key.primary_region = self.region_name + replica_key.primary_region = key.primary_region replica_key.replication_status = None replica_key.using_default_replication_regions = None replica_backend.keys[replica_key.key_arn] = replica_key + def add_key_replication_regions(self, key_identifier, replication_regions): + if key_identifier not in self.keys: + raise ResourceNotFoundException(key_identifier) + + key = self.keys[key_identifier] + + if key.multi_region_key_type is None: + key.multi_region_key_type = "PRIMARY" + key.primary_region = self.region_name + key.replication_status = {} + key.using_default_replication_regions = False + + self._create_replicas(key, replication_regions) return key.to_dict() diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 6f8bf57519a0..23dbe93ba5c9 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -102,6 +102,14 @@ def test_create_key_with_replica_regions(): "eu-west-1": {"Status": "SYNCHRONIZED"}, } + key_id = key["KeyArn"].split(":key/")[1] + + for region in ("us-west-2", "eu-west-1"): + replica_client = boto3.client("payment-cryptography", region_name=region) + replica_arn = f"arn:aws:payment-cryptography:{region}:{ACCOUNT_ID}:key/{key_id}" + replica_key = replica_client.get_key(KeyIdentifier=replica_arn)["Key"] + assert replica_key["MultiRegionKeyType"] == "REPLICA" + assert replica_key["PrimaryRegion"] == "us-east-1" @mock_aws def test_get_key(): @@ -757,14 +765,9 @@ def test_add_key_replication_regions(): "eu-west-1": {"Status": "SYNCHRONIZED"}, } - replica_client = boto3.client("payment-cryptography", region_name="us-west-2") - replica_arn = f"arn:aws:payment-cryptography:us-west-2:{ACCOUNT_ID}:key/{key_id}" - replica_key = replica_client.get_key(KeyIdentifier=replica_arn)["Key"] - assert replica_key["MultiRegionKeyType"] == "REPLICA" - assert replica_key["PrimaryRegion"] == "us-east-1" - - replica_client_eu = boto3.client("payment-cryptography", region_name="eu-west-1") - replica_arn_eu = f"arn:aws:payment-cryptography:eu-west-1:{ACCOUNT_ID}:key/{key_id}" - replica_key_eu = replica_client_eu.get_key(KeyIdentifier=replica_arn_eu)["Key"] - assert replica_key_eu["MultiRegionKeyType"] == "REPLICA" - assert replica_key_eu["PrimaryRegion"] == "us-east-1" + for region in ("us-west-2", "eu-west-1"): + replica_client = boto3.client("payment-cryptography", region_name=region) + replica_arn = f"arn:aws:payment-cryptography:{region}:{ACCOUNT_ID}:key/{key_id}" + replica_key = replica_client.get_key(KeyIdentifier=replica_arn)["Key"] + assert replica_key["MultiRegionKeyType"] == "REPLICA" + assert replica_key["PrimaryRegion"] == "us-east-1" From 759b808d8007004f3d6b3e9082e2516f02525be5 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:47:02 -0400 Subject: [PATCH 18/29] feat(paymentcrypto): add enable_default_key_replication_regions --- moto/paymentcryptography/models.py | 7 ++++ moto/paymentcryptography/responses.py | 11 +++++- .../test_paymentcryptography.py | 38 +++++++++---------- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 54ffeb7e6329..286d207d28cb 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -121,6 +121,7 @@ class PaymentCryptographyControlPlaneBackend(BaseBackend): def __init__(self, region_name, account_id): super().__init__(region_name, account_id) + self.default_key_replication_regions = [] self.keys: dict[str, Key] = {} self.resource_policies: dict[str, str] = {} self.tagger = TaggingService( @@ -259,6 +260,12 @@ def add_key_replication_regions(self, key_identifier, replication_regions): self._create_replicas(key, replication_regions) return key.to_dict() + def enable_default_key_replication_regions(self, replication_regions): + for region in replication_regions: + if region not in self.default_key_replication_regions: + self.default_key_replication_regions.append(region) + + return self.default_key_replication_regions paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index e477ca007eff..1d487ddc5888 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -124,11 +124,18 @@ def delete_resource_policy(self): return json.dumps(dict()) def add_key_replication_regions(self): - key_identifier =self._get_param("KeyIdentifier") - replication_regions =self._get_param("ReplicationRegions") + key_identifier = self._get_param("KeyIdentifier") + replication_regions = self._get_param("ReplicationRegions") key = self.paymentcryptography_backend.add_key_replication_regions( key_identifier=key_identifier, replication_regions=replication_regions, ) return json.dumps(dict(Key=key)) + + def enable_default_key_replication_regions(self): + replication_regions = self._get_param("ReplicationRegions") + enabled_replication_regions = self.paymentcryptography_backend.enable_default_key_replication_regions( + replication_regions=replication_regions, + ) + return json.dumps(dict(EnabledReplicationRegions=enabled_replication_regions)) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 23dbe93ba5c9..a39f03251647 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -141,25 +141,6 @@ def test_get_key(): assert isinstance(key["CreateTimestamp"], datetime) -# @mock_aws -# def test_enable_default_key_replication_regions(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") - -# # Enable default replication regions -# key_replication_regions = client.enable_default_key_replication_regions( -# ReplicationRegions=[ -# "ap-southeast-1", -# ] -# ) - -# # Include default and newly added replication region -# assert key_replication_regions["EnabledReplicationRegions"] == [ -# "us-west-2", -# "eu-west-1", -# "ap-southeast-1", -# ] - - # @mock_aws # def test_get_default_key_replication_regions(): # client = boto3.client("payment-cryptography", region_name="us-east-1") @@ -771,3 +752,22 @@ def test_add_key_replication_regions(): replica_key = replica_client.get_key(KeyIdentifier=replica_arn)["Key"] assert replica_key["MultiRegionKeyType"] == "REPLICA" assert replica_key["PrimaryRegion"] == "us-east-1" + + +@mock_aws +def test_enable_default_key_replication_regions(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + # Enable default replication regions + key_replication_regions = client.enable_default_key_replication_regions( + ReplicationRegions=[ + "us-west-2", + "eu-west-1" + ] + )["EnabledReplicationRegions"] + + # Include default and newly added replication region + assert key_replication_regions == [ + "us-west-2", + "eu-west-1" + ] From a8551361cfb7de5542dff151e61725dd81619f06 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:57:56 -0400 Subject: [PATCH 19/29] feat(paymentcrypto): add get_default_key_replication_regions --- moto/paymentcryptography/models.py | 3 +++ moto/paymentcryptography/responses.py | 4 +++ .../test_paymentcryptography.py | 26 ++++++++++++------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 286d207d28cb..a2a34e09e9c8 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -267,6 +267,9 @@ def enable_default_key_replication_regions(self, replication_regions): return self.default_key_replication_regions + def get_default_key_replication_regions(self): + return self.default_key_replication_regions + paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, "payment-cryptography", diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 1d487ddc5888..94487e42ee4a 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -139,3 +139,7 @@ def enable_default_key_replication_regions(self): replication_regions=replication_regions, ) return json.dumps(dict(EnabledReplicationRegions=enabled_replication_regions)) + + def get_default_key_replication_regions(self): + get_replication_regions = self.paymentcryptography_backend.get_default_key_replication_regions() + return json.dumps(dict(EnabledReplicationRegions=get_replication_regions)) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index a39f03251647..2791464f26b1 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -140,16 +140,6 @@ def test_get_key(): assert retrieved_key["DeriveKeyUsage"] == "TR31_P0_PIN_ENCRYPTION_KEY" assert isinstance(key["CreateTimestamp"], datetime) - -# @mock_aws -# def test_get_default_key_replication_regions(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") - -# default_regions = client.get_default_key_replication_regions()["EnabledReplicationRegions"] -# assert "us-west-2" in default_regions -# assert "eu-west-1" in default_regions - - # @mock_aws # def test_disable_default_key_replication_regions(): # client = boto3.client("payment-cryptography", region_name="us-east-1") @@ -771,3 +761,19 @@ def test_enable_default_key_replication_regions(): "us-west-2", "eu-west-1" ] + + +@mock_aws +def test_get_default_key_replication_regions(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + # Enable default replication regions + default_regions = client.enable_default_key_replication_regions( + ReplicationRegions=[ + "us-west-2", + "eu-west-1" + ] + )["EnabledReplicationRegions"] + + assert "us-west-2" in default_regions + assert "eu-west-1" in default_regions From ea92617d3a6196f2901a380173e10fb4be7c186e Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:04:43 -0400 Subject: [PATCH 20/29] feat(paymentcrypto): add disable_default_key_replicaiton_regions --- moto/paymentcryptography/models.py | 6 +++ moto/paymentcryptography/responses.py | 7 ++++ .../test_paymentcryptography.py | 37 ++++++++++++------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index a2a34e09e9c8..192fee5d4ca8 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -270,6 +270,12 @@ def enable_default_key_replication_regions(self, replication_regions): def get_default_key_replication_regions(self): return self.default_key_replication_regions + def disable_default_key_replication_regions(self, replication_regions): + for region in replication_regions: + if region in self.default_key_replication_regions: + self.default_key_replication_regions.remove(region) + return self.default_key_replication_regions + paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, "payment-cryptography", diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 94487e42ee4a..500bd3104422 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -143,3 +143,10 @@ def enable_default_key_replication_regions(self): def get_default_key_replication_regions(self): get_replication_regions = self.paymentcryptography_backend.get_default_key_replication_regions() return json.dumps(dict(EnabledReplicationRegions=get_replication_regions)) + + def disable_default_key_replication_regions(self): + replication_regions = self._get_param("ReplicationRegions") + enabled_replication_regions = self.paymentcryptography_backend.disable_default_key_replication_regions( + replication_regions=replication_regions, + ) + return json.dumps(dict(EnabledReplicationRegions=enabled_replication_regions)) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 2791464f26b1..889c93a85344 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -140,20 +140,6 @@ def test_get_key(): assert retrieved_key["DeriveKeyUsage"] == "TR31_P0_PIN_ENCRYPTION_KEY" assert isinstance(key["CreateTimestamp"], datetime) -# @mock_aws -# def test_disable_default_key_replication_regions(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") - -# # Disable default replication regions -# key_replication_regions = client.disable_default_key_replication_regions( -# ReplicationRegions=[ -# "us-west-2", -# ] -# ) - -# # Check that the specified region is removed from the enabled replication regions -# assert "us-west-2" not in key_replication_regions["EnabledReplicationRegions"] - # @mock_aws # def test_create_alias(): @@ -777,3 +763,26 @@ def test_get_default_key_replication_regions(): assert "us-west-2" in default_regions assert "eu-west-1" in default_regions + + +@mock_aws +def test_disable_default_key_replication_regions(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + # Enable default replication regions + default_regions = client.enable_default_key_replication_regions( + ReplicationRegions=[ + "us-west-2", + "eu-west-1" + ] + )["EnabledReplicationRegions"] + + # Disable default replication regions + key_replication_regions = client.disable_default_key_replication_regions( + ReplicationRegions=[ + "us-west-2", + ] + ) + + # Check that the specified region is removed from the enabled replication regions + assert "us-west-2" not in key_replication_regions["EnabledReplicationRegions"] From 253b6d4e127ad6d87094804772a04119cdcaa420 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:50:10 -0400 Subject: [PATCH 21/29] feat(paymentcrypto): add alias commands --- moto/paymentcryptography/exceptions.py | 5 + moto/paymentcryptography/models.py | 47 ++- moto/paymentcryptography/responses.py | 53 +++ .../test_paymentcryptography.py | 381 ++++++++++-------- 4 files changed, 312 insertions(+), 174 deletions(-) diff --git a/moto/paymentcryptography/exceptions.py b/moto/paymentcryptography/exceptions.py index 9ec7b5512140..03f2188586f2 100644 --- a/moto/paymentcryptography/exceptions.py +++ b/moto/paymentcryptography/exceptions.py @@ -5,3 +5,8 @@ class ResourceNotFoundException(JsonRESTError): def __init__(self, msg: str): super().__init__("ResourceNotFoundException", f"{msg}") + + +class ConflictException(JsonRESTError): + def __init__(self, msg: str): + super().__init__("ConflictException", f"{msg}") diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 192fee5d4ca8..76aac54b89bc 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -9,7 +9,7 @@ from moto.core.common_models import BaseModel from moto.utilities.tagging_service import TaggingService -from .exceptions import ResourceNotFoundException +from .exceptions import ConflictException, ResourceNotFoundException def _random_key_id() -> str: @@ -115,12 +115,24 @@ def to_dict(self) -> dict[str, Any]: result["DeletePendingTimestamp"] = self.delete_timestamp return result +class Alias(BaseModel): + def __init__(self, alias_name: str, key_arn: Optional[str]): + self.alias_name = alias_name + self.key_arn = key_arn + + def to_dict(self) -> dict[str, Any]: + result: dict[str, Any] = {"AliasName": self.alias_name} + if self.key_arn is not None: + result["KeyArn"] = self.key_arn + return result + class PaymentCryptographyControlPlaneBackend(BaseBackend): """Implementation of PaymentCryptographyControlPlane APIs.""" def __init__(self, region_name, account_id): super().__init__(region_name, account_id) + self.aliases: dict[str, Alias] = {} self.default_key_replication_regions = [] self.keys: dict[str, Key] = {} self.resource_policies: dict[str, str] = {} @@ -276,6 +288,39 @@ def disable_default_key_replication_regions(self, replication_regions): self.default_key_replication_regions.remove(region) return self.default_key_replication_regions + def create_alias(self, alias_name, key_arn: Optional[str]) -> dict[str, Any]: + if alias_name in self.aliases: + raise ConflictException(f"Alias {alias_name} already exists") + if key_arn is not None and key_arn not in self.keys: + raise ResourceNotFoundException(key_arn) + alias = Alias(alias_name=alias_name, key_arn=key_arn) + self.aliases[alias_name] = alias + return alias.to_dict() + + def get_alias(self, alias_name: str) -> dict[str, Any]: + if alias_name not in self.aliases: + raise ResourceNotFoundException(alias_name) + return self.aliases[alias_name].to_dict() + + def list_aliases(self, key_arn: Optional[str], next_token, max_results) -> tuple[list[dict[str, Any]], Any]: + aliases = list(self.aliases.values()) + if key_arn is not None: + aliases = [alias for alias in aliases if alias.key_arn == key_arn] + return [alias.to_dict() for alias in aliases], next_token + + def update_alias(self, alias_name: str, key_arn: Optional[str]) -> dict[str, Any]: + if alias_name not in self.aliases: + raise ResourceNotFoundException(alias_name) + if key_arn is not None and key_arn not in self.keys: + raise ResourceNotFoundException(key_arn) + self.aliases[alias_name].key_arn = key_arn + return self.aliases[alias_name].to_dict() + + def delete_alias(self, alias_name: str) -> None: + if alias_name not in self.aliases: + raise ResourceNotFoundException(alias_name) + del self.aliases[alias_name] + paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, "payment-cryptography", diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 500bd3104422..e6b546fdc106 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -150,3 +150,56 @@ def disable_default_key_replication_regions(self): replication_regions=replication_regions, ) return json.dumps(dict(EnabledReplicationRegions=enabled_replication_regions)) + + def create_alias(self): + + alias_name = self._get_param("AliasName") + key_arn = self._get_param("KeyArn") + alias = self.paymentcryptography_backend.create_alias( + alias_name=alias_name, + key_arn=key_arn, + ) + # TODO: adjust response + return json.dumps(dict(Alias=alias)) + + def get_alias(self): + + alias_name = self._get_param("AliasName") + alias = self.paymentcryptography_backend.get_alias( + alias_name=alias_name, + ) + # TODO: adjust response + return json.dumps(dict(Alias=alias)) + + def list_aliases(self): + + key_arn = self._get_param("KeyArn") + next_token = self._get_param("NextToken") + max_results = self._get_param("MaxResults") + aliases, next_token = self.paymentcryptography_backend.list_aliases( + key_arn=key_arn, + next_token=next_token, + max_results=max_results, + ) + # TODO: adjust response + return json.dumps(dict(Aliases=aliases, NextToken=next_token)) + + def update_alias(self): + + alias_name = self._get_param("AliasName") + key_arn = self._get_param("KeyArn") + alias = self.paymentcryptography_backend.update_alias( + alias_name=alias_name, + key_arn=key_arn, + ) + # TODO: adjust response + return json.dumps(dict(Alias=alias)) + + def delete_alias(self): + + alias_name = self._get_param("AliasName") + self.paymentcryptography_backend.delete_alias( + alias_name=alias_name, + ) + # TODO: adjust response + return json.dumps(dict()) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 889c93a85344..81d453e58a7e 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -141,179 +141,6 @@ def test_get_key(): assert isinstance(key["CreateTimestamp"], datetime) -# @mock_aws -# def test_create_alias(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") - -# key = client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# )["Key"] - -# key_arn = key["KeyArn"] - -# # Create an alias for the key -# alias_name = "alias/my-key-alias" -# client.create_alias( -# AliasName=alias_name, -# KeyArn=key_arn -# ) - -# # Retrieve the alias and check its properties -# alias = client.list_aliases()["Aliases"] -# assert alias["AliasName"] == alias_name -# assert alias["KeyArn"] == key_arn - - -# @mock_aws -# def test_get_alias(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") - -# key = client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# )["Key"] - -# key_arn = key["KeyArn"] - -# # Create an alias for the key -# alias_name = "alias/my-key-alias" -# client.create_alias( -# AliasName=alias_name, -# KeyArn=key_arn -# ) - -# retrieved_alias = client.get_alias(AliasName=alias_name) -# assert retrieved_alias["AliasName"] == alias_name -# assert retrieved_alias["KeyArn"] == key_arn - - -# @mock_aws -# def test_update_alias(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") - -# key = client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# )["Key"] - -# # Create an alias for the key -# alias_name = "alias/my-key-alias" -# client.create_alias( -# AliasName=alias_name, -# KeyArn=key["KeyArn"] -# ) - -# # Check alias for the key -# retrieved_alias = client.get_alias(AliasName=alias_name) -# assert retrieved_alias["AliasName"] == alias_name - -# # Update the alias -# new_alias_name = "alias/my-updated-key-alias" - -# client.update_alias( -# AliasName=new_alias_name, -# KeyArn=key["KeyArn"], -# ) - -# # Check key for the updated alias -# updated_alias = client.get_alias(AliasName=new_alias_name) -# assert updated_alias["AliasName"] == new_alias_name - - -# @mock_aws -# def test_delete_alias(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") - -# key = client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# )["Key"] - -# key_arn = key["KeyArn"] - -# # Create an alias for the key -# alias_name = "alias/my-key-alias" -# client.create_alias( -# AliasName=alias_name, -# KeyArn=key_arn -# ) - -# # Delete the alias -# client.delete_alias(AliasName=alias_name) - -# # Check that the alias no longer exists -# aliases_list = client.list_aliases()["Aliases"] -# assert all(alias["AliasName"] != alias_name for alias in aliases_list) - - -# @mock_aws -# def test_list_aliases(): -# client = boto3.client("payment-cryptography", region_name="us-east-1") - -# key1 = client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# )["Key"] - -# key2 = client.create_key( -# KeyAttributes=KEY_ATTRIBUTES_1, -# Exportable=True, -# Enabled = True, -# Tags = [ -# {"Key": "Environment", "Value": "Test"}, -# ], -# DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", -# MultiRegionKeyType="PRIMARY", -# )["Key"] - -# # Create aliases for the keys -# alias_name1 = "alias/my-key-alias-1" -# alias_name2 = "alias/my-key-alias-2" -# client.create_alias( -# AliasName=alias_name1, -# KeyArn=key1["KeyArn"] -# ) -# client.create_alias( -# AliasName=alias_name2, -# KeyArn=key2["KeyArn"] -# ) - -# aliases_list = client.list_aliases()["Aliases"] -# assert len(aliases_list) == 2 -# assert any(alias["AliasName"] == alias_name1 for alias in aliases_list) -# assert any(alias["AliasName"] == alias_name2 for alias in aliases_list) - # @mock_aws # def test_put_resource_policy(): @@ -786,3 +613,211 @@ def test_disable_default_key_replication_regions(): # Check that the specified region is removed from the enabled replication regions assert "us-west-2" not in key_replication_regions["EnabledReplicationRegions"] + + +@mock_aws +def test_create_alias(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + )["Key"] + + key_arn = key["KeyArn"] + + # Create an alias for the key + alias_name = "alias/my-key-alias" + alias = client.create_alias( + AliasName=alias_name, + KeyArn=key_arn + )["Alias"] + + assert alias["AliasName"] == alias_name + assert alias["KeyArn"] == key["KeyArn"] + + +@mock_aws +def test_create_alias_no_key(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + alias_name = "alias/unbound" + alias = client.create_alias(AliasName=alias_name)["Alias"] + assert alias["AliasName"] == alias_name + assert "KeyArn" not in alias + + +@mock_aws +def test_create_alias_duplicate_raises(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + client.create_alias(AliasName="alias/dup") + + with pytest.raises(ClientError) as exc: + client.create_alias(AliasName="alias/dup") + assert exc.value.response["Error"]["Code"] == "ConflictException" + + +@mock_aws +def test_get_alias(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + )["Key"] + + key_arn = key["KeyArn"] + + # Create an alias for the key + alias_name = "alias/my-key-alias" + client.create_alias( + AliasName=alias_name, + KeyArn=key_arn + ) + + retrieved_alias = client.get_alias(AliasName=alias_name)["Alias"] + assert retrieved_alias["AliasName"] == alias_name + assert retrieved_alias["KeyArn"] == key_arn + + +@mock_aws +def test_get_alias_not_found(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + with pytest.raises(ClientError) as exc: + client.get_alias(AliasName="alias/doesnotexist") + assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException" + +@mock_aws +def test_update_alias(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + )["Key"] + + # Create an alias for the key + alias_name = "alias/my-key-alias" + client.create_alias( + AliasName=alias_name, + KeyArn=key["KeyArn"] + ) + + # Check alias for the key + retrieved_alias = client.get_alias(AliasName=alias_name)["Alias"] + assert retrieved_alias["AliasName"] == alias_name + + key2 = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + )["Key"] + + updated = client.update_alias( + AliasName=alias_name, + KeyArn=key2["KeyArn"], + )["Alias"] + + assert updated["AliasName"] == alias_name + assert updated["KeyArn"] == key2["KeyArn"] + retrieved = client.get_alias(AliasName=alias_name)["Alias"] + assert retrieved["KeyArn"] == key2["KeyArn"] + + +@mock_aws +def test_delete_alias(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + key = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + )["Key"] + + key_arn = key["KeyArn"] + + # Create an alias for the key + alias_name = "alias/my-key-alias" + client.create_alias( + AliasName=alias_name, + KeyArn=key_arn + ) + + # Delete the alias + client.delete_alias(AliasName=alias_name) + + # Check that the alias no longer exists + aliases_list = client.list_aliases()["Aliases"] + assert all(alias["AliasName"] != alias_name for alias in aliases_list) + + +@mock_aws +def test_delete_alias_not_found(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + with pytest.raises(ClientError) as exc: + client.delete_alias(AliasName="alias/doesnotexist") + assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_list_aliases(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + + key1 = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + )["Key"] + + key2 = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled = True, + Tags = [ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + )["Key"] + + # Create aliases for the keys + alias_name1 = "alias/my-key-alias-1" + alias_name2 = "alias/my-key-alias-2" + client.create_alias( + AliasName=alias_name1, + KeyArn=key1["KeyArn"] + ) + client.create_alias( + AliasName=alias_name2, + KeyArn=key2["KeyArn"] + ) + + aliases_list = client.list_aliases()["Aliases"] + assert len(aliases_list) == 2 + assert any(alias["AliasName"] == alias_name1 for alias in aliases_list) + assert any(alias["AliasName"] == alias_name2 for alias in aliases_list) From f3c955a951d18083e1509bc9e6ad18fc2e1644a5 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:57:22 -0400 Subject: [PATCH 22/29] feat(paymnentcrypto): add start_key_usage and stop_key_usage --- moto/paymentcryptography/models.py | 28 +++++- moto/paymentcryptography/responses.py | 15 +++- .../test_paymentcryptography.py | 85 +++++++++++++++++++ 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 76aac54b89bc..8acb88d0cf34 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -70,7 +70,7 @@ def __init__( self.key_origin = "AWS_PAYMENT_CRYPTOGRAPHY" self.create_timestamp = now self.usage_start_timestamp = now if enabled else None - self.usage_stop_timestamp = None + self.usage_stop_timestamp = now if not enabled else None self.delete_pending_timestamp = None self.delete_timestamp = None self.derive_key_usage = derive_key_usage @@ -88,8 +88,6 @@ def __init__( } self.using_default_replication_regions = False - # TODO: add missing mpa details - self.tags = tags or [] def to_dict(self) -> dict[str, Any]: @@ -109,10 +107,14 @@ def to_dict(self) -> dict[str, Any]: "ReplicationStatus": self.replication_status, "UsingDefaultReplicationRegions": self.using_default_replication_regions } + if self.usage_start_timestamp is not None: + result["UsageStartTimestamp"] = self.usage_start_timestamp + if self.usage_stop_timestamp is not None: + result["UsageStopTimestamp"] = self.usage_stop_timestamp if self.delete_pending_timestamp is not None: result["DeletePendingTimestamp"] = self.delete_pending_timestamp if self.delete_timestamp is not None: - result["DeletePendingTimestamp"] = self.delete_timestamp + result["DeleteTimestamp"] = self.delete_timestamp return result class Alias(BaseModel): @@ -321,6 +323,24 @@ def delete_alias(self, alias_name: str) -> None: raise ResourceNotFoundException(alias_name) del self.aliases[alias_name] + def start_key_usage(self, key_identifier): + if key_identifier not in self.keys: + raise ResourceNotFoundException(key_identifier) + key = self.keys[key_identifier] + if not key.enabled: + key.enabled = True + key.usage_start_timestamp = unix_time() + return key.to_dict() + + def stop_key_usage(self, key_identifier): + if key_identifier not in self.keys: + raise ResourceNotFoundException(key_identifier) + key = self.keys[key_identifier] + if key.enabled: + key.enabled = False + key.usage_stop_timestamp = unix_time() + return key.to_dict() + paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, "payment-cryptography", diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index e6b546fdc106..22090407830d 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -201,5 +201,18 @@ def delete_alias(self): self.paymentcryptography_backend.delete_alias( alias_name=alias_name, ) - # TODO: adjust response return json.dumps(dict()) + + def start_key_usage(self): + key_identifier = self._get_param("KeyIdentifier") + key = self.paymentcryptography_backend.start_key_usage( + key_identifier=key_identifier, + ) + return json.dumps(dict(Key=key)) + + def stop_key_usage(self): + key_identifier = self._get_param("KeyIdentifier") + key = self.paymentcryptography_backend.stop_key_usage( + key_identifier=key_identifier, + ) + return json.dumps(dict(Key=key)) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 81d453e58a7e..9e3c21bf3649 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -821,3 +821,88 @@ def test_list_aliases(): assert len(aliases_list) == 2 assert any(alias["AliasName"] == alias_name1 for alias in aliases_list) assert any(alias["AliasName"] == alias_name2 for alias in aliases_list) + + +@mock_aws +def test_start_key_usage(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + # key is created with Enabled=False + key_arn = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled=False, + Tags=[ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", + )["Key"]["KeyArn"] + + before = client.get_key(KeyIdentifier=key_arn)["Key"] + assert before["Enabled"] is False + assert "UsageStartTimestamp" not in before + + result = client.start_key_usage(KeyIdentifier=key_arn)["Key"] + assert result["Enabled"] is True + assert isinstance(result["UsageStartTimestamp"], datetime) + + +@mock_aws +def test_start_key_usage_already_enabled_does_not_update_timestamp(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + # key is created with Enabled=True + key_arn = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled=True, + Tags=[ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", + )["Key"]["KeyArn"] + + before = client.get_key(KeyIdentifier=key_arn)["Key"]["UsageStartTimestamp"] + # starting a key that is already enabled shouldn't change anything + after = client.start_key_usage(KeyIdentifier=key_arn)["Key"]["UsageStartTimestamp"] + assert before == after + + +@mock_aws +def test_start_key_usage_not_found(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + + with pytest.raises(ClientError) as exc: + client.start_key_usage(KeyIdentifier=missing_arn) + assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException" + + +@mock_aws +def test_stop_key_usage(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + key_arn = client.create_key( + KeyAttributes=KEY_ATTRIBUTES_1, + Exportable=True, + Enabled=True, + Tags=[ + {"Key": "Environment", "Value": "Test"}, + ], + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", + )["Key"]["KeyArn"] + + before = client.get_key(KeyIdentifier=key_arn)["Key"] + assert before["Enabled"] is True + assert "UsageStopTimestamp" not in before + + result = client.stop_key_usage(KeyIdentifier=key_arn)["Key"] + assert result["Enabled"] is False + assert isinstance(result["UsageStopTimestamp"], datetime) + + +@mock_aws +def test_stop_key_usage_not_found(): + client = boto3.client("payment-cryptography", region_name="us-east-1") + missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + + with pytest.raises(ClientError) as exc: + client.stop_key_usage(KeyIdentifier=missing_arn) + assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException" From 1ed97e3390699c521449330f3608fdc0b96db545 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:10:38 -0400 Subject: [PATCH 23/29] feat(paymentcrypto): lint and format --- moto/paymentcryptography/exceptions.py | 1 + moto/paymentcryptography/models.py | 124 +++++++----- moto/paymentcryptography/responses.py | 114 ++++++----- .../test_paymentcryptography.py | 187 ++++++++---------- 4 files changed, 222 insertions(+), 204 deletions(-) diff --git a/moto/paymentcryptography/exceptions.py b/moto/paymentcryptography/exceptions.py index 03f2188586f2..e9ead40481de 100644 --- a/moto/paymentcryptography/exceptions.py +++ b/moto/paymentcryptography/exceptions.py @@ -1,4 +1,5 @@ """Exceptions raised by the paymentcryptography service.""" + from moto.core.exceptions import JsonRESTError diff --git a/moto/paymentcryptography/models.py b/moto/paymentcryptography/models.py index 8acb88d0cf34..1989e3836159 100644 --- a/moto/paymentcryptography/models.py +++ b/moto/paymentcryptography/models.py @@ -2,11 +2,11 @@ import random import string -from moto.core.utils import unix_time from typing import Any, Optional from moto.core.base_backend import BackendDict, BaseBackend from moto.core.common_models import BaseModel +from moto.core.utils import unix_time from moto.utilities.tagging_service import TaggingService from .exceptions import ConflictException, ResourceNotFoundException @@ -22,6 +22,7 @@ def _key_check_value() -> str: kcv = "".join(random.choices("0123456789ABCDEF", k=6)) return kcv + def _key_check_value_algorithm(key_algorithm: str) -> str: # TDES keys use ANSI X9.24, AES keys use CMAC, HMAC keys use the HMAC hash, # and asymmetric (RSA/ECC) keys use SHA-1. @@ -43,23 +44,27 @@ def __init__( key_check_value_algorithm: Optional[str], enabled: bool, exportable: bool, - tags: list[dict[str, str]], - derive_key_usage: str, + tags: Optional[list[dict[str, str]]], + derive_key_usage: Optional[str], replication_regions: Optional[list[str]], - ): + ) -> None: self.key_id = _random_key_id() self.key_arn = ( f"arn:aws:payment-cryptography:{region_name}:{account_id}:key/{self.key_id}" ) now = unix_time() - self.key_attributes = key_attributes - key_attributes_key_algorithm = key_attributes.get("KeyAlgorithm") + self.key_attributes: Optional[dict[str, Any]] = key_attributes + key_attributes_key_algorithm = ( + key_attributes.get("KeyAlgorithm") if key_attributes else None + ) - self.key_check_value = _key_check_value() + self.key_check_value: str = _key_check_value() if key_check_value_algorithm: self.key_check_value_algorithm = key_check_value_algorithm else: - self.key_check_value_algorithm = _key_check_value_algorithm(key_attributes_key_algorithm) + self.key_check_value_algorithm = _key_check_value_algorithm( + key_attributes_key_algorithm or "" + ) self.exportable = exportable self.enabled = enabled @@ -69,15 +74,15 @@ def __init__( # If a key is imported, the origin is EXTERNAL. self.key_origin = "AWS_PAYMENT_CRYPTOGRAPHY" self.create_timestamp = now - self.usage_start_timestamp = now if enabled else None - self.usage_stop_timestamp = now if not enabled else None - self.delete_pending_timestamp = None - self.delete_timestamp = None - self.derive_key_usage = derive_key_usage + self.usage_start_timestamp: Optional[float] = now if enabled else None + self.usage_stop_timestamp: Optional[float] = now if not enabled else None + self.delete_pending_timestamp: Optional[float] = None + self.delete_timestamp: Optional[float] = None + self.derive_key_usage: Optional[str] = derive_key_usage self.multi_region_key_type: Optional[str] = None self.primary_region: Optional[str] = None - self.replication_status: Optional[dict] = None + self.replication_status: Optional[dict[str, dict[str, str]]] = None self.using_default_replication_regions: Optional[bool] = None if replication_regions is not None: @@ -88,7 +93,7 @@ def __init__( } self.using_default_replication_regions = False - self.tags = tags or [] + self.tags: list[dict[str, str]] = tags or [] def to_dict(self) -> dict[str, Any]: result = { @@ -105,7 +110,7 @@ def to_dict(self) -> dict[str, Any]: "MultiRegionKeyType": self.multi_region_key_type, "PrimaryRegion": self.primary_region, "ReplicationStatus": self.replication_status, - "UsingDefaultReplicationRegions": self.using_default_replication_regions + "UsingDefaultReplicationRegions": self.using_default_replication_regions, } if self.usage_start_timestamp is not None: result["UsageStartTimestamp"] = self.usage_start_timestamp @@ -117,6 +122,7 @@ def to_dict(self) -> dict[str, Any]: result["DeleteTimestamp"] = self.delete_timestamp return result + class Alias(BaseModel): def __init__(self, alias_name: str, key_arn: Optional[str]): self.alias_name = alias_name @@ -132,10 +138,10 @@ def to_dict(self) -> dict[str, Any]: class PaymentCryptographyControlPlaneBackend(BaseBackend): """Implementation of PaymentCryptographyControlPlane APIs.""" - def __init__(self, region_name, account_id): + def __init__(self, region_name: str, account_id: str) -> None: super().__init__(region_name, account_id) self.aliases: dict[str, Alias] = {} - self.default_key_replication_regions = [] + self.default_key_replication_regions: list[str] = [] self.keys: dict[str, Key] = {} self.resource_policies: dict[str, str] = {} self.tagger = TaggingService( @@ -145,19 +151,20 @@ def __init__(self, region_name, account_id): def create_key( self, key_attributes: Optional[dict[str, Any]], - key_check_value_algorithm: str, + key_check_value_algorithm: Optional[str], enabled: bool, exportable: bool, - tags: list[dict[str, str]], - derive_key_usage: str, - replication_regions: list[str], + tags: Optional[list[dict[str, str]]], + derive_key_usage: Optional[str], + replication_regions: Optional[list[str]], ) -> dict[str, Any]: - + tags = tags or [] + derive_key_usage = derive_key_usage or "" key = Key( account_id=self.account_id, region_name=self.region_name, key_attributes=key_attributes, - key_check_value_algorithm=key_check_value_algorithm, # TODO: match kcv to key_check_value_algorithm + key_check_value_algorithm=key_check_value_algorithm, # TODO: match kcv to key_check_value_algorithm exportable=exportable, enabled=enabled, tags=tags, @@ -175,30 +182,41 @@ def create_key( return key.to_dict() - def list_keys(self, key_state, next_token, max_results): + def list_keys( + self, + key_state: Optional[str], + next_token: Optional[str], + max_results: Optional[int], + ) -> tuple[list[dict[str, Any]], Optional[str]]: keys = list(self.keys.values()) if key_state: keys = [key for key in keys if key.key_state == key_state] return [key.to_dict() for key in keys], next_token - def list_tags_for_resource(self, resource_arn, next_token, max_results): + def list_tags_for_resource( + self, resource_arn: str, next_token: Optional[str], max_results: Optional[int] + ) -> tuple[list[dict[str, str]], Optional[str]]: tags = self.tagger.list_tags_for_resource(resource_arn)["Tags"] return tags, next_token - def tag_resource(self, resource_arn, tags: list[dict[str, str]]): + def tag_resource(self, resource_arn: str, tags: list[dict[str, str]]) -> None: self.tagger.tag_resource(resource_arn, tags) - def get_key(self, key_identifier): + def get_key(self, key_identifier: str) -> dict[str, Any]: if key_identifier not in self.keys: raise ResourceNotFoundException(key_identifier) key = self.keys.get(key_identifier) + if key is None: + raise ResourceNotFoundException(key_identifier) return key.to_dict() - def untag_resource(self, resource_arn, tag_keys): + def untag_resource(self, resource_arn: str, tag_keys: list[str]) -> None: self.tagger.untag_resource_using_names(resource_arn, tag_keys) - def delete_key(self, key_identifier, delete_key_in_days): + def delete_key( + self, key_identifier: str, delete_key_in_days: Optional[int] + ) -> dict[str, Any]: if key_identifier not in self.keys: raise ResourceNotFoundException(key_identifier) @@ -207,10 +225,10 @@ def delete_key(self, key_identifier, delete_key_in_days): key.key_state = "DELETE_PENDING" key.enabled = False key.usage_stop_timestamp = unix_time() - key.delete_pending_timestamp = unix_time() + days *86400 + key.delete_pending_timestamp = unix_time() + days * 86400 return key.to_dict() - def put_resource_policy(self, resource_arn, policy): + def put_resource_policy(self, resource_arn: str, policy: str) -> dict[str, Any]: if resource_arn not in self.keys: raise ResourceNotFoundException(resource_arn) self.resource_policies[resource_arn] = policy @@ -219,7 +237,7 @@ def put_resource_policy(self, resource_arn, policy): "ResourceArn": resource_arn, } - def get_resource_policy(self, resource_arn): + def get_resource_policy(self, resource_arn: str) -> dict[str, Any]: if resource_arn not in self.keys: raise ResourceNotFoundException(resource_arn) return { @@ -227,14 +245,18 @@ def get_resource_policy(self, resource_arn): "ResourceArn": resource_arn, } - def delete_resource_policy(self, resource_arn): + def delete_resource_policy(self, resource_arn: str) -> None: if resource_arn not in self.keys: raise ResourceNotFoundException(resource_arn) self.resource_policies.pop(resource_arn, None) - def _create_replicas(self, key: "Key", replication_regions: list[str]) -> None: + if key.replication_status is None: + key.replication_status = {} + for region in replication_regions: + if key.replication_status is None: + key.replication_status = {} if region not in key.replication_status: key.replication_status[region] = {"Status": "SYNCHRONIZED"} @@ -259,7 +281,9 @@ def _create_replicas(self, key: "Key", replication_regions: list[str]) -> None: replica_key.using_default_replication_regions = None replica_backend.keys[replica_key.key_arn] = replica_key - def add_key_replication_regions(self, key_identifier, replication_regions): + def add_key_replication_regions( + self, key_identifier: str, replication_regions: list[str] + ) -> dict[str, Any]: if key_identifier not in self.keys: raise ResourceNotFoundException(key_identifier) @@ -274,27 +298,31 @@ def add_key_replication_regions(self, key_identifier, replication_regions): self._create_replicas(key, replication_regions) return key.to_dict() - def enable_default_key_replication_regions(self, replication_regions): + def enable_default_key_replication_regions( + self, replication_regions: list[str] + ) -> list[str]: for region in replication_regions: if region not in self.default_key_replication_regions: self.default_key_replication_regions.append(region) return self.default_key_replication_regions - def get_default_key_replication_regions(self): + def get_default_key_replication_regions(self) -> list[str]: return self.default_key_replication_regions - def disable_default_key_replication_regions(self, replication_regions): + def disable_default_key_replication_regions( + self, replication_regions: list[str] + ) -> list[str]: for region in replication_regions: if region in self.default_key_replication_regions: self.default_key_replication_regions.remove(region) return self.default_key_replication_regions - def create_alias(self, alias_name, key_arn: Optional[str]) -> dict[str, Any]: + def create_alias(self, alias_name: str, key_arn: Optional[str]) -> dict[str, Any]: if alias_name in self.aliases: raise ConflictException(f"Alias {alias_name} already exists") if key_arn is not None and key_arn not in self.keys: - raise ResourceNotFoundException(key_arn) + raise ResourceNotFoundException(key_arn) alias = Alias(alias_name=alias_name, key_arn=key_arn) self.aliases[alias_name] = alias return alias.to_dict() @@ -304,7 +332,12 @@ def get_alias(self, alias_name: str) -> dict[str, Any]: raise ResourceNotFoundException(alias_name) return self.aliases[alias_name].to_dict() - def list_aliases(self, key_arn: Optional[str], next_token, max_results) -> tuple[list[dict[str, Any]], Any]: + def list_aliases( + self, + key_arn: Optional[str], + next_token: Optional[str], + max_results: Optional[int], + ) -> tuple[list[dict[str, Any]], Optional[str]]: aliases = list(self.aliases.values()) if key_arn is not None: aliases = [alias for alias in aliases if alias.key_arn == key_arn] @@ -323,7 +356,7 @@ def delete_alias(self, alias_name: str) -> None: raise ResourceNotFoundException(alias_name) del self.aliases[alias_name] - def start_key_usage(self, key_identifier): + def start_key_usage(self, key_identifier: str) -> dict[str, Any]: if key_identifier not in self.keys: raise ResourceNotFoundException(key_identifier) key = self.keys[key_identifier] @@ -332,7 +365,7 @@ def start_key_usage(self, key_identifier): key.usage_start_timestamp = unix_time() return key.to_dict() - def stop_key_usage(self, key_identifier): + def stop_key_usage(self, key_identifier: str) -> dict[str, Any]: if key_identifier not in self.keys: raise ResourceNotFoundException(key_identifier) key = self.keys[key_identifier] @@ -341,6 +374,7 @@ def stop_key_usage(self, key_identifier): key.usage_stop_timestamp = unix_time() return key.to_dict() + paymentcryptography_backends = BackendDict( PaymentCryptographyControlPlaneBackend, "payment-cryptography", @@ -360,6 +394,6 @@ def stop_key_usage(self, key_identifier): "ap-northeast-3", "ap-south-1", "ap-south-2", - "af-south-1" + "af-south-1", ], ) diff --git a/moto/paymentcryptography/responses.py b/moto/paymentcryptography/responses.py index 22090407830d..1b2242c55c1b 100644 --- a/moto/paymentcryptography/responses.py +++ b/moto/paymentcryptography/responses.py @@ -10,7 +10,7 @@ class PaymentCryptographyControlPlaneResponse(BaseResponse): """Handler for PaymentCryptographyControlPlane requests and responses.""" - def __init__(self): + def __init__(self) -> None: super().__init__(service_name="payment-cryptography") @property @@ -18,7 +18,6 @@ def paymentcryptography_backend(self) -> PaymentCryptographyControlPlaneBackend: """Return backend instance specific for this region.""" return paymentcryptography_backends[self.current_account][self.region] - def create_key(self) -> str: key_attributes = self._get_param("KeyAttributes") key_check_value_algorithm = self._get_param("KeyCheckValueAlgorithm") @@ -37,10 +36,9 @@ def create_key(self) -> str: replication_regions=replication_regions, ) + return json.dumps({"Key": key}) - return json.dumps(dict(Key=key)) - - def list_keys(self): + def list_keys(self) -> str: key_state = self._get_param("KeyState") next_token = self._get_param("NextToken") max_results = self._get_param("MaxResults") @@ -50,11 +48,11 @@ def list_keys(self): max_results=max_results, ) - return json.dumps(dict(Keys=keys, NextToken=next_token)) + return json.dumps({"Keys": keys, "NextToken": next_token}) # add templates from here - def list_tags_for_resource(self): + def list_tags_for_resource(self) -> str: resource_arn = self._get_param("ResourceArn") next_token = self._get_param("NextToken") max_results = self._get_param("MaxResults") @@ -63,67 +61,67 @@ def list_tags_for_resource(self): next_token=next_token, max_results=max_results, ) - return json.dumps(dict(Tags=tags, NextToken=next_token)) + return json.dumps({"Tags": tags, "NextToken": next_token}) - def tag_resource(self): + def tag_resource(self) -> str: resource_arn = self._get_param("ResourceArn") tags = self._get_param("Tags") self.paymentcryptography_backend.tag_resource( resource_arn=resource_arn, tags=tags, ) - return json.dumps(dict()) + return json.dumps({}) - def get_key(self): + def get_key(self) -> str: key_identifier = self._get_param("KeyIdentifier") key = self.paymentcryptography_backend.get_key( key_identifier=key_identifier, ) - return json.dumps(dict(Key=key)) + return json.dumps({"Key": key}) - def untag_resource(self): + def untag_resource(self) -> str: resource_arn = self._get_param("ResourceArn") - tag_keys = self._get_param("TagKeys") + tag_keys = self._get_param("TagKeys") self.paymentcryptography_backend.untag_resource( resource_arn=resource_arn, tag_keys=tag_keys, ) - return json.dumps(dict()) + return json.dumps({}) - def delete_key(self): + def delete_key(self) -> str: key_identifier = self._get_param("KeyIdentifier") delete_key_in_days = self._get_param("DeleteKeyInDays") key = self.paymentcryptography_backend.delete_key( key_identifier=key_identifier, delete_key_in_days=delete_key_in_days, ) - return json.dumps(dict(Key=key)) + return json.dumps({"Key": key}) - def put_resource_policy(self): - resource_arn = self._get_param("ResourceArn") - policy = self._get_param("Policy") + def put_resource_policy(self) -> str: + resource_arn = self._get_param("ResourceArn") + policy = self._get_param("Policy") result = self.paymentcryptography_backend.put_resource_policy( resource_arn=resource_arn, policy=policy, ) return json.dumps(result) - def get_resource_policy(self): - resource_arn = self._get_param("ResourceArn") + def get_resource_policy(self) -> str: + resource_arn = self._get_param("ResourceArn") policy = self.paymentcryptography_backend.get_resource_policy( resource_arn=resource_arn, ) return json.dumps(policy) - def delete_resource_policy(self): - resource_arn = self._get_param("ResourceArn") + def delete_resource_policy(self) -> str: + resource_arn = self._get_param("ResourceArn") self.paymentcryptography_backend.delete_resource_policy( resource_arn=resource_arn, ) - return json.dumps(dict()) + return json.dumps({}) - def add_key_replication_regions(self): + def add_key_replication_regions(self) -> str: key_identifier = self._get_param("KeyIdentifier") replication_regions = self._get_param("ReplicationRegions") key = self.paymentcryptography_backend.add_key_replication_regions( @@ -131,48 +129,50 @@ def add_key_replication_regions(self): replication_regions=replication_regions, ) - return json.dumps(dict(Key=key)) + return json.dumps({"Key": key}) - def enable_default_key_replication_regions(self): + def enable_default_key_replication_regions(self) -> str: replication_regions = self._get_param("ReplicationRegions") - enabled_replication_regions = self.paymentcryptography_backend.enable_default_key_replication_regions( - replication_regions=replication_regions, + enabled_replication_regions = ( + self.paymentcryptography_backend.enable_default_key_replication_regions( + replication_regions=replication_regions, + ) ) - return json.dumps(dict(EnabledReplicationRegions=enabled_replication_regions)) + return json.dumps({"EnabledReplicationRegions": enabled_replication_regions}) - def get_default_key_replication_regions(self): - get_replication_regions = self.paymentcryptography_backend.get_default_key_replication_regions() - return json.dumps(dict(EnabledReplicationRegions=get_replication_regions)) + def get_default_key_replication_regions(self) -> str: + get_replication_regions = ( + self.paymentcryptography_backend.get_default_key_replication_regions() + ) + return json.dumps({"EnabledReplicationRegions": get_replication_regions}) - def disable_default_key_replication_regions(self): + def disable_default_key_replication_regions(self) -> str: replication_regions = self._get_param("ReplicationRegions") - enabled_replication_regions = self.paymentcryptography_backend.disable_default_key_replication_regions( - replication_regions=replication_regions, + enabled_replication_regions = ( + self.paymentcryptography_backend.disable_default_key_replication_regions( + replication_regions=replication_regions, + ) ) - return json.dumps(dict(EnabledReplicationRegions=enabled_replication_regions)) - - def create_alias(self): + return json.dumps({"EnabledReplicationRegions": enabled_replication_regions}) + def create_alias(self) -> str: alias_name = self._get_param("AliasName") key_arn = self._get_param("KeyArn") alias = self.paymentcryptography_backend.create_alias( alias_name=alias_name, key_arn=key_arn, ) - # TODO: adjust response - return json.dumps(dict(Alias=alias)) - - def get_alias(self): + return json.dumps({"Alias": alias}) + def get_alias(self) -> str: alias_name = self._get_param("AliasName") alias = self.paymentcryptography_backend.get_alias( alias_name=alias_name, ) - # TODO: adjust response - return json.dumps(dict(Alias=alias)) - def list_aliases(self): + return json.dumps({"Alias": alias}) + def list_aliases(self) -> str: key_arn = self._get_param("KeyArn") next_token = self._get_param("NextToken") max_results = self._get_param("MaxResults") @@ -181,38 +181,36 @@ def list_aliases(self): next_token=next_token, max_results=max_results, ) - # TODO: adjust response - return json.dumps(dict(Aliases=aliases, NextToken=next_token)) - def update_alias(self): + return json.dumps({"Aliases": aliases, "NextToken": next_token}) + def update_alias(self) -> str: alias_name = self._get_param("AliasName") key_arn = self._get_param("KeyArn") alias = self.paymentcryptography_backend.update_alias( alias_name=alias_name, key_arn=key_arn, ) - # TODO: adjust response - return json.dumps(dict(Alias=alias)) - def delete_alias(self): + return json.dumps({"Alias": alias}) + def delete_alias(self) -> str: alias_name = self._get_param("AliasName") self.paymentcryptography_backend.delete_alias( alias_name=alias_name, ) - return json.dumps(dict()) + return json.dumps({}) - def start_key_usage(self): + def start_key_usage(self) -> str: key_identifier = self._get_param("KeyIdentifier") key = self.paymentcryptography_backend.start_key_usage( key_identifier=key_identifier, ) - return json.dumps(dict(Key=key)) + return json.dumps({"Key": key}) - def stop_key_usage(self): + def stop_key_usage(self) -> str: key_identifier = self._get_param("KeyIdentifier") key = self.paymentcryptography_backend.stop_key_usage( key_identifier=key_identifier, ) - return json.dumps(dict(Key=key)) + return json.dumps({"Key": key}) diff --git a/tests/test_paymentcryptography/test_paymentcryptography.py b/tests/test_paymentcryptography/test_paymentcryptography.py index 9e3c21bf3649..c6075725f6f5 100644 --- a/tests/test_paymentcryptography/test_paymentcryptography.py +++ b/tests/test_paymentcryptography/test_paymentcryptography.py @@ -1,11 +1,11 @@ """Unit tests for paymentcryptography-supported APIs.""" +import json from datetime import datetime import boto3 -from botocore.exceptions import ClientError -import json import pytest +from botocore.exceptions import ClientError from moto import mock_aws from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID @@ -27,6 +27,7 @@ }, } + def _sample_policy(key_arn): policy = { "Version": "2012-10-17", @@ -44,10 +45,11 @@ def _sample_policy(key_arn): ], "Resource": key_arn, } - ] + ], } return json.dumps(policy) + @mock_aws def test_create_key(): client = boto3.client("payment-cryptography", region_name="us-east-1") @@ -83,15 +85,15 @@ def test_create_key_with_replica_regions(): key = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", ReplicationRegions=[ "us-west-2", "eu-west-1", - ] + ], )["Key"] assert key["MultiRegionKeyType"] == "PRIMARY" @@ -111,6 +113,7 @@ def test_create_key_with_replica_regions(): assert replica_key["MultiRegionKeyType"] == "REPLICA" assert replica_key["PrimaryRegion"] == "us-east-1" + @mock_aws def test_get_key(): client = boto3.client("payment-cryptography", region_name="us-east-1") @@ -118,11 +121,11 @@ def test_get_key(): key = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], - DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", )["Key"] key_arn = key["KeyArn"] @@ -141,7 +144,6 @@ def test_get_key(): assert isinstance(key["CreateTimestamp"], datetime) - # @mock_aws # def test_put_resource_policy(): # client = boto3.client("payment-cryptography", region_name="us-east-1") @@ -303,6 +305,7 @@ def test_list_keys_single_region(): assert key["KeyCheckValue"] is not None assert key["KeyState"] == "CREATE_COMPLETE" + @mock_aws def test_list_tags_for_resource(): client = boto3.client("payment-cryptography", region_name="us-east-1") @@ -318,9 +321,10 @@ def test_list_tags_for_resource(): )["Key"] list_tags_resp = client.list_tags_for_resource(ResourceArn=key["KeyArn"]) - assert list_tags_resp['Tags'] == [ - {"Key": "Environment", "Value": "Test"}, - ] + assert list_tags_resp["Tags"] == [ + {"Key": "Environment", "Value": "Test"}, + ] + @mock_aws def test_tag_resource(): @@ -329,11 +333,11 @@ def test_tag_resource(): key = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], - DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", )["Key"] key_arn = key["KeyArn"] @@ -344,7 +348,7 @@ def test_tag_resource(): Tags=[ {"Key": "foo", "Value": "1"}, {"Key": "bar", "Value": "2"}, - ] + ], ) # Retrieve the key and check the tags with list_tags_for_resource @@ -355,8 +359,6 @@ def test_tag_resource(): assert {"Key": "bar", "Value": "2"} in retrieved_key - - @mock_aws def test_untag_resource(): client = boto3.client("payment-cryptography", region_name="ap-southeast-1") @@ -364,25 +366,24 @@ def test_untag_resource(): key = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, {"Key": "foo", "Value": "1"}, - {"Key": "bar", "Value": "2"} + {"Key": "bar", "Value": "2"}, ], - DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", )["Key"] key_arn = key["KeyArn"] # Untag the key - client.untag_resource( - ResourceArn=key_arn, - TagKeys=["foo", "bar"] - ) + client.untag_resource(ResourceArn=key_arn, TagKeys=["foo", "bar"]) # Retrieve the key and check the tags - retrieved_key_after_untag = client.list_tags_for_resource(ResourceArn=key_arn)["Tags"] + retrieved_key_after_untag = client.list_tags_for_resource(ResourceArn=key_arn)[ + "Tags" + ] assert len(retrieved_key_after_untag) == 1 assert {"Key": "Environment", "Value": "Test"} in retrieved_key_after_untag @@ -398,7 +399,7 @@ def test_delete_key(): Tags=[ {"Key": "Environment", "Value": "Test"}, ], - DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", )["Key"] key_arn = key["KeyArn"] @@ -418,7 +419,9 @@ def test_delete_key(): def test_delete_key_not_found(): client = boto3.client("payment-cryptography", region_name="us-east-1") - missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + missing_arn = ( + f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + ) with pytest.raises(ClientError) as exc: client.delete_key(KeyIdentifier=missing_arn) assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException" @@ -440,7 +443,9 @@ def test_put_resource_policy(): key_arn = key["KeyArn"] - put_resp = client.put_resource_policy(ResourceArn=key_arn, Policy=_sample_policy(key_arn)) + put_resp = client.put_resource_policy( + ResourceArn=key_arn, Policy=_sample_policy(key_arn) + ) assert put_resp["ResourceArn"] == key_arn assert json.loads(put_resp["Policy"]) == json.loads(_sample_policy(key_arn)) @@ -453,7 +458,9 @@ def test_put_resource_policy(): @mock_aws def test_put_resource_policy_key_not_found(): client = boto3.client("payment-cryptography", region_name="us-east-1") - missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + missing_arn = ( + f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + ) with pytest.raises(ClientError) as exc: client.put_resource_policy(ResourceArn=missing_arn, Policy="{}") @@ -481,10 +488,13 @@ def test_get_resource_policy_when_none_set(): assert get_resp["ResourceArn"] == key_arn assert json.loads(get_resp["Policy"]) == {} + @mock_aws def test_get_resource_policy_key_not_found(): client = boto3.client("payment-cryptography", region_name="us-east-1") - missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + missing_arn = ( + f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + ) with pytest.raises(ClientError) as exc: client.get_resource_policy(ResourceArn=missing_arn) @@ -522,8 +532,8 @@ def test_add_key_replication_regions(): key = primary_client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", @@ -538,7 +548,7 @@ def test_add_key_replication_regions(): ReplicationRegions=[ "us-west-2", "eu-west-1", - ] + ], )["Key"] assert primary_key["MultiRegionKeyType"] == "PRIMARY" @@ -563,17 +573,11 @@ def test_enable_default_key_replication_regions(): # Enable default replication regions key_replication_regions = client.enable_default_key_replication_regions( - ReplicationRegions=[ - "us-west-2", - "eu-west-1" - ] + ReplicationRegions=["us-west-2", "eu-west-1"] )["EnabledReplicationRegions"] # Include default and newly added replication region - assert key_replication_regions == [ - "us-west-2", - "eu-west-1" - ] + assert key_replication_regions == ["us-west-2", "eu-west-1"] @mock_aws @@ -582,10 +586,7 @@ def test_get_default_key_replication_regions(): # Enable default replication regions default_regions = client.enable_default_key_replication_regions( - ReplicationRegions=[ - "us-west-2", - "eu-west-1" - ] + ReplicationRegions=["us-west-2", "eu-west-1"] )["EnabledReplicationRegions"] assert "us-west-2" in default_regions @@ -597,12 +598,9 @@ def test_disable_default_key_replication_regions(): client = boto3.client("payment-cryptography", region_name="us-east-1") # Enable default replication regions - default_regions = client.enable_default_key_replication_regions( - ReplicationRegions=[ - "us-west-2", - "eu-west-1" - ] - )["EnabledReplicationRegions"] + client.enable_default_key_replication_regions( + ReplicationRegions=["us-west-2", "eu-west-1"] + ) # Disable default replication regions key_replication_regions = client.disable_default_key_replication_regions( @@ -622,21 +620,18 @@ def test_create_alias(): key = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], - DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", )["Key"] key_arn = key["KeyArn"] # Create an alias for the key alias_name = "alias/my-key-alias" - alias = client.create_alias( - AliasName=alias_name, - KeyArn=key_arn - )["Alias"] + alias = client.create_alias(AliasName=alias_name, KeyArn=key_arn)["Alias"] assert alias["AliasName"] == alias_name assert alias["KeyArn"] == key["KeyArn"] @@ -668,21 +663,18 @@ def test_get_alias(): key = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], - DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", )["Key"] key_arn = key["KeyArn"] # Create an alias for the key alias_name = "alias/my-key-alias" - client.create_alias( - AliasName=alias_name, - KeyArn=key_arn - ) + client.create_alias(AliasName=alias_name, KeyArn=key_arn) retrieved_alias = client.get_alias(AliasName=alias_name)["Alias"] assert retrieved_alias["AliasName"] == alias_name @@ -696,6 +688,7 @@ def test_get_alias_not_found(): client.get_alias(AliasName="alias/doesnotexist") assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException" + @mock_aws def test_update_alias(): client = boto3.client("payment-cryptography", region_name="us-east-1") @@ -703,19 +696,16 @@ def test_update_alias(): key = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], - DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", )["Key"] # Create an alias for the key alias_name = "alias/my-key-alias" - client.create_alias( - AliasName=alias_name, - KeyArn=key["KeyArn"] - ) + client.create_alias(AliasName=alias_name, KeyArn=key["KeyArn"]) # Check alias for the key retrieved_alias = client.get_alias(AliasName=alias_name)["Alias"] @@ -724,11 +714,11 @@ def test_update_alias(): key2 = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], - DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", )["Key"] updated = client.update_alias( @@ -749,21 +739,18 @@ def test_delete_alias(): key = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], - DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", )["Key"] key_arn = key["KeyArn"] # Create an alias for the key alias_name = "alias/my-key-alias" - client.create_alias( - AliasName=alias_name, - KeyArn=key_arn - ) + client.create_alias(AliasName=alias_name, KeyArn=key_arn) # Delete the alias client.delete_alias(AliasName=alias_name) @@ -788,34 +775,28 @@ def test_list_aliases(): key1 = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], - DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", )["Key"] key2 = client.create_key( KeyAttributes=KEY_ATTRIBUTES_1, Exportable=True, - Enabled = True, - Tags = [ + Enabled=True, + Tags=[ {"Key": "Environment", "Value": "Test"}, ], - DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY" + DeriveKeyUsage="TR31_P0_PIN_ENCRYPTION_KEY", )["Key"] # Create aliases for the keys alias_name1 = "alias/my-key-alias-1" alias_name2 = "alias/my-key-alias-2" - client.create_alias( - AliasName=alias_name1, - KeyArn=key1["KeyArn"] - ) - client.create_alias( - AliasName=alias_name2, - KeyArn=key2["KeyArn"] - ) + client.create_alias(AliasName=alias_name1, KeyArn=key1["KeyArn"]) + client.create_alias(AliasName=alias_name2, KeyArn=key2["KeyArn"]) aliases_list = client.list_aliases()["Aliases"] assert len(aliases_list) == 2 @@ -869,7 +850,9 @@ def test_start_key_usage_already_enabled_does_not_update_timestamp(): @mock_aws def test_start_key_usage_not_found(): client = boto3.client("payment-cryptography", region_name="us-east-1") - missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + missing_arn = ( + f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + ) with pytest.raises(ClientError) as exc: client.start_key_usage(KeyIdentifier=missing_arn) @@ -901,7 +884,9 @@ def test_stop_key_usage(): @mock_aws def test_stop_key_usage_not_found(): client = boto3.client("payment-cryptography", region_name="us-east-1") - missing_arn = f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + missing_arn = ( + f"arn:aws:payment-cryptography:us-east-1:{ACCOUNT_ID}:key/doesnotexist123" + ) with pytest.raises(ClientError) as exc: client.stop_key_usage(KeyIdentifier=missing_arn) From 1c0a559106fd55897af66305d348c741d4735360 Mon Sep 17 00:00:00 2001 From: algobarb <78746954+algobarb@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:18:25 -0400 Subject: [PATCH 24/29] feat(paymentcrypto): update docs --- docs/docs/services/payment-cryptography.rst | 51 +++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/docs/services/payment-cryptography.rst diff --git a/docs/docs/services/payment-cryptography.rst b/docs/docs/services/payment-cryptography.rst new file mode 100644 index 000000000000..4e28dfe049c6 --- /dev/null +++ b/docs/docs/services/payment-cryptography.rst @@ -0,0 +1,51 @@ +.. _implementedservice_payment-cryptography: + +.. |start-h3| raw:: html + +