diff --git a/README.md b/README.md index 055b0c5..e093561 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A Python library for integrating [Payload](https://payload.com). ## Installation -## Install using pip +### Install using pip ```bash pip install payload-api @@ -39,6 +39,27 @@ import payload pl = payload.Session('secret_key_3bW9JMZtPVDOfFNzwRdfE') ``` +### API Versioning + +The Payload API supports multiple versions. You can specify which version to use when making requests: + +```python +import payload as pl +pl.api_key = 'secret_key_3bW9JMZtPVDOfFNzwRdfE' +pl.api_version = 'v2' # Use API v2 +``` + +Or with sessions: + +```python +import payload +pl = payload.Session( + 'secret_key_3bW9JMZtPVDOfFNzwRdfE', + api_version='v2' +) +``` + +API v2 introduces new objects including `Profile`, `Intent`, `Entity`, `Transfer`, `ProcessingAgreement`, and more. See the [Payload API Documentation](https://docs.payload.com) for details on API versions. ### Creating an Object @@ -60,7 +81,9 @@ customer = pl.Customer.create( payment = pl.Payment.create( amount=100.0, payment_method=pl.Card( - card_number='4242 4242 4242 4242' + card_number='4242 4242 4242 4242', + expiry='12/28', + card_code='123' ) ) ``` @@ -107,12 +130,21 @@ payments = pl.Payment.filter_by( ### Testing the Payload Python Library -Tests are contained within the tests/ directory. To run a test file, once within the -pipenv shell, enter the command in terminal +Tests are contained within the tests/ directory. To run tests: ```bash -TEST_SECRET_KEY=test_api_key pytest tests/{__FILENAME__}.py - ``` +# Install dependencies +pdm install + +# Run integration tests +TEST_SECRET_KEY=test_api_key pdm run pytest tests/int/ + +# Run unit tests +pdm run pytest tests/unit/ + +# Run a specific test file +TEST_SECRET_KEY=test_api_key pdm run pytest tests/int/test_transaction.py +``` ## Documentation diff --git a/payload/__init__.py b/payload/__init__.py index 08888fa..b39e82c 100644 --- a/payload/__init__.py +++ b/payload/__init__.py @@ -4,20 +4,151 @@ Documentation: https://docs.payload.com -:copyright: (c) 2021 Payload (http://payload.com) +:copyright: (c) 2026 Payload (http://payload.com) :license: MIT License """ -from .version import __version__ -from .exceptions import * -from .objects import * -from .arm import Attr as attr, ARMRequest, session_factory +__all__ = [ + # Version + '__version__', + # ARM + 'ARMRequest', + 'attr', + 'session_factory', + 'Session', + # Functions + 'create', + 'update', + 'delete', + # Module variables + 'URL', + 'api_key', + 'api_url', + 'api_version', + # Submodules + 'objects', + # Exceptions + 'BadRequest', + 'Forbidden', + 'InternalServerError', + 'InvalidAttributes', + 'NotFound', + 'PayloadError', + 'ServiceUnavailable', + 'TooManyRequests', + 'TransactionDeclined', + 'Unauthorized', + 'UnknownResponse', + # Objects + 'AccessToken', + 'Account', + 'BankAccount', + 'BillingCharge', + 'BillingSchedule', + 'Card', + 'ChargeItem', + 'ClientToken', + 'Credit', + 'Customer', + 'Deposit', + 'Invoice', + 'Ledger', + 'LineItem', + 'OAuthToken', + 'Org', + 'Payment', + 'PaymentActivation', + 'PaymentItem', + 'PaymentLink', + 'PaymentMethod', + 'ProcessingAccount', + 'ProcessingRule', + 'Refund', + 'Transaction', + 'User', + 'Webhook', + # New API v2 Objects + 'Profile', + 'BillingItem', + 'Intent', + 'InvoiceItem', + 'PaymentAllocation', + 'Entity', + 'Stakeholder', + 'ProcessingAgreement', + 'Transfer', + 'TransactionOperation', + 'CheckFront', + 'CheckBack', + 'ProcessingSettings', +] + from . import objects +from .arm import ARMRequest +from .arm import Attr as attr +from .arm import session_factory +from .exceptions import ( + BadRequest, + Forbidden, + InternalServerError, + InvalidAttributes, + NotFound, + PayloadError, + ServiceUnavailable, + TooManyRequests, + TransactionDeclined, + Unauthorized, + UnknownResponse, +) +from .objects import ( # API v2 Objects + AccessToken, + Account, + BankAccount, + BillingCharge, + BillingItem, + BillingSchedule, + Card, + ChargeItem, + CheckBack, + CheckFront, + ClientToken, + Credit, + Customer, + Deposit, + Entity, + Intent, + Invoice, + InvoiceItem, + Ledger, + LineItem, + OAuthToken, + Org, + Payment, + PaymentActivation, + PaymentAllocation, + PaymentItem, + PaymentLink, + PaymentMethod, + ProcessingAccount, + ProcessingAgreement, + ProcessingRule, + ProcessingSettings, + Profile, + Refund, + Stakeholder, + Transaction, + TransactionOperation, + Transfer, + User, + Webhook, +) +from .version import __version__ URL = 'https://api.payload.com' api_key = None api_url = URL +api_version = None Session = session_factory('PayloadSession', objects) diff --git a/payload/arm/request.py b/payload/arm/request.py index c168e50..1a9054f 100644 --- a/payload/arm/request.py +++ b/payload/arm/request.py @@ -31,6 +31,9 @@ def _request(self, method, id=None, headers=None, params=None, json=None): auth = (session.api_key, '') files = {} + if session.api_version: + headers['X-API-Version'] = session.api_version + if json: flat_data = nested_qstring_keys(copy.copy(json)) for k in list(flat_data): @@ -63,6 +66,7 @@ def _request(self, method, id=None, headers=None, params=None, json=None): auth=auth, data=flat_data, files=files, + headers=headers, ) else: response = getattr(requests, method)( diff --git a/payload/arm/session.py b/payload/arm/session.py index a41aeb7..5e85b21 100644 --- a/payload/arm/session.py +++ b/payload/arm/session.py @@ -1,22 +1,24 @@ -from .request import ARMRequest -from .object import ARMObject, ARMObjectWrapper -from . import Attr -import types -import inspect import payload +from . import Attr +from .object import ARMObjectWrapper +from .request import ARMRequest + + def get_object(objects, name): if isinstance(objects, dict): return objects[name] else: return getattr(objects, name) + class Session(object): attr = Attr - def __init__(self, api_key=None, api_url=None): + def __init__(self, api_key=None, api_url=None, api_version=None): self.api_key = api_key or payload.api_key self.api_url = api_url or payload.api_url + self.api_version = api_version or payload.api_version def create(self, *args, **kwargs): return ARMRequest(session=self).create(*args, **kwargs) @@ -30,5 +32,6 @@ def delete(self, *args, **kwargs): def __getattr__(self, name): return ARMObjectWrapper(get_object(self._objects, name), self) + def session_factory(name, objects): return type(name, (Session,), {'_objects': objects}) diff --git a/payload/objects.py b/payload/objects.py index ac5c500..9eca3a1 100644 --- a/payload/objects.py +++ b/payload/objects.py @@ -1,86 +1,168 @@ from .arm.object import ARMObject + class AccessToken(ARMObject): - __spec__ = {'object': 'access_token'} + __spec__ = {"object": "access_token"} + class ClientToken(AccessToken): - __spec__ = { 'polymorphic': { 'type': 'client' } } + __spec__ = {"polymorphic": {"type": "client"}} + class OAuthToken(ARMObject): - __spec__ = { 'endpoint': '/oauth/token', 'object': 'oauth_token' } + __spec__ = {"endpoint": "/oauth/token", "object": "oauth_token"} + class Account(ARMObject): - __spec__ = { 'object': 'account' } + __spec__ = {"object": "account"} + class Customer(ARMObject): - __spec__ = { 'object': 'customer' } + __spec__ = {"object": "customer"} + class ProcessingAccount(ARMObject): - __spec__ = { 'object': 'processing_account' } + __spec__ = {"object": "processing_account"} + class Org(ARMObject): - __spec__ = { 'endpoint': '/accounts/orgs', 'object': 'org' } + __spec__ = {"endpoint": "/accounts/orgs", "object": "org"} + class User(ARMObject): - __spec__ = { 'object': 'user' } + __spec__ = {"object": "user"} + class Transaction(ARMObject): - __spec__ = { 'endpoint': '/transactions', 'object': 'transaction' } + __spec__ = {"endpoint": "/transactions", "object": "transaction"} def void(self): - self.update(status='voided') + self.update(status="voided") return self + class Payment(Transaction): - __spec__ = { 'polymorphic': { 'type': 'payment' } } + __spec__ = {"polymorphic": {"type": "payment"}} + class Refund(Transaction): - __spec__ = { 'polymorphic': { 'type': 'refund' } } + __spec__ = {"polymorphic": {"type": "refund"}} + class Credit(Transaction): - __spec__ = { 'polymorphic': { 'type': 'credit' } } + __spec__ = {"polymorphic": {"type": "credit"}} + class Deposit(Transaction): - __spec__ = { 'polymorphic': { 'type': 'deposit' } } + __spec__ = {"polymorphic": {"type": "deposit"}} + class Ledger(ARMObject): - __spec__ = { 'object': 'transaction_ledger' } + __spec__ = {"object": "transaction_ledger"} + class PaymentMethod(ARMObject): - __spec__ = { 'object': 'payment_method' } + __spec__ = {"object": "payment_method"} + class Card(PaymentMethod): - __spec__ = { 'polymorphic': { 'type': 'card' } } - field_map = set(['card_number', 'expiry', 'card_code']) + __spec__ = {"polymorphic": {"type": "card"}} + field_map = set(["card_number", "expiry", "card_code"]) + class BankAccount(PaymentMethod): - __spec__ = { 'polymorphic': { 'type': 'bank_account' } } - field_map = set(['account_number', 'routing_number', 'account_type']) + __spec__ = {"polymorphic": {"type": "bank_account"}} + field_map = set(["account_number", "routing_number", "account_type"]) + class BillingSchedule(ARMObject): - __spec__ = { 'object': 'billing_schedule' } + __spec__ = {"object": "billing_schedule"} + class BillingCharge(ARMObject): - __spec__ = { 'object': 'billing_charge' } + __spec__ = {"object": "billing_charge"} + class Invoice(ARMObject): - __spec__ = { 'object': 'invoice' } + __spec__ = {"object": "invoice"} + class LineItem(ARMObject): - __spec__ = { 'object': 'line_item' } + __spec__ = {"object": "line_item"} + class ChargeItem(LineItem): - __spec__ = { 'polymorphic': { 'entry_type': 'charge' } } + __spec__ = {"polymorphic": {"entry_type": "charge"}} + class PaymentItem(LineItem): - __spec__ = { 'polymorphic': { 'entry_type': 'payment' } } + __spec__ = {"polymorphic": {"entry_type": "payment"}} + class Webhook(ARMObject): - __spec__ = { 'object': 'webhook' } + __spec__ = {"object": "webhook"} + class PaymentLink(ARMObject): - __spec__ = { 'object': 'payment_link' } + __spec__ = {"object": "payment_link"} + class PaymentActivation(ARMObject): - __spec__ = { 'object': 'payment_activation' } + __spec__ = {"object": "payment_activation"} + + +# Introduced in API v2 +class Profile(ARMObject): + __spec__ = {"object": "profile"} + + +class BillingItem(ARMObject): + __spec__ = {"object": "billing_item"} + + +class Intent(ARMObject): + __spec__ = {"object": "intent"} + + +class InvoiceItem(ARMObject): + __spec__ = {"object": "invoice_item"} + + +class PaymentAllocation(ARMObject): + __spec__ = {"object": "payment_allocation"} + + +class Entity(ARMObject): + __spec__ = {"object": "entity"} + + +class Stakeholder(ARMObject): + __spec__ = {"object": "stakeholder"} + + +class ProcessingAgreement(ARMObject): + __spec__ = {"object": "processing_agreement"} + + +class Transfer(ARMObject): + __spec__ = {"object": "transfer"} + + +class TransactionOperation(ARMObject): + __spec__ = {"object": "transaction_operation"} + + +class CheckFront(ARMObject): + __spec__ = {"object": "check_front"} + + +class CheckBack(ARMObject): + __spec__ = {"object": "check_back"} + + +class ProcessingRule(ARMObject): + __spec__ = {"object": "processing_rule"} + +class ProcessingSettings(ARMObject): + __spec__ = {"object": "processing_settings"} diff --git a/pdm.lock b/pdm.lock index f35955e..4d6a102 100644 --- a/pdm.lock +++ b/pdm.lock @@ -3,10 +3,12 @@ [metadata] groups = ["default", "dev"] -cross_platform = true -static_urls = false -lock_version = "4.3" -content_hash = "sha256:3ee5e7ef83012dd8dc138af0f2a410020b0d6cf21df0fccb54205233362a419c" +strategy = ["cross_platform"] +lock_version = "4.5.0" +content_hash = "sha256:a64fb9823184a96d22c4d16e69685c05e756188e2aea4c16c74cca1eecd8c818" + +[[metadata.targets]] +requires_python = ">=3.7" [[package]] name = "astroid" diff --git a/pyproject.toml b/pyproject.toml index 5a3fc26..06f2c31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "payload-api" -version = "0.4.11" +version = "0.5.0" description = "Payload Python Library" authors = [ {name = "Payload", email = "help@payload.com"}, @@ -35,7 +35,8 @@ dev = [ "pytest", "python-dateutil", "mock", - "pylint" + "pylint", + "faker" ] [tool.pdm.scripts] diff --git a/tests/__init__.py b/tests/int/__init__.py similarity index 100% rename from tests/__init__.py rename to tests/int/__init__.py diff --git a/tests/fixtures.py b/tests/int/fixtures.py similarity index 95% rename from tests/fixtures.py rename to tests/int/fixtures.py index f52b614..884c5a6 100644 --- a/tests/fixtures.py +++ b/tests/int/fixtures.py @@ -3,11 +3,19 @@ import random import pytest +from faker import Faker import payload as pl +fake = Faker() + class Fixtures(object): + @staticmethod + def card_expiry(): + """Generate a valid card expiration date in MM/YY format.""" + return fake.credit_card_expire(date_format='%m/%y') + @pytest.fixture(scope='session', autouse=True) def api_key(self): pl.api_key = os.environ['TEST_SECRET_KEY'] @@ -78,7 +86,7 @@ def card_payment(self, processing_account): amount=random.random() * 100, payment_method=pl.Card( card_number='4242 4242 4242 4242', - expiry='12/35', + expiry=self.card_expiry(), card_code='123', billing_address=dict(postal_code='11111'), ), diff --git a/tests/test_access_token.py b/tests/int/test_access_token.py similarity index 100% rename from tests/test_access_token.py rename to tests/int/test_access_token.py diff --git a/tests/test_account.py b/tests/int/test_account.py similarity index 100% rename from tests/test_account.py rename to tests/int/test_account.py diff --git a/tests/test_billing.py b/tests/int/test_billing.py similarity index 100% rename from tests/test_billing.py rename to tests/int/test_billing.py diff --git a/tests/test_invoice.py b/tests/int/test_invoice.py similarity index 97% rename from tests/test_invoice.py rename to tests/int/test_invoice.py index fba5985..69a3d04 100644 --- a/tests/test_invoice.py +++ b/tests/int/test_invoice.py @@ -33,7 +33,7 @@ def test_pay_invoice(self, invoice, customer_account): card_payment = pl.Card.create( account_id=customer_account.id, card_number='4242 4242 4242 4242', - expiry='12/35', + expiry=self.card_expiry(), card_code='123', billing_address=dict(postal_code='11111'), ) diff --git a/tests/test_payment_link.py b/tests/int/test_payment_link.py similarity index 100% rename from tests/test_payment_link.py rename to tests/int/test_payment_link.py diff --git a/tests/test_payment_method.py b/tests/int/test_payment_method.py similarity index 95% rename from tests/test_payment_method.py rename to tests/int/test_payment_method.py index 60b0b79..ee1e199 100644 --- a/tests/test_payment_method.py +++ b/tests/int/test_payment_method.py @@ -26,7 +26,7 @@ def test_payment_filters(self, api_key, processing_account): processing_id=processing_account.id, payment_method=pl.Card( card_number='4242 4242 4242 4242', - expiry='05/35', + expiry=self.card_expiry(), card_code='123', billing_address=dict(postal_code='11111'), ), @@ -75,7 +75,7 @@ def test_blind_refund_card_payment(self, api_key, processing_account): amount=10, processing_id=processing_account.id, payment_method=pl.Card( - card_number='4242 4242 4242 4242', expiry='12/25', card_code='123' + card_number='4242 4242 4242 4242', expiry=self.card_expiry(), card_code='123' ), ) @@ -104,5 +104,5 @@ def test_partial_refund_bank_payment(self, api_key, bank_payment): def test_invalid_payment_method_type_invalid_attributes(self, api_key): with pytest.raises(BadRequest): pl.Transaction.create( - type='invalid', card_number='4242 4242 4242 4242', expiry='12/25' + type='invalid', card_number='4242 4242 4242 4242', expiry=self.card_expiry() ) diff --git a/tests/test_session.py b/tests/int/test_session.py similarity index 100% rename from tests/test_session.py rename to tests/int/test_session.py diff --git a/tests/test_transaction.py b/tests/int/test_transaction.py similarity index 93% rename from tests/test_transaction.py rename to tests/int/test_transaction.py index a7e3362..bdfab57 100644 --- a/tests/test_transaction.py +++ b/tests/int/test_transaction.py @@ -16,7 +16,7 @@ def test_unified_payout_batching(self, api_key, processing_account): amount=10, processing_id=processing_account.id, payment_method=pl.Card( - card_number="4242 4242 4242 4242", expiry="12/25", card_code="123" + card_number="4242 4242 4242 4242", expiry=self.card_expiry(), card_code="123" ), ) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/arm/__init__.py b/tests/unit/arm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_request.py b/tests/unit/arm/test_request.py similarity index 74% rename from tests/test_request.py rename to tests/unit/arm/test_request.py index bc1c19a..f7928fe 100644 --- a/tests/test_request.py +++ b/tests/unit/arm/test_request.py @@ -1,25 +1,23 @@ -from payload.utils import ( - get_object_cls, - map_object, - nested_qstring_keys, - data2object, - convert_fieldmap, - object2data, -) +import inspect +from unittest.mock import MagicMock, Mock, call, patch +from urllib.parse import urljoin -from payload.utils import get_object_cls -from mock import Mock, patch, call, create_autospec import pytest -import payload -from unittest.mock import Mock, patch -from payload.arm.request import ARMRequest -from urllib.parse import urljoin -import inspect +import payload import payload.objects as objects_module -from payload.objects import * from payload.arm.attr import Attr - +from payload.arm.object import ARMObject +from payload.arm.request import ARMRequest +from payload.arm.session import Session +from payload.objects import Account, Payment, PaymentItem, Transaction +from payload.utils import ( + convert_fieldmap, + data2object, + get_object_cls, + nested_qstring_keys, + object2data, +) arm_object_classes = [] @@ -53,7 +51,7 @@ def assert_mock_get_called_with_correct_values( ) if expected_files: - kwargs.update(dict(files=expected_files, data={})) + kwargs.update(dict(files=expected_files, data={}, headers={})) else: kwargs.update(dict(json=None, headers={})) @@ -71,7 +69,7 @@ def arm_request_from_class(arm_object_class, mock_session): @pytest.fixture def mock_session(): - return Mock(api_url='test', api_key='test') + return Mock(api_url='test', api_key='test', api_version=None) @pytest.fixture @@ -611,39 +609,14 @@ def test_get_object_cls(arm_object_class): assert get_object_cls(item_data) == expected -# Test nested_qstring_keys -@pytest.mark.parametrize( - 'base, expected', - [ - ({'a': {'b': {'c': 1}}}, {'a[b][c]': 1}), - ({'x': [{'y': 2}, {'z': 3}]}, {'x[0][y]': 2, 'x[1][z]': 3}), - ({}, {}), - ], -) -def test_nested_qstring_keys(base, expected): - assert nested_qstring_keys(base) == expected - - -# Test data2object -@pytest.mark.parametrize('arm_object_class', arm_object_classes) -def test_data2object(arm_object_class): - item_data = {'object': arm_object_class['object']} - if 'polymorphic' in arm_object_class: - item_data.update(arm_object_class['polymorphic']) - field_map = set() - session = None - expected = ( - arm_object_class['Object'](**item_data) if arm_object_class['Object'] else item_data - ) - assert data2object(item_data, field_map, session) is expected - - # Test convert_fieldmap @pytest.mark.parametrize('arm_object_class', arm_object_classes) -def test_convert_fieldmap(arm_object_class): +def test_convert_fieldmap_on_arm_object_classes(arm_object_class): obj = {'type': arm_object_class['object']} field_map = set(arm_object_class.keys()) - {'Object', 'object', 'polymorphic'} - expected = {arm_object_class['object']: {k: arm_object_class[k] for k in field_map}} + expected = obj.copy() + if mapped_fields := {k: arm_object_class[k] for k in field_map}: + expected[arm_object_class['object']] = mapped_fields convert_fieldmap(obj, field_map) assert obj == expected @@ -803,3 +776,206 @@ def test_nested_qstring_keys(base, expected): def test_data2object(item, field_map, session, expected): result = data2object(item, field_map, session) assert result == expected + + +class MockObject(ARMObject): + __spec__ = {'object': 'mock_object', 'endpoint': '/mock'} + + +class TestARMRequestApiVersion: + """Unit tests for ARMRequest._request api_version header functionality""" + + @patch('payload.arm.request.requests') + def test_api_version_header_included_when_set(self, mock_requests): + """Test that X-API-Version header is included when session.api_version is set""" + # Setup mock response + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + 'object': 'mock_object', + 'id': 'mock_123', + } + mock_requests.get.return_value = mock_response + + # Create session with api_version + session = Session( + api_key='test_key', api_url='https://api.test.com', api_version='v2.1' + ) + + # Create request and execute + request = ARMRequest(Object=MockObject, session=session) + request.get('mock_123') + + # Verify requests.get was called with X-API-Version header + mock_requests.get.assert_called_once() + call_kwargs = mock_requests.get.call_args[1] + assert 'headers' in call_kwargs + assert 'X-API-Version' in call_kwargs['headers'] + assert call_kwargs['headers']['X-API-Version'] == 'v2.1' + + @patch('payload.arm.request.requests') + def test_api_version_header_not_included_when_none(self, mock_requests): + """Test that X-API-Version header is not included when session.api_version is None""" + # Setup mock response + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + 'object': 'mock_object', + 'id': 'mock_123', + } + mock_requests.get.return_value = mock_response + + # Create session without api_version + session = Session(api_key='test_key', api_url='https://api.test.com', api_version=None) + + # Create request and execute + request = ARMRequest(Object=MockObject, session=session) + request.get('mock_123') + + # Verify requests.get was called without X-API-Version header + mock_requests.get.assert_called_once() + call_kwargs = mock_requests.get.call_args[1] + headers = call_kwargs.get('headers', {}) + assert 'X-API-Version' not in headers + + @patch('payload.arm.request.requests') + @patch('payload.api_version', 'v2.2') + def test_api_version_header_uses_global_payload_when_no_session(self, mock_requests): + """Test that X-API-Version header uses global payload.api_version when no session provided""" + # Setup mock response + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + 'object': 'mock_object', + 'id': 'mock_123', + } + mock_requests.get.return_value = mock_response + + # Create request without session (will use global payload module) + request = ARMRequest(Object=MockObject, session=None) + request.get('mock_123') + + # Verify requests.get was called with X-API-Version header from global payload + mock_requests.get.assert_called_once() + call_kwargs = mock_requests.get.call_args[1] + assert 'headers' in call_kwargs + assert 'X-API-Version' in call_kwargs['headers'] + assert call_kwargs['headers']['X-API-Version'] == 'v2.2' + + @patch('payload.arm.request.requests') + def test_api_version_header_in_post_request(self, mock_requests): + """Test that X-API-Version header is included in POST requests""" + # Setup mock response + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + 'object': 'mock_object', + 'id': 'mock_123', + } + mock_requests.post.return_value = mock_response + + # Create session with api_version + session = Session( + api_key='test_key', api_url='https://api.test.com', api_version='v2.3' + ) + + # Create request and execute + request = ARMRequest(Object=MockObject, session=session) + request.create({'field': 'value'}) + + # Verify requests.post was called with X-API-Version header + mock_requests.post.assert_called_once() + call_kwargs = mock_requests.post.call_args[1] + assert 'headers' in call_kwargs + assert 'X-API-Version' in call_kwargs['headers'] + assert call_kwargs['headers']['X-API-Version'] == 'v2.3' + + @patch('payload.arm.request.requests') + def test_api_version_header_in_put_request(self, mock_requests): + """Test that X-API-Version header is included in PUT requests""" + # Setup mock response + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + 'object': 'mock_object', + 'id': 'mock_123', + } + mock_requests.put.return_value = mock_response + + # Create session with api_version + session = Session( + api_key='test_key', api_url='https://api.test.com', api_version='v2.4' + ) + + # Create request and execute update + request = ARMRequest(Object=MockObject, session=session) + request.update(field='new_value') + + # Verify requests.put was called with X-API-Version header + mock_requests.put.assert_called_once() + call_kwargs = mock_requests.put.call_args[1] + assert 'headers' in call_kwargs + assert 'X-API-Version' in call_kwargs['headers'] + assert call_kwargs['headers']['X-API-Version'] == 'v2.4' + + @patch('payload.arm.request.requests') + def test_api_version_header_in_delete_request(self, mock_requests): + """Test that X-API-Version header is included in DELETE requests""" + # Setup mock response + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + 'object': 'mock_object', + 'id': 'mock_123', + } + mock_requests.delete.return_value = mock_response + + # Create session with api_version + session = Session( + api_key='test_key', api_url='https://api.test.com', api_version='v2.5' + ) + + # Create mock object to delete + mock_obj = MockObject() + mock_obj.id = 'mock_123' + + # Create request and execute delete + request = ARMRequest(Object=MockObject, session=session) + request.delete(mock_obj) + + # Verify requests.delete was called with X-API-Version header + mock_requests.delete.assert_called_once() + call_kwargs = mock_requests.delete.call_args[1] + assert 'headers' in call_kwargs + assert 'X-API-Version' in call_kwargs['headers'] + assert call_kwargs['headers']['X-API-Version'] == 'v2.5' + + @patch('payload.arm.request.requests') + def test_api_version_header_with_existing_headers(self, mock_requests): + """Test that X-API-Version header is merged with existing headers""" + # Setup mock response + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + 'object': 'mock_object', + 'id': 'mock_123', + } + mock_requests.get.return_value = mock_response + + # Create session with api_version + session = Session( + api_key='test_key', api_url='https://api.test.com', api_version='v2.6' + ) + + # Create request and execute with custom headers + request = ARMRequest(Object=MockObject, session=session) + request._request('get', id='mock_123', headers={'X-Custom-Header': 'custom_value'}) + + # Verify both headers are present + mock_requests.get.assert_called_once() + call_kwargs = mock_requests.get.call_args[1] + assert 'headers' in call_kwargs + assert 'X-API-Version' in call_kwargs['headers'] + assert 'X-Custom-Header' in call_kwargs['headers'] + assert call_kwargs['headers']['X-API-Version'] == 'v2.6' + assert call_kwargs['headers']['X-Custom-Header'] == 'custom_value'