diff --git a/mpt_api_client/constants.py b/mpt_api_client/constants.py new file mode 100644 index 00000000..438cc0cb --- /dev/null +++ b/mpt_api_client/constants.py @@ -0,0 +1 @@ +APPLICATION_JSON = "application/json" diff --git a/mpt_api_client/http/mixins.py b/mpt_api_client/http/mixins.py index 375d32a7..2ea73b84 100644 --- a/mpt_api_client/http/mixins.py +++ b/mpt_api_client/http/mixins.py @@ -3,6 +3,7 @@ from typing import Self from urllib.parse import urljoin +from mpt_api_client.constants import APPLICATION_JSON from mpt_api_client.http.query_state import QueryState from mpt_api_client.http.types import FileTypes, Response from mpt_api_client.models import Collection, FileModel, ResourceData @@ -84,7 +85,7 @@ def create( files[data_key] = ( None, _json_to_file_payload(resource_data), - "application/json", + APPLICATION_JSON, ) response = self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined] @@ -105,6 +106,77 @@ def download(self, resource_id: str) -> FileModel: return FileModel(response) +class CreateWithIconMixin[Model]: + """Create resource with icon mixin.""" + + def create( + self, + resource_data: ResourceData, + icon: FileTypes, + data_key: str, + icon_key: str, + ) -> Model: + """Create resource with icon. + + Args: + resource_data: Resource data. + data_key: Key for the resource data. + icon: Icon image in jpg, png, GIF, etc. + icon_key: Key for the icon. + + Returns: + Created resource. + """ + files: dict[str, FileTypes] = {} + files[data_key] = ( + None, + json.dumps(resource_data), + APPLICATION_JSON, + ) + files[icon_key] = icon + response = self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined] + + return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return] + + +class UpdateWithIconMixin[Model]: + """Update resource with icon mixin.""" + + def update( + self, + resource_id: str, + resource_data: ResourceData, + icon: FileTypes, + data_key: str, + icon_key: str, + ) -> Model: + """Update resource with icon. + + Args: + resource_id: Resource ID. + resource_data: Resource data. + data_key: Key for the resource data. + icon: Icon image in jpg, png, GIF, etc. + icon_key: Key for the icon. + + Returns: + Updated resource. + """ + files: dict[str, FileTypes] = {} + files[data_key] = ( + None, + json.dumps(resource_data), + APPLICATION_JSON, + ) + files[icon_key] = icon + + url = urljoin(f"{self.path}/", resource_id) # type: ignore[attr-defined] + + response = self.http_client.request("put", url, files=files) # type: ignore[attr-defined] + + return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return] + + class AsyncCreateMixin[Model]: """Create resource mixin.""" @@ -174,7 +246,7 @@ async def create( files[data_key] = ( None, _json_to_file_payload(resource_data), - "application/json", + APPLICATION_JSON, ) response = await self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined] @@ -196,6 +268,77 @@ async def download(self, resource_id: str) -> FileModel: return FileModel(response) +class AsyncCreateWithIconMixin[Model]: + """Create resource with icon mixin.""" + + async def create( + self, + resource_data: ResourceData, + icon: FileTypes, + data_key: str, + icon_key: str, + ) -> Model: + """Create resource with icon. + + Args: + resource_data: Resource data. + data_key: Key for the resource data. + icon: Icon image in jpg, png, GIF, etc. + icon_key: Key for the icon. + + Returns: + Created resource. + """ + files: dict[str, FileTypes] = {} + files[data_key] = ( + None, + json.dumps(resource_data), + APPLICATION_JSON, + ) + files[icon_key] = icon + response = await self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined] + + return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return] + + +class AsyncUpdateWithIconMixin[Model]: + """Update resource with icon mixin.""" + + async def update( + self, + resource_id: str, + resource_data: ResourceData, + icon: FileTypes, + data_key: str, + icon_key: str, + ) -> Model: + """Update resource with icon. + + Args: + resource_id: Resource ID. + resource_data: Resource data. + data_key: Key for the resource data. + icon: Icon image in jpg, png, GIF, etc. + icon_key: Key for the icon. + + Returns: + Updated resource. + """ + files: dict[str, FileTypes] = {} + files[data_key] = ( + None, + json.dumps(resource_data), + APPLICATION_JSON, + ) + files[icon_key] = icon + + url = urljoin(f"{self.path}/", resource_id) # type: ignore[attr-defined] + + response = await self.http_client.request("put", url, files=files) # type: ignore[attr-defined] + + return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return] + + class GetMixin[Model]: """Get resource mixin.""" diff --git a/mpt_api_client/resources/catalog/products.py b/mpt_api_client/resources/catalog/products.py index 21a04614..e98caf93 100644 --- a/mpt_api_client/resources/catalog/products.py +++ b/mpt_api_client/resources/catalog/products.py @@ -1,11 +1,17 @@ -import json +from typing import override from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncModifiableResourceMixin, + AsyncCreateWithIconMixin, + AsyncDeleteMixin, + AsyncGetMixin, + AsyncUpdateWithIconMixin, CollectionMixin, - ModifiableResourceMixin, + CreateWithIconMixin, + DeleteMixin, + GetMixin, + UpdateWithIconMixin, ) from mpt_api_client.http.types import FileTypes from mpt_api_client.models import Model, ResourceData @@ -56,38 +62,71 @@ class ProductsServiceConfig: class ProductsService( + CreateWithIconMixin[Product], + UpdateWithIconMixin[Product], PublishableMixin[Product], - ModifiableResourceMixin[Product], + GetMixin[Product], + DeleteMixin, CollectionMixin[Product], Service[Product], ProductsServiceConfig, ): """Products service.""" + @override def create( self, resource_data: ResourceData, icon: FileTypes, + data_key: str = "product", + icon_key: str = "icon", ) -> Product: """Create product with icon. Args: resource_data: Product data. icon: Icon image in jpg, png, GIF, etc. + data_key: Key for the product data. + icon_key: Key for the icon. Returns: Created resource. """ - files: dict[str, FileTypes] = {} - files["product"] = ( - None, - json.dumps(resource_data), - "application/json", + return super().create( + resource_data=resource_data, + icon=icon, + data_key=data_key, + icon_key=icon_key, ) - files["icon"] = icon - response = self.http_client.request("post", self.path, files=files) - return self._model_class.from_response(response) + @override + def update( + self, + resource_id: str, + resource_data: ResourceData, + icon: FileTypes, + data_key: str = "product", + icon_key: str = "icon", + ) -> Product: + """Update product with icon. + + Args: + resource_id: Product ID. + resource_data: Product data. + icon: Icon image in jpg, png, GIF, etc. + data_key: Key for the product data. + icon_key: Key for the icon. + + Returns: + Updated resource. + """ + return super().update( + resource_id=resource_id, + resource_data=resource_data, + icon=icon, + data_key=data_key, + icon_key=icon_key, + ) def item_groups(self, product_id: str) -> ItemGroupsService: """Return item_groups service.""" @@ -135,37 +174,71 @@ def update_settings(self, product_id: str, settings: ResourceData) -> Product: class AsyncProductsService( + AsyncCreateWithIconMixin[Product], + AsyncUpdateWithIconMixin[Product], AsyncPublishableMixin[Product], - AsyncModifiableResourceMixin[Product], + AsyncGetMixin[Product], + AsyncDeleteMixin, AsyncCollectionMixin[Product], AsyncService[Product], ProductsServiceConfig, ): """Products service.""" + @override async def create( self, resource_data: ResourceData, icon: FileTypes, + data_key: str = "product", + icon_key: str = "icon", ) -> Product: """Create product with icon. Args: resource_data: Product data. icon: Icon image in jpg, png, GIF, etc. + data_key: Key for the product data. + icon_key: Key for the icon. Returns: Created resource. """ - files: dict[str, FileTypes] = {} - files["product"] = ( - None, - json.dumps(resource_data), - "application/json", - ) - files["icon"] = icon - response = await self.http_client.request("post", self.path, files=files) - return self._model_class.from_response(response) + return await super().create( + resource_data=resource_data, + data_key=data_key, + icon=icon, + icon_key=icon_key, + ) + + @override + async def update( + self, + resource_id: str, + resource_data: ResourceData, + icon: FileTypes, + data_key: str = "product", + icon_key: str = "icon", + ) -> Product: + """Update product with icon. + + Args: + resource_id: Product ID. + resource_data: Product data. + icon: Icon image in jpg, png, GIF, etc. + data_key: Key for the product data. + icon_key: Key for the icon. + + Returns: + Updated resource. + """ + return await super().update( + resource_id=resource_id, + resource_data=resource_data, + data_key=data_key, + icon=icon, + icon_key=icon_key, + ) def item_groups(self, product_id: str) -> AsyncItemGroupsService: """Return item_groups service.""" diff --git a/setup.cfg b/setup.cfg index 766345c6..4bbe6d39 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,17 +33,17 @@ extend-ignore = per-file-ignores = mpt_api_client/mpt_client.py: WPS214 WPS235 - mpt_api_client/http/mixins.py: WPS202 + mpt_api_client/http/mixins.py: WPS202 WPS204 WPS235 mpt_api_client/resources/*: WPS215 mpt_api_client/models/model.py: WPS215 WPS110 mpt_api_client/resources/accounts/*.py: WPS202 WPS215 WPS214 mpt_api_client/resources/billing/*.py: WPS202 WPS204 WPS214 WPS215 - mpt_api_client/resources/catalog/*.py: WPS110 WPS214 WPS215 - mpt_api_client/resources/catalog/products.py: WPS204 WPS214 WPS215 + mpt_api_client/resources/catalog/*.py: WPS110 WPS214 WPS215 WPS235 + mpt_api_client/resources/catalog/products.py: WPS204 WPS214 WPS215 WPS235 mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214 tests/unit/http/test_async_service.py: WPS204 WPS202 tests/unit/http/test_service.py: WPS204 WPS202 - tests/unit/http/test_mixins.py: WPS204 WPS202 + tests/unit/http/test_mixins.py: WPS204 WPS202 WPS210 tests/unit/resources/catalog/test_products.py: WPS202 WPS210 tests/unit/resources/*/test_mixins.py: WPS118 WPS202 WPS204 WPS235 tests/unit/resources/accounts/test_users.py: WPS204 WPS202 WPS210 diff --git a/tests/e2e/catalog/product/test_async_product.py b/tests/e2e/catalog/product/test_async_product.py index af05d52c..ac01bb4c 100644 --- a/tests/e2e/catalog/product/test_async_product.py +++ b/tests/e2e/catalog/product/test_async_product.py @@ -22,10 +22,14 @@ def test_create_product(async_created_product, product_data): @pytest.mark.flaky -async def test_update_product(async_mpt_vendor, async_created_product): +async def test_update_product(async_mpt_vendor, async_created_product, product_icon): update_data = {"name": "Updated Product"} - product = await async_mpt_vendor.catalog.products.update(async_created_product.id, update_data) + product = await async_mpt_vendor.catalog.products.update( + async_created_product.id, + update_data, + icon=product_icon, + ) assert product.name == update_data["name"] diff --git a/tests/e2e/catalog/product/test_sync_product.py b/tests/e2e/catalog/product/test_sync_product.py index 3f793ff9..f680cec2 100644 --- a/tests/e2e/catalog/product/test_sync_product.py +++ b/tests/e2e/catalog/product/test_sync_product.py @@ -22,10 +22,10 @@ def test_create_product(created_product, product_data): @pytest.mark.flaky -def test_update_product(mpt_vendor, created_product): +def test_update_product(mpt_vendor, created_product, product_icon): update_data = {"name": "Updated Product"} - product = mpt_vendor.catalog.products.update(created_product.id, update_data) + product = mpt_vendor.catalog.products.update(created_product.id, update_data, icon=product_icon) assert product.name == update_data["name"] diff --git a/tests/unit/http/test_mixins.py b/tests/unit/http/test_mixins.py index f7e94a53..0d350357 100644 --- a/tests/unit/http/test_mixins.py +++ b/tests/unit/http/test_mixins.py @@ -7,12 +7,18 @@ from mpt_api_client import RQLQuery from mpt_api_client.exceptions import MPTAPIError +from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( + AsyncCreateWithIconMixin, AsyncManagedResourceMixin, AsyncModifiableResourceMixin, + AsyncUpdateWithIconMixin, + CreateWithIconMixin, ManagedResourceMixin, ModifiableResourceMixin, + UpdateWithIconMixin, ) +from mpt_api_client.http.types import FileTypes from mpt_api_client.resources.catalog.products_media import ( AsyncMediaService, MediaService, @@ -32,6 +38,96 @@ def async_media_service(async_http_client): ) +@pytest.fixture +def icon_service(http_client): + return DummyIconService(http_client=http_client) + + +@pytest.fixture +def async_icon_service(async_http_client): + return AsyncDummyIconService(http_client=async_http_client) + + +class DummyIconService( + CreateWithIconMixin[DummyModel], + UpdateWithIconMixin[DummyModel], + Service[DummyModel], +): + _endpoint = "/public/v1/dummy/icon/" + _model_class = DummyModel + _collection_key = "data" + + def create( + self, + resource_data: dict, + icon: FileTypes, + icon_key: str = "icon", + data_key: str = "data", + ) -> DummyModel: + return super().create( + resource_data=resource_data, + icon=icon, + icon_key=icon_key, + data_key=data_key, + ) + + def update( + self, + resource_id: str, + resource_data: dict, + icon: FileTypes, + icon_key: str = "icon", + data_key: str = "data", + ) -> DummyModel: + return super().update( + resource_id=resource_id, + resource_data=resource_data, + icon=icon, + icon_key=icon_key, + data_key=data_key, + ) + + +class AsyncDummyIconService( + AsyncCreateWithIconMixin[DummyModel], + AsyncUpdateWithIconMixin[DummyModel], + AsyncService[DummyModel], +): + _endpoint = "/public/v1/dummy/icon/" + _model_class = DummyModel + _collection_key = "data" + + async def create( + self, + resource_data: dict, + icon: FileTypes, + icon_key: str = "icon", + data_key: str = "data", + ) -> DummyModel: + return await super().create( + resource_data=resource_data, + icon=icon, + icon_key=icon_key, + data_key=data_key, + ) + + async def update( + self, + resource_id: str, + resource_data: dict, + icon: FileTypes, + icon_key: str = "icon", + data_key: str = "data", + ) -> DummyModel: + return await super().update( + resource_id=resource_id, + resource_data=resource_data, + icon=icon, + icon_key=icon_key, + data_key=data_key, + ) + + async def test_async_create_mixin(async_dummy_service): # noqa: WPS210 resource_data = {"name": "Test Resource", "status": "active"} new_resource_data = {"id": "new-resource-id", "name": "Test Resource", "status": "active"} @@ -942,3 +1038,187 @@ class AsyncManagedService(AsyncManagedResourceMixin[DummyModel]): # noqa: WPS43 f"AsyncManagedResourceMixin should have {method_name} method" ) assert callable(getattr(async_service, method_name)), f"{method_name} should be callable" + + +def test_sync_create_with_icon_with_data(icon_service): + resource_data = {"id": "OBJ-0000-0001", "name": "Icon Object"} + resource_key = "icon_data" + icon = ("icon.png", io.BytesIO(b"Icon content"), "image/png") + icon_key = "icon" + + with respx.mock: + mock_route = respx.post("https://api.example.com/public/v1/dummy/icon/").mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + json=resource_data, + ) + ) + new_resource = icon_service.create( + resource_data=resource_data, + icon=icon, + data_key=resource_key, + icon_key=icon_key, + ) + + request: httpx.Request = mock_route.calls[0].request + + assert ( + b'Content-Disposition: form-data; name="icon_data"\r\n' + b"Content-Type: application/json\r\n\r\n" + b'{"id": "OBJ-0000-0001", "name": "Icon Object"}\r\n' in request.content + ) + assert ( + b'Content-Disposition: form-data; name="icon"; filename="icon.png"\r\n' + b"Content-Type: image/png\r\n\r\n" + b"Icon content\r\n" in request.content + ) + assert "multipart/form-data" in request.headers["Content-Type"] + assert new_resource.to_dict() == resource_data + + +def test_sync_update_with_icon_with_data(icon_service): + resource_id = "OBJ-0000-0001" + resource_data = {"name": "Updated Icon Object"} + resource_key = "icon_data" + icon = ("icon.png", io.BytesIO(b"Updated icon content"), "image/png") + icon_key = "icon" + + with respx.mock: + mock_route = respx.put(f"https://api.example.com/public/v1/dummy/icon/{resource_id}").mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + json={"id": resource_id, "name": "Updated Icon Object"}, + ) + ) + updated_resource = icon_service.update( + resource_id, + resource_data=resource_data, + icon=icon, + data_key=resource_key, + icon_key=icon_key, + ) + + request: httpx.Request = mock_route.calls[0].request + + assert ( + b'Content-Disposition: form-data; name="icon_data"\r\n' + b"Content-Type: application/json\r\n\r\n" + b'{"name": "Updated Icon Object"}\r\n' in request.content + ) + assert ( + b'Content-Disposition: form-data; name="icon"; filename="icon.png"\r\n' + b"Content-Type: image/png\r\n\r\n" + b"Updated icon content\r\n" in request.content + ) + assert "multipart/form-data" in request.headers["Content-Type"] + assert updated_resource.to_dict() == { + "id": resource_id, + "name": "Updated Icon Object", + } + + +async def test_async_create_with_icon_no_data(async_icon_service): + resource_data = {"id": "OBJ-0000-0001", "name": "Icon Object"} + with respx.mock: + mock_route = respx.post("https://api.example.com/public/v1/dummy/icon/").mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + json=resource_data, + ) + ) + icon = ("icon.png", io.BytesIO(b"Icon content"), "image/png") + new_resource = await async_icon_service.create(resource_data=None, icon=icon) + + request: httpx.Request = mock_route.calls[0].request + + assert ( + b'Content-Disposition: form-data; name="icon"; filename="icon.png"\r\n' + b"Content-Type: image/png\r\n\r\n" + b"Icon content\r\n" in request.content + ) + assert new_resource.to_dict() == resource_data + + +def test_sync_create_with_icon_no_data(icon_service): + resource_data = {"id": "OBJ-0000-0001", "name": "Icon Object"} + with respx.mock: + mock_route = respx.post("https://api.example.com/public/v1/dummy/icon/").mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + json=resource_data, + ) + ) + icon = ("icon.png", io.BytesIO(b"Icon content"), "image/png") + new_resource = icon_service.create(resource_data=None, icon=icon) + + request: httpx.Request = mock_route.calls[0].request + + assert ( + b'Content-Disposition: form-data; name="icon"; filename="icon.png"\r\n' + b"Content-Type: image/png\r\n\r\n" + b"Icon content\r\n" in request.content + ) + assert new_resource.to_dict() == resource_data + + +async def test_async_create_with_icon_with_data(async_icon_service): + resource_data = {"id": "OBJ-0000-0001", "name": "Icon Object"} + with respx.mock: + mock_route = respx.post("https://api.example.com/public/v1/dummy/icon/").mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + json=resource_data, + ) + ) + icon = ("icon.png", io.BytesIO(b"Icon content"), "image/png") + new_resource = await async_icon_service.create(resource_data=None, icon=icon) + + request: httpx.Request = mock_route.calls[0].request + + assert ( + b'Content-Disposition: form-data; name="icon"; filename="icon.png"\r\n' + b"Content-Type: image/png\r\n\r\n" + b"Icon content\r\n" in request.content + ) + assert new_resource.to_dict() == resource_data + + +async def test_async_update_with_icon_with_data(async_icon_service): + resource_id = "OBJ-0000-0001" + resource_data = {"name": "Updated Icon Object"} + resource_key = "icon_data" + icon = ("icon.png", io.BytesIO(b"Updated icon content"), "image/png") + icon_key = "icon" + + with respx.mock: + mock_route = respx.put(f"https://api.example.com/public/v1/dummy/icon/{resource_id}").mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + json={"id": resource_id, "name": "Updated Icon Object"}, + ) + ) + updated_resource = await async_icon_service.update( + resource_id, + resource_data=resource_data, + icon=icon, + data_key=resource_key, + icon_key=icon_key, + ) + + request: httpx.Request = mock_route.calls[0].request + + assert ( + b'Content-Disposition: form-data; name="icon_data"\r\n' + b"Content-Type: application/json\r\n\r\n" + b'{"name": "Updated Icon Object"}\r\n' in request.content + ) + assert ( + b'Content-Disposition: form-data; name="icon"; filename="icon.png"\r\n' + b"Content-Type: image/png\r\n\r\n" + b"Updated icon content\r\n" in request.content + ) + assert "multipart/form-data" in request.headers["Content-Type"] + assert updated_resource.to_dict() == { + "id": resource_id, + "name": "Updated Icon Object", + } diff --git a/tests/unit/resources/catalog/test_products.py b/tests/unit/resources/catalog/test_products.py index 6d159aaf..8b0bd5f3 100644 --- a/tests/unit/resources/catalog/test_products.py +++ b/tests/unit/resources/catalog/test_products.py @@ -142,7 +142,6 @@ def test_product_create(products_service, tmp_path): product_data = {"name": "New Product", "category": "Books"} expected_response = {"id": "PRD-123", "name": "New Product", "category": "Books"} - # Create a temporary icon file icon_path = tmp_path / "icon.png" icon_path.write_bytes(b"fake image data") with icon_path.open("rb") as icon_file, respx.mock: @@ -179,3 +178,55 @@ async def test_async_product_create(async_products_service, tmp_path): assert request.method == "POST" assert request.url.path == "/public/v1/catalog/products" assert product.to_dict() == expected_response + + +def test_sync_product_update(products_service, tmp_path): + """Test updating a product (sync).""" + product_id = "PRD-123" + update_data = {"name": "Updated Product", "category": "Electronics"} + expected_response = {"id": product_id, "name": "Updated Product", "category": "Electronics"} + + icon_path = tmp_path / "icon.png" + icon_path.write_bytes(b"fake updated image data") + with icon_path.open("rb") as icon_file, respx.mock: + mock_route = respx.put( + f"https://api.example.com/public/v1/catalog/products/{product_id}" + ).mock(return_value=httpx.Response(httpx.codes.OK, json=expected_response)) + + product = products_service.update( + product_id, + update_data, + icon=icon_file, + ) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.method == "PUT" + assert request.url.path == f"/public/v1/catalog/products/{product_id}" + assert product.to_dict() == expected_response + + +async def test_async_product_update(async_products_service, tmp_path): + """Test updating a product (async).""" + product_id = "PRD-456" + update_data = {"name": "Async Updated Product", "category": "Gadgets"} + expected_response = {"id": product_id, "name": "Async Updated Product", "category": "Gadgets"} + + icon_path = tmp_path / "icon.png" + icon_path.write_bytes(b"fake async updated image data") + with icon_path.open("rb") as icon_file, respx.mock: + mock_route = respx.put( + f"https://api.example.com/public/v1/catalog/products/{product_id}" + ).mock(return_value=httpx.Response(httpx.codes.OK, json=expected_response)) + + product = await async_products_service.update( + product_id, + update_data, + icon=icon_file, + ) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.method == "PUT" + assert request.url.path == f"/public/v1/catalog/products/{product_id}" + assert product.to_dict() == expected_response diff --git a/tests/unit/test_constants.py b/tests/unit/test_constants.py new file mode 100644 index 00000000..387ed128 --- /dev/null +++ b/tests/unit/test_constants.py @@ -0,0 +1,5 @@ +from mpt_api_client.constants import APPLICATION_JSON + + +def test_expected_constants(): + assert APPLICATION_JSON == "application/json"