diff --git a/method/method.py b/method/method.py index 42b16c8..6e2510f 100644 --- a/method/method.py +++ b/method/method.py @@ -12,6 +12,10 @@ from method.resources.Events import EventResource from method.resources.CardProduct import CardProductResource from method.resources.Opal import OpalResource +from method.resources.Secrets import SecretResource +from method.resources.ForwardingRequests import ForwardingRequestResource +from method.resources.Teams import TeamResource +from method.resources.ManagedAccounts import ManagedAccountResource class Method: accounts: AccountResource @@ -26,6 +30,10 @@ class Method: simulate: SimulateResource card_products: CardProductResource opal: OpalResource + secrets: SecretResource + forwarding_requests: ForwardingRequestResource + teams: TeamResource + managed_accounts: ManagedAccountResource def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): _opts: ConfigurationOpts = {**(opts or {}), **kwargs} # type: ignore @@ -43,6 +51,10 @@ def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): self.simulate = SimulateResource(config) self.card_products = CardProductResource(config) self.opal = OpalResource(config) + self.secrets = SecretResource(config) + self.forwarding_requests = ForwardingRequestResource(config) + self.teams = TeamResource(config) + self.managed_accounts = ManagedAccountResource(config) def ping(self) -> MethodResponse[PingResponse]: return self.healthcheck.retrieve() diff --git a/method/resource.py b/method/resource.py index 37e50b4..b0192ea 100644 --- a/method/resource.py +++ b/method/resource.py @@ -147,7 +147,7 @@ def __init__(self, config: Configuration): 'Authorization': 'Bearer {token}'.format(token=config.api_key), 'Content-Type': 'application/json', 'User-Agent': 'Method-Python/v{version}'.format(version=version('method-python')), - 'method-version': '2025-07-04' + 'method-version': '2025-12-01' }) def _make_request(self, method: str, path: Optional[str] = None, data: Optional[Dict] = None, params: Optional[Dict] = None, headers: Optional[Dict] = None, raw: bool = False, download: bool = False) -> Union[MethodResponse[T], str]: diff --git a/method/resources/Accounts/PaymentInstruments.py b/method/resources/Accounts/PaymentInstruments.py index 4e34efd..3f8c931 100644 --- a/method/resources/Accounts/PaymentInstruments.py +++ b/method/resources/Accounts/PaymentInstruments.py @@ -6,7 +6,8 @@ AccountPaymentInstrumentTypesLiterals = Literal[ 'card', - 'network_token' + 'network_token', + 'inbound_achwire_payment' ] class AccountPaymentInstrumentCreateOpts(TypedDict): @@ -20,11 +21,17 @@ class AccountPaymentInstrumentCard(TypedDict): exp_month: int exp_year: int +class AccountPaymentInstrumentInboundACHWirePayment(TypedDict): + account_number: str + routing_number: str + reversal_account: Optional[str] + AccountPaymentInstrumentStatusesLiterals = Literal[ 'completed', 'in_progress', 'pending', - 'failed' + 'failed', + 'closed' ] class AccountPaymentInstrument(TypedDict): @@ -33,6 +40,7 @@ class AccountPaymentInstrument(TypedDict): type: AccountPaymentInstrumentTypesLiterals network_token: Optional[AccountPaymentInstrumentNetworkToken] card: Optional[AccountPaymentInstrumentCard] + inbound_achwire_payment: Optional[AccountPaymentInstrumentInboundACHWirePayment] chargeable: bool status: AccountPaymentInstrumentStatusesLiterals error: Optional[ResourceError] @@ -52,3 +60,7 @@ def list(self, params: Optional[ResourceListOpts] = None) -> MethodResponse[List def create(self, data: AccountPaymentInstrumentCreateOpts) -> MethodResponse[AccountPaymentInstrument]: return super(AccountPaymentInstrumentsResource, self)._create(data) + + # Supported only for payment instruments of type inbound_achwire_payment; returns the closed instrument. + def delete(self, pmt_inst_id: str) -> MethodResponse[AccountPaymentInstrument]: + return super(AccountPaymentInstrumentsResource, self)._delete(pmt_inst_id) diff --git a/method/resources/Accounts/Subscriptions.py b/method/resources/Accounts/Subscriptions.py index 9bb1d2c..8cbe543 100644 --- a/method/resources/Accounts/Subscriptions.py +++ b/method/resources/Accounts/Subscriptions.py @@ -5,6 +5,11 @@ AccountSubscriptionTypesLiterals = Literal[ + 'attribute', + 'card_brand', + 'payment_instrument', + 'payment_instrument.card', + 'payment_instrument.network_token', 'transaction', 'update', 'update.snapshot' @@ -21,6 +26,11 @@ class AccountSubscription(TypedDict): AccountSubscriptionsResponse = TypedDict('AccountSubscriptionsResponse', { + 'attribute': Optional[AccountSubscription], + 'card_brand': Optional[AccountSubscription], + 'payment_instrument': Optional[AccountSubscription], + 'payment_instrument.card': Optional[AccountSubscription], + 'payment_instrument.network_token': Optional[AccountSubscription], 'transaction': Optional[AccountSubscription], 'update': Optional[AccountSubscription], 'update.snapshot': Optional[AccountSubscription] diff --git a/method/resources/Accounts/Types.py b/method/resources/Accounts/Types.py index 6e27c65..6650cd2 100644 --- a/method/resources/Accounts/Types.py +++ b/method/resources/Accounts/Types.py @@ -23,13 +23,19 @@ 'update', 'attribute', 'transaction', - 'payment_instrument' + 'payment_instrument', + 'payment_instrument.card', + 'payment_instrument.inbound_achwire_payment', + 'payment_instrument.network_token' ] AccountSubscriptionTypesLiterals = Literal[ + 'attribute', 'card_brand', 'payment_instrument', + 'payment_instrument.card', + 'payment_instrument.network_token', 'transaction', 'update', 'update.snapshot' diff --git a/method/resources/ForwardingRequests/ForwardingRequest.py b/method/resources/ForwardingRequests/ForwardingRequest.py new file mode 100644 index 0000000..11dbc0d --- /dev/null +++ b/method/resources/ForwardingRequests/ForwardingRequest.py @@ -0,0 +1,68 @@ +from typing import TypedDict, Optional, List, Dict, Any, Literal + +from method.resource import MethodResponse, Resource, RequestOpts +from method.configuration import Configuration + + +ForwardingRequestStatusesLiterals = Literal[ + 'completed', + 'failed' +] + + +ForwardingRequestMethodsLiterals = Literal[ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE' +] + + +class ForwardingRequestDetail(TypedDict): + url: str + method: ForwardingRequestMethodsLiterals + headers: Dict[str, str] + body: str + + +class ForwardingResponseDetail(TypedDict): + status_code: Optional[int] + headers: Dict[str, str] + body: Any + + +class ForwardingRequestStatusHistoryItem(TypedDict): + status: str + message: Optional[str] + + +class ForwardingRequest(TypedDict): + id: str + bindings: Dict[str, str] + request: ForwardingRequestDetail + response: ForwardingResponseDetail + duration_ms: int + status: ForwardingRequestStatusesLiterals + status_history: List[ForwardingRequestStatusHistoryItem] + created_at: str + + +class ForwardingRequestCreateOpts(TypedDict): + bindings: Dict[str, str] + url: str + method: ForwardingRequestMethodsLiterals + headers: Dict[str, str] + body: str + metadata: Optional[Dict[str, Any]] + + +class ForwardingRequestResource(Resource): + def __init__(self, config: Configuration): + super(ForwardingRequestResource, self).__init__(config.add_path('forwarding_requests')) + + def create(self, opts: ForwardingRequestCreateOpts, request_opts: Optional[RequestOpts] = None) -> MethodResponse[ForwardingRequest]: + return super(ForwardingRequestResource, self)._create(opts, request_opts=request_opts) + + def retrieve(self, _id: str) -> MethodResponse[ForwardingRequest]: + return super(ForwardingRequestResource, self)._get_with_id(_id) diff --git a/method/resources/ForwardingRequests/__init__.py b/method/resources/ForwardingRequests/__init__.py new file mode 100644 index 0000000..9e1cc63 --- /dev/null +++ b/method/resources/ForwardingRequests/__init__.py @@ -0,0 +1 @@ +from method.resources.ForwardingRequests.ForwardingRequest import ForwardingRequest, ForwardingRequestResource diff --git a/method/resources/ManagedAccounts/ManagedAccount.py b/method/resources/ManagedAccounts/ManagedAccount.py new file mode 100644 index 0000000..cde4e9e --- /dev/null +++ b/method/resources/ManagedAccounts/ManagedAccount.py @@ -0,0 +1,55 @@ +from typing import TypedDict, Optional, List + +from method.resource import MethodResponse, Resource +from method.configuration import Configuration + + +class ManagedAccount(TypedDict): + id: str + routing: str + number: str + current_balance: int + available_balance: int + + +class ManagedAccountTransaction(TypedDict): + id: str + description: str + date: str + amount: int + + +class ManagedAccountTransactionListOpts(TypedDict): + page_limit: Optional[int] + from_date: Optional[str] + to_date: Optional[str] + page_cursor: Optional[str] + + +class ManagedAccountTransactionsResource(Resource): + def __init__(self, config: Configuration): + super(ManagedAccountTransactionsResource, self).__init__(config.add_path('transactions')) + + def list(self, params: Optional[ManagedAccountTransactionListOpts] = None) -> MethodResponse[List[ManagedAccountTransaction]]: + return super(ManagedAccountTransactionsResource, self)._list(params) + + +class ManagedAccountSubResources: + transactions: ManagedAccountTransactionsResource + + def __init__(self, _id: str, config: Configuration): + self.transactions = ManagedAccountTransactionsResource(config.add_path(_id)) + + +class ManagedAccountResource(Resource): + def __init__(self, config: Configuration): + super(ManagedAccountResource, self).__init__(config.add_path('managed_accounts')) + + def __call__(self, macc_id: str) -> ManagedAccountSubResources: + return ManagedAccountSubResources(macc_id, self.config) + + def list(self) -> MethodResponse[List[ManagedAccount]]: + return super(ManagedAccountResource, self)._list(None) + + def retrieve(self, macc_id: str) -> MethodResponse[ManagedAccount]: + return super(ManagedAccountResource, self)._get_with_id(macc_id) diff --git a/method/resources/ManagedAccounts/__init__.py b/method/resources/ManagedAccounts/__init__.py new file mode 100644 index 0000000..90ae1c2 --- /dev/null +++ b/method/resources/ManagedAccounts/__init__.py @@ -0,0 +1 @@ +from method.resources.ManagedAccounts.ManagedAccount import ManagedAccount, ManagedAccountTransaction, ManagedAccountResource diff --git a/method/resources/Secrets/Secret.py b/method/resources/Secrets/Secret.py new file mode 100644 index 0000000..11cfac8 --- /dev/null +++ b/method/resources/Secrets/Secret.py @@ -0,0 +1,46 @@ +from typing import TypedDict, Optional, List, Dict, Any, Literal + +from method.resource import MethodResponse, Resource, RequestOpts, ResourceListOpts +from method.configuration import Configuration +from method.errors import ResourceError + + +SecretStatusesLiterals = Literal[ + 'active', + 'deleted' +] + + +class Secret(TypedDict): + id: str + metadata: Optional[Dict[str, Any]] + status: SecretStatusesLiterals + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class SecretCreateOpts(TypedDict): + value: str + metadata: Optional[Dict[str, Any]] + + +class SecretListOpts(ResourceListOpts): + pass + + +class SecretResource(Resource): + def __init__(self, config: Configuration): + super(SecretResource, self).__init__(config.add_path('secrets')) + + def create(self, opts: SecretCreateOpts, request_opts: Optional[RequestOpts] = None) -> MethodResponse[Secret]: + return super(SecretResource, self)._create(opts, request_opts=request_opts) + + def retrieve(self, _id: str) -> MethodResponse[Secret]: + return super(SecretResource, self)._get_with_id(_id) + + def list(self, params: Optional[SecretListOpts] = None) -> MethodResponse[List[Secret]]: + return super(SecretResource, self)._list(params) + + def delete(self, _id: str) -> MethodResponse[None]: + return super(SecretResource, self)._delete(_id) diff --git a/method/resources/Secrets/__init__.py b/method/resources/Secrets/__init__.py new file mode 100644 index 0000000..097c6ab --- /dev/null +++ b/method/resources/Secrets/__init__.py @@ -0,0 +1 @@ +from method.resources.Secrets.Secret import Secret, SecretResource diff --git a/method/resources/Teams/Team.py b/method/resources/Teams/Team.py new file mode 100644 index 0000000..fccfb7c --- /dev/null +++ b/method/resources/Teams/Team.py @@ -0,0 +1,152 @@ +from typing import TypedDict, Optional, List, Literal, Union + +from method.resource import MethodResponse, Resource, RequestOpts +from method.configuration import Configuration + + +TeamStatusesLiterals = Literal[ + 'active', + 'verified', + 'disabled', + 'pending_disablement' +] + + +TeamContactTypesLiterals = Literal[ + 'Admin', + 'Billing', + 'Technical' +] + + +class TeamContact(TypedDict): + name: str + email: str + type: TeamContactTypesLiterals + + +class TeamAddress(TypedDict): + line1: str + line2: Optional[str] + city: str + state: str + zipcode: str + country: Optional[str] + + +class Team(TypedDict): + id: str + parent_id: Optional[str] + name: str + legal_name: str + ein: str + contacts: List[TeamContact] + address: TeamAddress + status: TeamStatusesLiterals + logo: Optional[str] + created_at: str + updated_at: str + + +class TeamCreateOpts(TypedDict): + name: str + legal_name: str + ein: str + contacts: List[TeamContact] + address: Optional[TeamAddress] + + +class TeamEncryptionKeyRSAKey(TypedDict): + keyModulus: str + keyExponent: str + + +class TeamEncryptionKeyCertificate(TypedDict): + certificate: str + + +class TeamEncryptionKeyOpts(TypedDict): + encryption_key: Union[TeamEncryptionKeyRSAKey, TeamEncryptionKeyCertificate] + + +MLEPublicKeyTypesLiterals = Literal[ + 'direct', + 'well_known' +] + + +MLEPublicKeyStatusesLiterals = Literal[ + 'active', + 'disabled' +] + + +class JWK(TypedDict): + kid: Optional[str] + kty: str + alg: Optional[str] + use: Optional[str] + n: str + e: str + + +class MLEPublicKey(TypedDict): + id: str + type: MLEPublicKeyTypesLiterals + jwk: Optional[JWK] + well_known_endpoint: Optional[str] + status: MLEPublicKeyStatusesLiterals + contact: str + created_at: str + updated_at: str + + +class MLEPublicKeyCreateOpts(TypedDict): + type: MLEPublicKeyTypesLiterals + contact: str + jwk: Optional[JWK] + well_known_endpoint: Optional[str] + + +class TeamMLEPublicKeysResource(Resource): + def __init__(self, config: Configuration): + super(TeamMLEPublicKeysResource, self).__init__(config.add_path('public_keys')) + + def create(self, opts: MLEPublicKeyCreateOpts, request_opts: Optional[RequestOpts] = None) -> MethodResponse[MLEPublicKey]: + return super(TeamMLEPublicKeysResource, self)._create(opts, request_opts=request_opts) + + def list(self) -> MethodResponse[List[MLEPublicKey]]: + return super(TeamMLEPublicKeysResource, self)._list(None) + + def retrieve(self, _id: str) -> MethodResponse[MLEPublicKey]: + return super(TeamMLEPublicKeysResource, self)._get_with_id(_id) + + def delete(self, _id: str) -> MethodResponse[MLEPublicKey]: + return super(TeamMLEPublicKeysResource, self)._delete(_id) + + +class TeamMLEResource(Resource): + public_keys: TeamMLEPublicKeysResource + + def __init__(self, config: Configuration): + _config = config.add_path('mle') + super(TeamMLEResource, self).__init__(_config) + self.public_keys = TeamMLEPublicKeysResource(_config) + + +class TeamResource(Resource): + mle: TeamMLEResource + + def __init__(self, config: Configuration): + _config = config.add_path('teams') + super(TeamResource, self).__init__(_config) + self.mle = TeamMLEResource(_config) + + def list(self) -> MethodResponse[List[Team]]: + return super(TeamResource, self)._list(None) + + def create(self, opts: TeamCreateOpts, request_opts: Optional[RequestOpts] = None) -> MethodResponse[Team]: + return super(TeamResource, self)._create(opts, request_opts=request_opts) + + def update_encryption_key(self, opts: TeamEncryptionKeyOpts) -> MethodResponse[None]: + return super(TeamResource, self)._create_with_sub_path('default_encryption_key', opts) diff --git a/method/resources/Teams/__init__.py b/method/resources/Teams/__init__.py new file mode 100644 index 0000000..3812bb8 --- /dev/null +++ b/method/resources/Teams/__init__.py @@ -0,0 +1,2 @@ +from method.resources.Teams.Team import Team, TeamCreateOpts, TeamEncryptionKeyOpts, MLEPublicKey, TeamResource, TeamMLEResource, \ + TeamMLEPublicKeysResource diff --git a/method/resources/__init__.py b/method/resources/__init__.py index 30baf82..cffc21c 100644 --- a/method/resources/__init__.py +++ b/method/resources/__init__.py @@ -14,4 +14,10 @@ from method.resources.Merchant import Merchant, MerchantProviderIds, MerchantResource from method.resources.Report import Report, ReportCreateOpts, ReportResource from method.resources.Webhook import Webhook, WebhookCreateOpts, WebhookResource -from method.resources.Events.Event import Event, EventResource \ No newline at end of file +from method.resources.Events.Event import Event, EventResource +from method.resources.Secrets.Secret import Secret, SecretCreateOpts, SecretResource +from method.resources.ForwardingRequests.ForwardingRequest import ForwardingRequest, ForwardingRequestCreateOpts, ForwardingRequestResource +from method.resources.Teams.Team import Team, TeamContact, TeamAddress, TeamCreateOpts, TeamEncryptionKeyOpts, MLEPublicKey, \ + MLEPublicKeyCreateOpts, TeamResource, TeamMLEResource, TeamMLEPublicKeysResource +from method.resources.ManagedAccounts.ManagedAccount import ManagedAccount, ManagedAccountTransaction, \ + ManagedAccountTransactionListOpts, ManagedAccountResource \ No newline at end of file diff --git a/test/resources/ForwardingRequest_test.py b/test/resources/ForwardingRequest_test.py new file mode 100644 index 0000000..8428f99 --- /dev/null +++ b/test/resources/ForwardingRequest_test.py @@ -0,0 +1,110 @@ +import os +import pytest +from method import Method +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +pytestmark = pytest.mark.skipif(not API_KEY, reason='API_KEY is not set; skipping live dev API tests.') + +method = Method(env='dev', api_key=API_KEY) + +# Forwarding requests are executed synchronously against a destination URL that must be +# whitelisted for the team (e.g. Fidel, Tabapay, Recurly), and their bindings must reference +# real resources owned by the team (e.g. a card payment instrument, a secret, an entity). +# A maintainer must provide the following to exercise these tests, then remove the skips: +# FWD_REQUEST_URL - a destination URL whitelisted for the team +# FWD_REQUEST_PMT_INST_ID - an active card payment instrument id (pmt_inst_...) owned by the team +FWD_REQUEST_URL = os.getenv('FWD_REQUEST_URL') +FWD_REQUEST_PMT_INST_ID = os.getenv('FWD_REQUEST_PMT_INST_ID') + +SKIP_REASON = ( + 'Requires a whitelisted destination URL (FWD_REQUEST_URL) and a real card payment ' + 'instrument (FWD_REQUEST_PMT_INST_ID) provisioned for the team in the dev environment.' +) + +forwarding_requests_create_response = None +forwarding_requests_retrieve_response = None + +@pytest.fixture(scope='module') +def setup(): + holder_1_response = method.entities.create({ + 'type': 'individual', + 'individual': { + 'first_name': 'Kevin', + 'last_name': 'Doyle', + 'dob': '1930-03-11', + 'email': 'kevin.doyle@gmail.com', + 'phone': '+15121231111', + } + }) + + secret_1_response = method.secrets.create({ + 'value': 'test_secret_value' + }) + + yield { + 'holder_1_id': holder_1_response['id'], + 'secret_1_id': secret_1_response['id'], + } + + # Entities cannot be deleted via the public API, so only the secret is cleaned up. + method.secrets.delete(secret_1_response['id']) + + +@pytest.mark.skip(reason=SKIP_REASON) +def test_create_forwarding_request(setup): + global forwarding_requests_create_response + + forwarding_requests_create_response = method.forwarding_requests.create({ + 'bindings': { + 'card': FWD_REQUEST_PMT_INST_ID, + 'individual': setup['holder_1_id'], + 'api_key': setup['secret_1_id'], + }, + 'url': FWD_REQUEST_URL, + 'method': 'POST', + 'headers': { + 'Authorization': 'Bearer {{ api_key.value }}' + }, + 'body': '{ "card": { "accountNumber": "{{ card.number }}" }, "owner": { "firstName": "{{ individual.first_name }}", "lastName": "{{ individual.last_name }}" } }' + }) + + expect_results = { + 'id': forwarding_requests_create_response['id'], + 'bindings': { + 'card': FWD_REQUEST_PMT_INST_ID, + 'individual': setup['holder_1_id'], + 'api_key': setup['secret_1_id'], + }, + 'request': forwarding_requests_create_response['request'], + 'response': forwarding_requests_create_response['response'], + 'duration_ms': forwarding_requests_create_response['duration_ms'], + 'status': 'completed', + 'status_history': forwarding_requests_create_response['status_history'], + 'created_at': forwarding_requests_create_response['created_at'], + } + + assert forwarding_requests_create_response == expect_results + + +@pytest.mark.skip(reason=SKIP_REASON) +def test_retrieve_forwarding_request(setup): + global forwarding_requests_retrieve_response + + forwarding_requests_retrieve_response = method.forwarding_requests.retrieve(forwarding_requests_create_response['id']) + + expect_results = { + 'id': forwarding_requests_create_response['id'], + 'bindings': forwarding_requests_create_response['bindings'], + 'request': forwarding_requests_retrieve_response['request'], + 'response': forwarding_requests_retrieve_response['response'], + 'duration_ms': forwarding_requests_retrieve_response['duration_ms'], + 'status': forwarding_requests_retrieve_response['status'], + 'status_history': forwarding_requests_retrieve_response['status_history'], + 'created_at': forwarding_requests_retrieve_response['created_at'], + } + + assert forwarding_requests_retrieve_response == expect_results diff --git a/test/resources/ManagedAccount_test.py b/test/resources/ManagedAccount_test.py new file mode 100644 index 0000000..2ab011d --- /dev/null +++ b/test/resources/ManagedAccount_test.py @@ -0,0 +1,65 @@ +import os +import pytest +from method import Method +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +pytestmark = pytest.mark.skipif(not API_KEY, reason='API_KEY is not set; skipping live dev API tests.') + +# Managed accounts cannot be created via the public API; they are provisioned per-team by +# Method. If the dev team has none, set MANAGED_ACCOUNT_ID to a macc_... id provisioned for +# the team to exercise the retrieve and transactions tests. +MANAGED_ACCOUNT_ID = os.getenv('MANAGED_ACCOUNT_ID') + +method = Method(env='dev', api_key=API_KEY) + +MANAGED_ACCOUNT_EXPECTED_KEYS = ['id', 'routing', 'number', 'current_balance', 'available_balance'] +MANAGED_ACCOUNT_TRANSACTION_EXPECTED_KEYS = ['id', 'description', 'date', 'amount'] + + +def get_test_managed_account_id(): + if MANAGED_ACCOUNT_ID: + return MANAGED_ACCOUNT_ID + + managed_accounts_list_response = method.managed_accounts.list() + + if len(managed_accounts_list_response) == 0: + pytest.skip('No managed accounts provisioned for the dev team and MANAGED_ACCOUNT_ID is not set.') + + return managed_accounts_list_response[0]['id'] + + +def test_list_managed_accounts(): + managed_accounts_list_response = method.managed_accounts.list() + + assert isinstance(managed_accounts_list_response.to_dict(), list) + + for managed_account in managed_accounts_list_response: + for key in MANAGED_ACCOUNT_EXPECTED_KEYS: + assert key in managed_account + + +def test_retrieve_managed_account(): + macc_id = get_test_managed_account_id() + + managed_accounts_retrieve_response = method.managed_accounts.retrieve(macc_id) + + assert managed_accounts_retrieve_response['id'] == macc_id + + for key in MANAGED_ACCOUNT_EXPECTED_KEYS: + assert key in managed_accounts_retrieve_response + + +def test_list_managed_account_transactions(): + macc_id = get_test_managed_account_id() + + managed_account_transactions_list_response = method.managed_accounts(macc_id).transactions.list() + + assert isinstance(managed_account_transactions_list_response.to_dict(), list) + + for transaction in managed_account_transactions_list_response: + for key in MANAGED_ACCOUNT_TRANSACTION_EXPECTED_KEYS: + assert key in transaction diff --git a/test/resources/Secret_test.py b/test/resources/Secret_test.py new file mode 100644 index 0000000..02a0412 --- /dev/null +++ b/test/resources/Secret_test.py @@ -0,0 +1,69 @@ +import os +import pytest +from method import Method +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +pytestmark = pytest.mark.skipif(not API_KEY, reason='API_KEY is not set; skipping live dev API tests.') + +method = Method(env='dev', api_key=API_KEY) + +secrets_create_response = None +secrets_retrieve_response = None +secrets_list_response = None +secrets_delete_response = None + +def test_create_secret(): + global secrets_create_response + + secrets_create_response = method.secrets.create({ + 'value': 'test_secret_value' + }) + + expect_results = { + 'id': secrets_create_response['id'], + 'metadata': None, + 'status': 'active', + 'error': None, + 'created_at': secrets_create_response['created_at'], + 'updated_at': secrets_create_response['updated_at'], + } + + assert secrets_create_response == expect_results + + +def test_retrieve_secret(): + global secrets_retrieve_response + + secrets_retrieve_response = method.secrets.retrieve(secrets_create_response['id']) + + expect_results = { + 'id': secrets_create_response['id'], + 'metadata': None, + 'status': 'active', + 'error': None, + 'created_at': secrets_retrieve_response['created_at'], + 'updated_at': secrets_retrieve_response['updated_at'], + } + + assert secrets_retrieve_response == expect_results + + +def test_list_secrets(): + global secrets_list_response + + secrets_list_response = method.secrets.list() + secret_ids = [secret['id'] for secret in secrets_list_response] + + assert secrets_create_response['id'] in secret_ids + + +def test_delete_secret(): + global secrets_delete_response + + secrets_delete_response = method.secrets.delete(secrets_create_response['id']) + + assert secrets_delete_response.to_dict() is None diff --git a/test/resources/TeamMLEPublicKey_test.py b/test/resources/TeamMLEPublicKey_test.py new file mode 100644 index 0000000..559769b --- /dev/null +++ b/test/resources/TeamMLEPublicKey_test.py @@ -0,0 +1,85 @@ +import os +import uuid +import pytest +from method import Method +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +pytestmark = pytest.mark.skipif(not API_KEY, reason='API_KEY is not set; skipping live dev API tests.') + +method = Method(env='dev', api_key=API_KEY) + +test_jwk = { + 'kid': str(uuid.uuid4()), + 'kty': 'RSA', + 'alg': 'RSA-OAEP-256', + 'use': 'enc', + 'n': 'x9hKPiAZKzHhAZx670NMvnvI0ZaEa1I92XsQklLORGVqqECy3oA7In8tkb0FEI2V2yJMZhMkf-4EbsTPZu_D7Hqo3E6fHR0FNd0gocpEy5fBf5at6o92ueVmYiDiXsgxFHZzhEo40a26diRBkzzxYpjxZNtvheQiM34n25kSqvJ3sacIguQs4erqgWl2YR8l1HYIX5_9n3wQ3cuU4a0fcHoLtVmD4fymZ1kiESUiU6qkw-XkYn0BZD3TwTbStQrkXDoFt9D7L7-PLCU5Nmqval5RtI2i4q_uks8t9Hg9YjrM3_FnulT18YiLJ0aGUTgx-qaNoJy5OLGfIg0cfhFrvQ', + 'e': 'AQAB' +} + +public_keys_create_response = None +public_keys_retrieve_response = None +public_keys_list_response = None +public_keys_delete_response = None + +def test_create_mle_public_key(): + global public_keys_create_response + + public_keys_create_response = method.teams.mle.public_keys.create({ + 'type': 'direct', + 'contact': 'engineering@methodfi.com', + 'jwk': test_jwk + }) + + expect_results = { + 'id': public_keys_create_response['id'], + 'type': 'direct', + 'jwk': public_keys_create_response['jwk'], + 'well_known_endpoint': None, + 'status': 'active', + 'contact': 'engineering@methodfi.com', + 'created_at': public_keys_create_response['created_at'], + 'updated_at': public_keys_create_response['updated_at'], + } + + assert public_keys_create_response == expect_results + + +def test_list_mle_public_keys(): + global public_keys_list_response + + public_keys_list_response = method.teams.mle.public_keys.list() + public_key_ids = [public_key['id'] for public_key in public_keys_list_response] + + assert public_keys_create_response['id'] in public_key_ids + + +def test_retrieve_mle_public_key(): + global public_keys_retrieve_response + + public_keys_retrieve_response = method.teams.mle.public_keys.retrieve(public_keys_create_response['id']) + + expect_results = { + 'id': public_keys_create_response['id'], + 'type': 'direct', + 'jwk': public_keys_retrieve_response['jwk'], + 'well_known_endpoint': None, + 'status': 'active', + 'contact': 'engineering@methodfi.com', + 'created_at': public_keys_retrieve_response['created_at'], + 'updated_at': public_keys_retrieve_response['updated_at'], + } + + assert public_keys_retrieve_response == expect_results + + +def test_delete_mle_public_key(): + global public_keys_delete_response + + public_keys_delete_response = method.teams.mle.public_keys.delete(public_keys_create_response['id']) + + assert public_keys_delete_response['status'] == 'disabled' diff --git a/test/resources/Team_test.py b/test/resources/Team_test.py new file mode 100644 index 0000000..24d6486 --- /dev/null +++ b/test/resources/Team_test.py @@ -0,0 +1,79 @@ +import os +import pytest +from method import Method +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +pytestmark = pytest.mark.skipif(not API_KEY, reason='API_KEY is not set; skipping live dev API tests.') + +method = Method(env='dev', api_key=API_KEY) + +TEAM_EXPECTED_KEYS = ['id', 'parent_id', 'name', 'legal_name', 'ein', 'contacts', 'address', 'status', 'created_at', 'updated_at', 'logo'] + +CREATE_SKIP_REASON = ( + 'POST /teams provisions a new sub-team in the shared dev environment, and teams cannot be ' + 'deleted via the public API.' +) + +ENCRYPTION_KEY_SKIP_REASON = ( + 'POST /teams/default_encryption_key replaces the dev team\'s active default encryption key, ' + 'mutating shared dev state for every consumer of the team.' +) + + +def test_list_teams(): + teams_list_response = method.teams.list() + + assert isinstance(teams_list_response.to_dict(), list) + + for team in teams_list_response: + for key in TEAM_EXPECTED_KEYS: + assert key in team + + +@pytest.mark.skip(reason=CREATE_SKIP_REASON) +def test_create_team(): + team_create_response = method.teams.create({ + 'name': 'Test Sub Team', + 'legal_name': 'Test Sub Team LLC', + 'ein': '12-3456789', + 'contacts': [{ + 'name': 'Kevin Doyle', + 'email': 'kevin.doyle@gmail.com', + 'type': 'Admin', + }], + }) + + expect_results = { + 'id': team_create_response['id'], + 'parent_id': team_create_response['parent_id'], + 'name': 'Test Sub Team', + 'legal_name': 'Test Sub Team LLC', + 'ein': '12-3456789', + 'contacts': [{ + 'name': 'Kevin Doyle', + 'email': 'kevin.doyle@gmail.com', + 'type': 'Admin', + }], + 'address': team_create_response['address'], + 'status': team_create_response['status'], + 'logo': team_create_response['logo'], + 'created_at': team_create_response['created_at'], + 'updated_at': team_create_response['updated_at'], + } + + assert team_create_response == expect_results + + +@pytest.mark.skip(reason=ENCRYPTION_KEY_SKIP_REASON) +def test_update_encryption_key(): + update_encryption_key_response = method.teams.update_encryption_key({ + 'encryption_key': { + 'certificate': '-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----', + }, + }) + + assert update_encryption_key_response.to_dict() is None