From a27a1bd986e7398ecce4dc868b223aba0d869645 Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Fri, 6 Dec 2024 20:46:05 +0100 Subject: [PATCH 1/6] changed datetime to include milliseconds to ensure correct ordering on dateadded desc --- mock-products.json | 310 ------------------ peterpy/database/models/product.py | 7 +- peterpy/handlers/product_handler.py | 12 +- peterpy/interfaces/repository.py | 2 +- .../database_product_repository.py | 22 +- peterpy/services/product_service.py | 7 +- tests/count_price.py | 16 + .../test_product_handler_integration.py | 104 +++--- tests/handlers/test_product_handler_unit.py | 2 + tests/mock-products.json | 12 + .../test_database_product_repository.py | 2 +- 11 files changed, 107 insertions(+), 389 deletions(-) delete mode 100644 mock-products.json create mode 100644 tests/count_price.py create mode 100644 tests/mock-products.json diff --git a/mock-products.json b/mock-products.json deleted file mode 100644 index a27dfc1..0000000 --- a/mock-products.json +++ /dev/null @@ -1,310 +0,0 @@ -[ - { - "name": "Orange", - "price": 0.5 - }, - { - "name": "Apple", - "price": 0.6 - }, - { - "name": "Banana", - "price": 0.4 - }, - { - "name": "Papaya", - "price": 0.7 - }, - { - "name": "Grapes", - "price": 0.8 - }, - { - "name": "Mango", - "price": 0.9 - }, - { - "name": "Pineapple", - "price": 1.0 - }, - { - "name": "Watermelon", - "price": 1.1 - }, - { - "name": "Strawberry", - "price": 1.2 - }, - { - "name": "Blueberry", - "price": 1.3 - }, - { - "name": "Raspberry", - "price": 1.4 - }, - { - "name": "Blackberry", - "price": 1.5 - }, - { - "name": "Cherry", - "price": 1.6 - }, - { - "name": "Peach", - "price": 1.7 - }, - { - "name": "Plum", - "price": 1.8 - }, - { - "name": "Apricot", - "price": 1.9 - }, - { - "name": "Kiwi", - "price": 2.0 - }, - { - "name": "Pomegranate", - "price": 2.1 - }, - { - "name": "Cranberry", - "price": 2.2 - }, - { - "name": "Gooseberry", - "price": 2.3 - }, - { - "name": "Lemon", - "price": 2.4 - }, - { - "name": "Lime", - "price": 2.5 - }, - { - "name": "Coconut", - "price": 2.6 - }, - { - "name": "Guava", - "price": 2.7 - }, - { - "name": "Lychee", - "price": 2.8 - }, - { - "name": "Passion Fruit", - "price": 2.9 - }, - { - "name": "Dragon Fruit", - "price": 3.0 - }, - { - "name": "Mangosteen", - "price": 3.1 - }, - { - "name": "Durian", - "price": 3.2 - }, - { - "name": "Jackfruit", - "price": 3.3 - }, - { - "name": "Starfruit", - "price": 3.4 - }, - { - "name": "Acai Berry", - "price": 3.5 - }, - { - "name": "Cantaloupe", - "price": 3.6 - }, - { - "name": "Honeydew", - "price": 3.7 - }, - { - "name": "Mandarin", - "price": 3.8 - }, - { - "name": "Tangerine", - "price": 3.9 - }, - { - "name": "Nectarine", - "price": 4.0 - }, - { - "name": "Grapefruit", - "price": 4.1 - }, - { - "name": "Clementine", - "price": 4.2 - }, - { - "name": "Persimmon", - "price": 4.3 - }, - { - "name": "Rambutan", - "price": 4.4 - }, - { - "name": "Longan", - "price": 4.5 - }, - { - "name": "Kumquat", - "price": 4.6 - }, - { - "name": "Pitaya", - "price": 4.7 - }, - { - "name": "Pawpaw", - "price": 4.8 - }, - { - "name": "Soursop", - "price": 4.9 - }, - { - "name": "Cherimoya", - "price": 5.0 - }, - { - "name": "Sapodilla", - "price": 5.1 - }, - { - "name": "Tamarillo", - "price": 5.2 - }, - { - "name": "Feijoa", - "price": 5.3 - }, - { - "name": "Pepino", - "price": 5.4 - }, - { - "name": "Ugli Fruit", - "price": 5.5 - }, - { - "name": "Yuzu", - "price": 5.6 - }, - { - "name": "Pomelo", - "price": 5.7 - }, - { - "name": "Mamey", - "price": 5.8 - }, - { - "name": "Papaya", - "price": 5.9 - }, - { - "name": "Acerola", - "price": 6.0 - }, - { - "name": "Barbados Cherry", - "price": 6.1 - }, - { - "name": "Bilimbi", - "price": 6.2 - }, - { - "name": "Black Sapote", - "price": 6.3 - }, - { - "name": "Breadfruit", - "price": 6.4 - }, - { - "name": "Buddha's Hand", - "price": 6.5 - }, - { - "name": "Calamondin", - "price": 6.6 - }, - { - "name": "Canistel", - "price": 6.7 - }, - { - "name": "Cempedak", - "price": 6.8 - }, - { - "name": "Chayote", - "price": 6.9 - }, - { - "name": "Cupuacu", - "price": 7.0 - }, - { - "name": "Damson", - "price": 7.1 - }, - { - "name": "Date", - "price": 7.2 - }, - { - "name": "Duku", - "price": 7.3 - }, - { - "name": "Feijoa", - "price": 7.4 - }, - { - "name": "Finger Lime", - "price": 7.5 - }, - { - "name": "Gac", - "price": 7.6 - }, - { - "name": "Galia Melon", - "price": 7.7 - }, - { - "name": "Genip", - "price": 7.8 - }, - { - "name": "Horned Melon", - "price": 7.9 - }, - { - "name": "Jabuticaba", - "price": 8.0 - } -] diff --git a/peterpy/database/models/product.py b/peterpy/database/models/product.py index bf6d099..2dbb4a7 100644 --- a/peterpy/database/models/product.py +++ b/peterpy/database/models/product.py @@ -1,6 +1,7 @@ from datetime import datetime, timezone -from sqlalchemy import BigInteger, Column, DateTime, String +from sqlalchemy import BigInteger, Column, String +from sqlalchemy.dialects.mysql import DATETIME from peterpy.database.models.base import Base @@ -12,9 +13,7 @@ class Product(Base): product_id = Column(String(255), primary_key=True) name = Column(String(255), nullable=False, unique=True) price = Column(BigInteger, nullable=False) - date_added = Column( - DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) - ) + date_added = Column(DATETIME(fsp=6), default=lambda: datetime.now(timezone.utc)) def __repr__(self) -> str: return ( diff --git a/peterpy/handlers/product_handler.py b/peterpy/handlers/product_handler.py index a12f742..3a60ee5 100644 --- a/peterpy/handlers/product_handler.py +++ b/peterpy/handlers/product_handler.py @@ -13,6 +13,9 @@ async def list_products(request: Request) -> Response: logging.debug("---------------------------------") logging.info("List products requested from %s", request.remote) + page = int(request.query.get("page", 1)) + limit = int(request.query.get("limit", 20)) + # converting the products to a list here # removes the memory advantage of using a generator # really using the benefits of a generator would be to use it @@ -20,7 +23,7 @@ async def list_products(request: Request) -> Response: # and stream the response items one by one back to the client # TODO: investigate how to stream the response item by item back to the client - products = list(product_service.all()) + products = list(product_service.all(page, limit)) return json_response(status=200, content={"products": products}) @@ -91,14 +94,11 @@ async def get_dashboard(request: Request) -> Response: logging.info("Dashboard requested from %s", request.remote) products_count = product_service.count() - products = product_service.all() - # products_total_value = sum([product.price for product in products]) - # below version is more efficient because it uses a generator expression - products_total_value = sum(product.price for product in products) + products_sum = product_service.sum() output = { "products_count": products_count, - "products_total_value": products_total_value, + "products_total_value": products_sum, } return json_response(status=200, content={"dashboard": output}) diff --git a/peterpy/interfaces/repository.py b/peterpy/interfaces/repository.py index c27085f..5af4863 100644 --- a/peterpy/interfaces/repository.py +++ b/peterpy/interfaces/repository.py @@ -34,7 +34,7 @@ def find_one(self, obj_id: UUID) -> T: raise NotImplementedError @abstractmethod - def all(self) -> Generator[T, None, None]: + def all(self, page: int, limit: int) -> Generator[T, None, None]: raise NotImplementedError @abstractmethod diff --git a/peterpy/repositories/database_product_repository.py b/peterpy/repositories/database_product_repository.py index 0321bc7..fb1f035 100644 --- a/peterpy/repositories/database_product_repository.py +++ b/peterpy/repositories/database_product_repository.py @@ -2,7 +2,7 @@ from typing import Dict, Generator, List from uuid import UUID -from sqlalchemy import select +from sqlalchemy import select, func from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session @@ -72,15 +72,27 @@ def find(self, query: Dict[str, str]) -> list: def find_one(self, obj_id: UUID) -> ProductEntity: raise NotImplementedError - def all(self) -> Generator[ProductEntity, None, None]: - stmt = select(ProductModel).order_by(ProductModel.date_added.desc()) + def all(self, page: int, limit: int) -> Generator[ProductEntity, None, None]: + offset = (page - 1) * limit + stmt = ( + select(ProductModel) + .order_by(ProductModel.date_added.desc()) + .offset(offset) + .limit(limit) + ) for product in self.session.execute(stmt): entity = product_model_to_entity(product[0]) yield entity + def sum(self) -> float: + stmt = select(func.sum(ProductModel.price)) + result = self.session.execute(stmt).scalar() + return result or 0.0 + def count(self) -> int: - products = self.all() - return len(list(products)) + stmt = select(func.count(ProductModel.product_id)) + result = self.session.execute(stmt).scalar() + return result or 0 def clear(self) -> None: raise NotImplementedError diff --git a/peterpy/services/product_service.py b/peterpy/services/product_service.py index c1d1e11..c9f1b46 100644 --- a/peterpy/services/product_service.py +++ b/peterpy/services/product_service.py @@ -48,9 +48,12 @@ def find(self, query: Dict[str, str]): def find_one(self, product_id): return self.repository.find_one(product_id) - def all(self): - generator_products = self.repository.all() + def all(self, page: int, limit: int): + generator_products = self.repository.all(page, limit) return generator_products def count(self): return self.repository.count() + + def sum(self): + return self.repository.sum() diff --git a/tests/count_price.py b/tests/count_price.py new file mode 100644 index 0000000..c64cb68 --- /dev/null +++ b/tests/count_price.py @@ -0,0 +1,16 @@ +import json +import os + + +def load_json_file(file_path): + with open(file_path, "r") as file: + return json.load(file) + + +mock_products_file_path = os.path.join(os.path.dirname(__file__), "mock-products.json") +mock_data = load_json_file(mock_products_file_path) + +# Calculate the total price +total_price = sum(product["price"] for product in mock_data) + +print(f"Total price: {total_price}") diff --git a/tests/handlers/test_product_handler_integration.py b/tests/handlers/test_product_handler_integration.py index f47dda3..bad29fa 100644 --- a/tests/handlers/test_product_handler_integration.py +++ b/tests/handlers/test_product_handler_integration.py @@ -1,4 +1,6 @@ +from asyncio import sleep import json +import os from unittest.mock import ANY, AsyncMock, Mock, patch import pytest @@ -11,23 +13,31 @@ from tests.helpers import create_uuid_from_string +def load_json_file(file_path): + with open(file_path, "r") as file: + return json.load(file) + + +mock_products_file_path = os.path.join( + os.path.dirname(__file__), "..", "mock-products.json" +) +mock_data = load_json_file(mock_products_file_path) + + class TestProductHandlers(BaseHandlerTestCase): - def seed(self): - # Seed the database with some products - product_1 = ProductModel( - product_id=str(create_uuid_from_string("p10")), - name="product_10", - price=10.0, - ) - product_2 = ProductModel( - product_id=str(create_uuid_from_string("p20")), - name="product_20", - price=10.0, - ) + async def seed(self): + mock_products = [] + for product in mock_data: + await sleep(0.4) + product_model = ProductModel( + product_id=str(create_uuid_from_string(product["name"])), + name=product["name"], + price=product["price"], + ) + mock_products.append(product_model) with DatabaseSession(self.connection.engine()) as session: - session.add(product_1) - session.add(product_2) + session.add_all(mock_products) session.commit() @patch( @@ -84,9 +94,9 @@ async def test_add_one_product_integration(self): in the middleware. .""" product_added_request_integration = Product( - product_id=create_uuid_from_string("product_added_request-1"), - name="product_added_request", - price=20.0, + product_id=create_uuid_from_string("Apple"), + name="Apple", + price=10.0, ) response = await self.client.request( @@ -118,35 +128,8 @@ async def test_add_products_and_dashboard_integration(self): in the middleware using the connection that is setup using Sqlite setup in the BaseHandlerTestCase. """ - product_added_request_integration = Product( - product_id=create_uuid_from_string("product_added_request-1"), - name="product_added_request", - price=20.0, - ) - product_added_request_integration2 = Product( - product_id=create_uuid_from_string("product_added_request-2"), - name="product_added_request 2", - price=50.0, - ) - response = await self.client.request( - "POST", - "/products", - json={ - "products": [ - { - "name": product_added_request_integration.name, - "price": product_added_request_integration.price, - }, - { - "name": product_added_request_integration2.name, - "price": product_added_request_integration2.price, - }, - ] - }, - ) - - assert response.status == 201 + await self.seed() response_dash = await self.client.request( "GET", @@ -155,10 +138,11 @@ async def test_add_products_and_dashboard_integration(self): assert response_dash.status == 200 response_body = await response_dash.text() + assert json.loads(response_body) == { "dashboard": { - "products_count": 2, - "products_total_value": 70.0, + "products_count": 10, + "products_total_value": 4690, } } @@ -167,24 +151,24 @@ async def test_list_products_integration(self): """ Test list products integration including the client """ - self.seed() + await self.seed() - response = await self.client.request("GET", "/product/list") + response = await self.client.request("GET", "/product/list?page=1&limit=2") assert response.status == 200 response_body = await response.text() assert json.loads(response_body) == { "products": [ { - "product_id": str(create_uuid_from_string("p20")), - "name": "product_20", - "price": 10.0, + "product_id": str(create_uuid_from_string("Tamarillo")), + "name": "Tamarillo", + "price": 495.0, "date_added": ANY, }, { - "product_id": str(create_uuid_from_string("p10")), - "name": "product_10", - "price": 10.0, + "product_id": str(create_uuid_from_string("Santol")), + "name": "Santol", + "price": 490.0, "date_added": ANY, }, ] @@ -193,18 +177,18 @@ async def test_list_products_integration(self): @pytest.mark.asyncio async def test_get_one_product_integration(self): """Test get one product endpoint""" - self.seed() + await self.seed() response = await self.client.request( - "GET", f"/product/{str(create_uuid_from_string("p20"))}" + "GET", f"/product/{str(create_uuid_from_string("Tamarillo"))}" ) assert response.status == 200 response_body = await response.text() assert json.loads(response_body) == { "product": { - "product_id": str(create_uuid_from_string("p20")), - "name": "product_20", - "price": 10.0, + "product_id": str(create_uuid_from_string("Tamarillo")), + "name": "Tamarillo", + "price": 495.0, "date_added": ANY, } } diff --git a/tests/handlers/test_product_handler_unit.py b/tests/handlers/test_product_handler_unit.py index 805ae5a..bac8c99 100644 --- a/tests/handlers/test_product_handler_unit.py +++ b/tests/handlers/test_product_handler_unit.py @@ -35,10 +35,12 @@ def products_generator(): request = Mock(spec=Request) request.__getitem__ = Mock(spec=ProductService) + request.query = {"page": 1, "limit": 10} request["product_service"].all.return_value = products_generator() response = await list_products(request) assert response.status == 200 response_json = json.loads(response.text) + assert response_json["products"][0]["name"] == "product_1" assert response_json["products"][1]["name"] == "product_2" diff --git a/tests/mock-products.json b/tests/mock-products.json new file mode 100644 index 0000000..258a55c --- /dev/null +++ b/tests/mock-products.json @@ -0,0 +1,12 @@ +[ + { "name": "Jostaberry", "price": 445 }, + { "name": "Kaffir Lime", "price": 450 }, + { "name": "Longan", "price": 455 }, + { "name": "Mangosteen", "price": 460 }, + { "name": "Nutmeg", "price": 465 }, + { "name": "Oregon Grape", "price": 470 }, + { "name": "Pawpaw", "price": 475 }, + { "name": "Raisin Tree", "price": 485 }, + { "name": "Santol", "price": 490 }, + { "name": "Tamarillo", "price": 495 } +] diff --git a/tests/repositories/test_database_product_repository.py b/tests/repositories/test_database_product_repository.py index 1faa6f2..7684bc1 100644 --- a/tests/repositories/test_database_product_repository.py +++ b/tests/repositories/test_database_product_repository.py @@ -78,5 +78,5 @@ def test_repository_find_product_not_found(repository): def test_repository_all_products(repository): - products = repository.all() + products = repository.all(page=1, limit=10) assert len(list(products)) == 2 From d1e6ff90d9a9687aa922ce5b6b33a6b8100563e6 Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Fri, 6 Dec 2024 22:47:25 +0100 Subject: [PATCH 2/6] cant assert on dateadded sorting on seeded products, its too fast --- .../test_product_handler_integration.py | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/tests/handlers/test_product_handler_integration.py b/tests/handlers/test_product_handler_integration.py index bad29fa..ab0d19b 100644 --- a/tests/handlers/test_product_handler_integration.py +++ b/tests/handlers/test_product_handler_integration.py @@ -25,10 +25,9 @@ def load_json_file(file_path): class TestProductHandlers(BaseHandlerTestCase): - async def seed(self): + def seed(self): mock_products = [] for product in mock_data: - await sleep(0.4) product_model = ProductModel( product_id=str(create_uuid_from_string(product["name"])), name=product["name"], @@ -37,9 +36,28 @@ async def seed(self): mock_products.append(product_model) with DatabaseSession(self.connection.engine()) as session: - session.add_all(mock_products) + for product in mock_products: + session.add(product) session.commit() + # def seed(self): + # # Seed the database with some products + # product_1 = ProductModel( + # product_id=str(create_uuid_from_string("p10")), + # name="p10", + # price=10.0, + # ) + # product_2 = ProductModel( + # product_id=str(create_uuid_from_string("p20")), + # name="p20", + # price=10.0, + # ) + + # with DatabaseSession(self.connection.engine()) as session: + # session.add(product_1) + # session.add(product_2) + # session.commit() + @patch( "peterpy.middlewares.ProductService", return_value=Mock(spec=ProductService), @@ -120,7 +138,7 @@ async def test_add_one_product_integration(self): } @pytest.mark.asyncio - async def test_add_products_and_dashboard_integration(self): + async def test_dashboard_integration(self): """ Test add products and dashboard full integration, without any mocks from making the request to the client, where the service @@ -129,7 +147,7 @@ async def test_add_products_and_dashboard_integration(self): using Sqlite setup in the BaseHandlerTestCase. """ - await self.seed() + self.seed() response_dash = await self.client.request( "GET", @@ -151,33 +169,18 @@ async def test_list_products_integration(self): """ Test list products integration including the client """ - await self.seed() + self.seed() - response = await self.client.request("GET", "/product/list?page=1&limit=2") + response = await self.client.request("GET", "/product/list?page=1&limit=5") assert response.status == 200 response_body = await response.text() - assert json.loads(response_body) == { - "products": [ - { - "product_id": str(create_uuid_from_string("Tamarillo")), - "name": "Tamarillo", - "price": 495.0, - "date_added": ANY, - }, - { - "product_id": str(create_uuid_from_string("Santol")), - "name": "Santol", - "price": 490.0, - "date_added": ANY, - }, - ] - } + assert len(json.loads(response_body)["products"]) == 5 @pytest.mark.asyncio async def test_get_one_product_integration(self): """Test get one product endpoint""" - await self.seed() + self.seed() response = await self.client.request( "GET", f"/product/{str(create_uuid_from_string("Tamarillo"))}" ) From 21858c70a9e9e19410f1f8c256ae0e6dfc0ce06e Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Fri, 6 Dec 2024 22:54:18 +0100 Subject: [PATCH 3/6] fix lint --- peterpy/repositories/database_product_repository.py | 1 + peterpy/repositories/memory_product_repository.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/peterpy/repositories/database_product_repository.py b/peterpy/repositories/database_product_repository.py index fb1f035..c82a5d2 100644 --- a/peterpy/repositories/database_product_repository.py +++ b/peterpy/repositories/database_product_repository.py @@ -90,6 +90,7 @@ def sum(self) -> float: return result or 0.0 def count(self) -> int: + # pylint: disable=not-callable stmt = select(func.count(ProductModel.product_id)) result = self.session.execute(stmt).scalar() return result or 0 diff --git a/peterpy/repositories/memory_product_repository.py b/peterpy/repositories/memory_product_repository.py index 164a1cb..2af2028 100644 --- a/peterpy/repositories/memory_product_repository.py +++ b/peterpy/repositories/memory_product_repository.py @@ -41,7 +41,7 @@ def find(self, query: Dict[str, str]) -> list: def find_one(self, obj_id: UUID) -> Product: return self.items[obj_id] - def all(self) -> Generator[Product, None, None]: + def all(self, page: int = 2, limit: int = 20) -> Generator[Product, None, None]: # for product in self.items.values(): # yield product yield from self.items.values() From 23cd75887868fb67fe61624a993b511fc125ca09 Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Fri, 6 Dec 2024 23:03:54 +0100 Subject: [PATCH 4/6] asserting page 2 and 3 --- .../repositories/memory_product_repository.py | 2 +- .../test_product_handler_integration.py | 33 +++++++------------ tests/mock-products.json | 3 +- .../test_memory_product_repository.py | 2 +- 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/peterpy/repositories/memory_product_repository.py b/peterpy/repositories/memory_product_repository.py index 2af2028..c9846ed 100644 --- a/peterpy/repositories/memory_product_repository.py +++ b/peterpy/repositories/memory_product_repository.py @@ -41,7 +41,7 @@ def find(self, query: Dict[str, str]) -> list: def find_one(self, obj_id: UUID) -> Product: return self.items[obj_id] - def all(self, page: int = 2, limit: int = 20) -> Generator[Product, None, None]: + def all(self, page: int, limit: int) -> Generator[Product, None, None]: # for product in self.items.values(): # yield product yield from self.items.values() diff --git a/tests/handlers/test_product_handler_integration.py b/tests/handlers/test_product_handler_integration.py index ab0d19b..4a72eca 100644 --- a/tests/handlers/test_product_handler_integration.py +++ b/tests/handlers/test_product_handler_integration.py @@ -40,24 +40,6 @@ def seed(self): session.add(product) session.commit() - # def seed(self): - # # Seed the database with some products - # product_1 = ProductModel( - # product_id=str(create_uuid_from_string("p10")), - # name="p10", - # price=10.0, - # ) - # product_2 = ProductModel( - # product_id=str(create_uuid_from_string("p20")), - # name="p20", - # price=10.0, - # ) - - # with DatabaseSession(self.connection.engine()) as session: - # session.add(product_1) - # session.add(product_2) - # session.commit() - @patch( "peterpy.middlewares.ProductService", return_value=Mock(spec=ProductService), @@ -159,8 +141,8 @@ async def test_dashboard_integration(self): assert json.loads(response_body) == { "dashboard": { - "products_count": 10, - "products_total_value": 4690, + "products_count": 11, + "products_total_value": 5190, } } @@ -172,11 +154,20 @@ async def test_list_products_integration(self): self.seed() response = await self.client.request("GET", "/product/list?page=1&limit=5") - assert response.status == 200 response_body = await response.text() assert len(json.loads(response_body)["products"]) == 5 + response2 = await self.client.request("GET", "/product/list?page=2&limit=5") + assert response2.status == 200 + response_body2 = await response2.text() + assert len(json.loads(response_body2)["products"]) == 5 + + response3 = await self.client.request("GET", "/product/list?page=3&limit=5") + assert response3.status == 200 + response_body3 = await response3.text() + assert len(json.loads(response_body3)["products"]) == 1 + @pytest.mark.asyncio async def test_get_one_product_integration(self): """Test get one product endpoint""" diff --git a/tests/mock-products.json b/tests/mock-products.json index 258a55c..90a95a1 100644 --- a/tests/mock-products.json +++ b/tests/mock-products.json @@ -8,5 +8,6 @@ { "name": "Pawpaw", "price": 475 }, { "name": "Raisin Tree", "price": 485 }, { "name": "Santol", "price": 490 }, - { "name": "Tamarillo", "price": 495 } + { "name": "Tamarillo", "price": 495 }, + { "name": "Another Tamarillo", "price": 500 } ] diff --git a/tests/repositories/test_memory_product_repository.py b/tests/repositories/test_memory_product_repository.py index 29fea97..493392c 100644 --- a/tests/repositories/test_memory_product_repository.py +++ b/tests/repositories/test_memory_product_repository.py @@ -56,7 +56,7 @@ def test_find_one_product(repository, product): def test_all_products(repository, product): repository.add(product) - all_products = list(repository.all()) + all_products = list(repository.all(1, 10)) assert len(all_products) == 1 assert all_products[0] == product From f66f331a184f041056d21dc1f69e3ea4c998fc50 Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Sun, 8 Dec 2024 09:45:28 +0100 Subject: [PATCH 5/6] using timedelta to control dateadded in seeded test products --- .../test_product_handler_integration.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/handlers/test_product_handler_integration.py b/tests/handlers/test_product_handler_integration.py index 4a72eca..16db3f9 100644 --- a/tests/handlers/test_product_handler_integration.py +++ b/tests/handlers/test_product_handler_integration.py @@ -1,4 +1,4 @@ -from asyncio import sleep +from datetime import datetime, timedelta import json import os from unittest.mock import ANY, AsyncMock, Mock, patch @@ -27,11 +27,13 @@ def load_json_file(file_path): class TestProductHandlers(BaseHandlerTestCase): def seed(self): mock_products = [] - for product in mock_data: + for index, product in enumerate(mock_data): + date_added = datetime.now() - timedelta(days=1) + timedelta(minutes=index) product_model = ProductModel( product_id=str(create_uuid_from_string(product["name"])), name=product["name"], price=product["price"], + date_added=date_added, ) mock_products.append(product_model) @@ -157,16 +159,34 @@ async def test_list_products_integration(self): assert response.status == 200 response_body = await response.text() assert len(json.loads(response_body)["products"]) == 5 + assert (json.loads(response_body)["products"][0]) == { + "product_id": str(create_uuid_from_string("Another Tamarillo")), + "name": "Another Tamarillo", + "price": 500.0, + "date_added": ANY, + } response2 = await self.client.request("GET", "/product/list?page=2&limit=5") assert response2.status == 200 response_body2 = await response2.text() assert len(json.loads(response_body2)["products"]) == 5 + assert (json.loads(response_body2)["products"][0]) == { + "product_id": str(create_uuid_from_string("Oregon Grape")), + "name": "Oregon Grape", + "price": 470.0, + "date_added": ANY, + } response3 = await self.client.request("GET", "/product/list?page=3&limit=5") assert response3.status == 200 response_body3 = await response3.text() assert len(json.loads(response_body3)["products"]) == 1 + assert (json.loads(response_body3)["products"][0]) == { + "product_id": str(create_uuid_from_string("Jostaberry")), + "name": "Jostaberry", + "price": 445.0, + "date_added": ANY, + } @pytest.mark.asyncio async def test_get_one_product_integration(self): From 22fbb282e0925e89fdc128bb4abc3d9beaf6288f Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Sun, 8 Dec 2024 10:06:14 +0100 Subject: [PATCH 6/6] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9474b46..5441e79 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,10 @@ To switch between the 2 methods, stop en remove all containers then build again - [x] setup database-session + commit in aiohttp-middleware (1 commit per request). in middleware try/catch with rollback in catch. app start open connection, app shutdown close connection - [x] setup dependency injection in app start so handler doesnt need to get repository from the request + and create new service every request (middleware adds product_service to the request.) - [x] add test-tooling and unit tests -- [ ] add pagination to /list +- [x] add pagination to /list - [ ] add custom exceptions (middleware catch Exception, return a fitting response for certain exceptions) - [ ] use sqlalchamy async queries https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html for better performance - [ ] setup migrations (use Flyway) - [ ] input validation -- [ ] Github Actions (linting, testing) +- [x] Github Actions (linting, testing) - ...