From cde10ec41734d285fd2e83333b9d07bf4c64b995 Mon Sep 17 00:00:00 2001 From: Josh Bristow <45632384+josh-bristow@users.noreply.github.com> Date: Thu, 15 Jun 2023 10:56:51 +0200 Subject: [PATCH 1/6] Update tests.py Unit Tests for Cart Functionality This pull request adds comprehensive unit tests for the Cart class in the cart.py module. The tests cover various scenarios to ensure the correctness and robustness of the Cart implementation. The test cases included in this pull request are as follows: Cart Initialization Test: This test verifies that the cart is properly initialized and empty when a request is made. Add Product Test: This test checks the functionality of adding products to the cart, including verifying the correct quantity and subtotal price calculations. Save Cart Test: This test ensures that the cart is marked as modified after it is saved. Remove Product Test: This test validates the ability to remove products from the cart and confirms that the cart is empty after removal. Get Cart Products Test: This test checks if the get_cart_products method returns the expected list of products in the cart. Get Subtotal Price Test: This test verifies the accuracy of calculating the subtotal price of all products in the cart. Get Shipping Cost Test: This test ensures that the get_shipping_cost method returns the correct shipping cost based on the contents of the cart. (Please note that the expected shipping cost value needs to be provided.) Cart Iteration Test: This test validates that iterating over the cart items returns the correct product details, including quantity, price, and total price. These tests are designed to cover different aspects of the Cart functionality, including initialization, adding/removing products, calculating prices, and iterating over cart items. They provide comprehensive coverage to catch any potential bugs or issues in the Cart implementation. Please review these unit tests and let me know if any further changes or additions are required. --- cart/tests.py | 126 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/cart/tests.py b/cart/tests.py index a39b155ac..c5ef29faf 100644 --- a/cart/tests.py +++ b/cart/tests.py @@ -1 +1,125 @@ -# Create your tests here. +import unittest +from decimal import Decimal +from unittest.mock import Mock, patch +from django.test import RequestFactory +from .cart import Cart +from store.models import Product + +class CartTestCase(unittest.TestCase): + def setUp(self): + self.factory = RequestFactory() + + def test_cart_initialization(self): + request = self.factory.get('/') + cart = Cart(request) + + self.assertEqual(len(cart), 0) + self.assertEqual(cart.get_subtotal_price(), Decimal('0')) + + def test_add_product(self): + request = self.factory.get('/') + cart = Cart(request) + + product = Mock(spec=Product) + product.price = Decimal('9.99') + + cart.add(product) + self.assertEqual(len(cart), 1) + self.assertEqual(cart.get_subtotal_price(), Decimal('9.99')) + + cart.add(product, quantity=2) + self.assertEqual(len(cart), 1) + self.assertEqual(cart.get_subtotal_price(), Decimal('29.97')) + + def test_save_cart(self): + request = self.factory.get('/') + cart = Cart(request) + cart.save() + + self.assertTrue(cart.session.modified) + + def test_remove_product(self): + request = self.factory.get('/') + cart = Cart(request) + + product = Mock(spec=Product) + cart.add(product) + + self.assertEqual(len(cart), 1) + cart.remove(product) + self.assertEqual(len(cart), 0) + + def test_get_cart_products(self): + request = self.factory.get('/') + cart = Cart(request) + + product1 = Mock(spec=Product) + product2 = Mock(spec=Product) + + cart.add(product1) + cart.add(product2) + + cart_products = cart.get_cart_products() + + self.assertEqual(len(cart_products), 2) + self.assertIn(product1, cart_products) + self.assertIn(product2, cart_products) + + def test_get_subtotal_price(self): + request = self.factory.get('/') + cart = Cart(request) + + product1 = Mock(spec=Product) + product1.price = Decimal('9.99') + + product2 = Mock(spec=Product) + product2.price = Decimal('19.99') + + cart.add(product1) + cart.add(product2, quantity=2) + + subtotal_price = cart.get_subtotal_price() + + self.assertEqual(subtotal_price, Decimal('49.97')) + + def test_get_shipping_cost(self): + request = self.factory.get('/') + cart = Cart(request) + + # Mocking the get_shipping_cost method + expected_shipping_cost = Decimal('10.00') + with patch.object(cart, 'get_shipping_cost', return_value=expected_shipping_cost): + shipping_cost = cart.get_shipping_cost() + + self.assertEqual(shipping_cost, expected_shipping_cost) + + def test_cart_iteration(self): + request = self.factory.get('/') + cart = Cart(request) + + product1 = Mock(spec=Product) + product1.title = 'Product 1' + product1.price = Decimal('9.99') + + product2 = Mock(spec=Product) + product2.title = 'Product 2' + product2.price = Decimal('19.99') + + cart.add(product1) + cart.add(product2, quantity=2) + + cart_items = list(cart) + + self.assertEqual(len(cart_items), 2) + self.assertEqual(cart_items[0]['product'], product1) + self.assertEqual(cart_items[0]['quantity'], 1) + self.assertEqual(cart_items[0]['price'], Decimal('9.99')) + self.assertEqual(cart_items[0]['total_price'], Decimal('9.99')) + + self.assertEqual(cart_items[1]['product'], product2) + self.assertEqual(cart_items[1]['quantity'], 2) + self.assertEqual(cart_items[1]['price'], Decimal('19.99')) + self.assertEqual(cart_items[1]['total_price'], Decimal('39.98')) + +if __name__ == '__main__': + unittest.main() From 80da7548fd9dd2f5143e340992e786c347d697b0 Mon Sep 17 00:00:00 2001 From: Josh Bristow <45632384+josh-bristow@users.noreply.github.com> Date: Thu, 15 Jun 2023 11:40:35 +0200 Subject: [PATCH 2/6] Update cart/tests.py Co-authored-by: Brylie Christopher Oxley --- cart/tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cart/tests.py b/cart/tests.py index c5ef29faf..6e8e18137 100644 --- a/cart/tests.py +++ b/cart/tests.py @@ -8,9 +8,11 @@ class CartTestCase(unittest.TestCase): def setUp(self): self.factory = RequestFactory() + self.request = self.factory.get('/') + self.request.session = {} def test_cart_initialization(self): - request = self.factory.get('/') + cart = Cart(request) self.assertEqual(len(cart), 0) From 4e5a4cab75a3c1393466e58e91e7517eb49bf74f Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Fri, 16 Jun 2023 10:36:06 +0300 Subject: [PATCH 3/6] Make Product.image optional --- store/migrations/0002_alter_product_image.py | 25 ++++++++++++++++++++ store/models.py | 6 ++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 store/migrations/0002_alter_product_image.py diff --git a/store/migrations/0002_alter_product_image.py b/store/migrations/0002_alter_product_image.py new file mode 100644 index 000000000..582b4ca09 --- /dev/null +++ b/store/migrations/0002_alter_product_image.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.1 on 2023-06-16 07:34 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("wagtailimages", "0025_alter_image_file_alter_rendition_file"), + ("store", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="product", + name="image", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + ), + ), + ] diff --git a/store/models.py b/store/models.py index 55a5abef1..027a7bdb5 100644 --- a/store/models.py +++ b/store/models.py @@ -42,7 +42,11 @@ class ProductIndexPage(Page): class Product(Page): image = models.ForeignKey( - "wagtailimages.Image", on_delete=models.SET_NULL, null=True, related_name="+" + "wagtailimages.Image", + on_delete=models.SET_NULL, + null=True, + blank=True, # note, making this required will break the tests + related_name="+", ) description = RichTextField(blank=True) price = models.DecimalField(max_digits=10, decimal_places=2) From e3f126441623e4f622f74c4d7a5056aa3900b6a6 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Fri, 16 Jun 2023 10:36:38 +0300 Subject: [PATCH 4/6] Add type annotations --- cart/cart.py | 55 +++++++++++++++++++++++++++++------------- shipping/calculator.py | 2 +- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/cart/cart.py b/cart/cart.py index acf72bca1..0a426c5a4 100644 --- a/cart/cart.py +++ b/cart/cart.py @@ -1,13 +1,15 @@ from decimal import Decimal +from collections.abc import Generator from django.conf import settings +from django.http import HttpRequest from shipping.calculator import get_book_shipping_cost from store.models import Product class Cart: - def __init__(self, request): + def __init__(self, request: HttpRequest) -> None: """Initialize the cart.""" self.session = request.session @@ -19,9 +21,14 @@ def __init__(self, request): self.cart = cart - def add(self, product, quantity=1, update_quantity=False): + def add( + self, + product: Product, + quantity: int = 1, + update_quantity: bool = False, + ) -> None: """Add a product to the cart or update its quantity.""" - product_id = str(product.id) + product_id = str(product.id) # type: ignore if product_id not in self.cart: self.cart[product_id] = { @@ -37,47 +44,55 @@ def add(self, product, quantity=1, update_quantity=False): self.save() - def save(self): + def save(self) -> None: # mark the session as "modified" # to make sure it gets saved self.session.modified = True - def remove(self, product): + def remove(self, product: Product) -> None: """Remove a product from the cart.""" - product_id = str(product.id) + product_id = str(product.id) # type: ignore if product_id in self.cart: del self.cart[product_id] self.save() - def get_cart_products(self): + def get_cart_products(self) -> list[Product]: product_ids = self.cart.keys() # get the product objects and add them to the cart return Product.objects.filter(id__in=product_ids) - def get_total_price(self): - return sum([self.get_subtotal_price(), self.get_shipping_cost()]) + def get_total_price(self) -> Decimal: + int_sum = sum( + [ + self.get_subtotal_price(), + self.get_shipping_cost(), + ] + ) + return Decimal(int_sum).quantize(Decimal("0.01")) - def get_subtotal_price(self): - return sum( + def get_subtotal_price(self) -> Decimal: + totals = [ Decimal(item["price"]) * item["quantity"] for item in self.cart.values() - ) + ] + product_sum = sum(totals) + return Decimal(product_sum).quantize(Decimal("0.01")) - def get_shipping_cost(self): + def get_shipping_cost(self) -> Decimal: book_quantity = sum(item["quantity"] for item in self.cart.values()) return get_book_shipping_cost(book_quantity) - def clear(self): + def clear(self) -> None: # remove cart from session del self.session[settings.CART_SESSION_ID] self.save() - def __iter__(self): + def __iter__(self) -> Generator: """Get cart products from the database.""" # get the product objects and add them to the cart products = self.get_cart_products() @@ -85,7 +100,10 @@ def __iter__(self): cart = self.cart.copy() for product in products: - cart[str(product.id)]["product"] = product + if str(product.id) not in cart: # type: ignore + continue + + cart[str(product.id)]["product"] = product # type: ignore for item in cart.values(): item["price"] = Decimal(item["price"]) @@ -93,8 +111,11 @@ def __iter__(self): yield item - def __len__(self): + def __len__(self) -> int: """Count all items in the cart.""" + + # TODO: determine whether this should count the number of products + # or the total quantity of products item_quantities = [item["quantity"] for item in self.cart.values()] return sum(item_quantities) diff --git a/shipping/calculator.py b/shipping/calculator.py index 0bc031f4e..739a91863 100644 --- a/shipping/calculator.py +++ b/shipping/calculator.py @@ -1,7 +1,7 @@ from decimal import Decimal -def get_book_shipping_cost(book_quantity=1): +def get_book_shipping_cost(book_quantity: int = 1) -> Decimal: """Calculate shipping costs for books in a cart/order. The shipping rules are flat rate for each book, with discounts From 5a40bb738e199095bfd46e3762d62a8a9ed867b7 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Fri, 16 Jun 2023 10:36:55 +0300 Subject: [PATCH 5/6] Fix the tests --- cart/tests.py | 215 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 129 insertions(+), 86 deletions(-) diff --git a/cart/tests.py b/cart/tests.py index 6e8e18137..ac2d1851e 100644 --- a/cart/tests.py +++ b/cart/tests.py @@ -1,127 +1,170 @@ -import unittest from decimal import Decimal from unittest.mock import Mock, patch -from django.test import RequestFactory -from .cart import Cart -from store.models import Product - -class CartTestCase(unittest.TestCase): - def setUp(self): - self.factory = RequestFactory() - self.request = self.factory.get('/') - self.request.session = {} +from django.test import RequestFactory, TestCase +from django.contrib.sessions.middleware import SessionMiddleware +from wagtail.models import Page - def test_cart_initialization(self): - - cart = Cart(request) +from .cart import Cart +from home.models import HomePage +from store.models import Product, ProductIndexPage, StoreIndexPage + + +class CartTestCase(TestCase): + def setUp(self) -> None: + self.request = RequestFactory().get("/") + + # Add session middleware to the request + middleware = SessionMiddleware(Mock()) + middleware.process_request(self.request) + self.request.session.save() + + # get Site Root + root_page = Page.objects.get(id=1) + # try: + # root_page = Page.objects.get(id=1) + # except Page.DoesNotExist: + # root_page = Page(id=1).save() + + # Create HomePage + home_page = HomePage( + title="Welcome", + ) + + root_page.add_child(instance=home_page) + # root_page.save() + + # Create StoreIndexPage + store_index_page = StoreIndexPage( + title="Bookstore", + show_in_menus=True, + ) + home_page.add_child(instance=store_index_page) + + # Create ProductIndexPage + product_index_page = ProductIndexPage( + title="Products", + ) + store_index_page.add_child(instance=product_index_page) + + self.product1 = Product( + title="Product 1", + price=Decimal("9.99"), + ) + self.product2 = Product( + title="Product 2", + price=Decimal("19.99"), + ) + product_index_page.add_child(instance=self.product1) + product_index_page.add_child(instance=self.product2) + + # self.product1 = Product.objects.create( + # id=1, title="Product 1", price=Decimal("9.99") + # ) + # self.product2 = Product.objects.create( + # id=2, title="Product 2", price=Decimal("19.99") + # ) + + # self.product1 = Mock(spec=Product, id=1) + # self.product1.title = "Product 1" + # self.product1.price = Decimal("9.99") + + # self.product2: Product = Mock(spec=Product, id=2) + # self.product2.title = "Product 2" + # self.product2.price = Decimal("19.99") + + def test_cart_initialization(self) -> None: + cart = Cart(self.request) self.assertEqual(len(cart), 0) - self.assertEqual(cart.get_subtotal_price(), Decimal('0')) - - def test_add_product(self): - request = self.factory.get('/') - cart = Cart(request) + self.assertEqual(cart.get_subtotal_price(), Decimal("0")) - product = Mock(spec=Product) - product.price = Decimal('9.99') + def test_add_product(self) -> None: + cart = Cart(self.request) - cart.add(product) + cart.add(self.product1) self.assertEqual(len(cart), 1) - self.assertEqual(cart.get_subtotal_price(), Decimal('9.99')) + self.assertEqual(cart.get_subtotal_price(), Decimal("9.99")) - cart.add(product, quantity=2) - self.assertEqual(len(cart), 1) - self.assertEqual(cart.get_subtotal_price(), Decimal('29.97')) + cart.add(self.product1, quantity=2) + self.assertEqual(len(cart), 3) + self.assertEqual(cart.get_subtotal_price(), Decimal("29.97")) - def test_save_cart(self): - request = self.factory.get('/') - cart = Cart(request) + def test_save_cart(self) -> None: + cart = Cart(self.request) cart.save() self.assertTrue(cart.session.modified) - def test_remove_product(self): - request = self.factory.get('/') - cart = Cart(request) + def test_remove_product(self) -> None: + cart = Cart(self.request) - product = Mock(spec=Product) - cart.add(product) + cart.add(self.product1) self.assertEqual(len(cart), 1) - cart.remove(product) + cart.remove(self.product1) self.assertEqual(len(cart), 0) - def test_get_cart_products(self): - request = self.factory.get('/') - cart = Cart(request) - - product1 = Mock(spec=Product) - product2 = Mock(spec=Product) + def test_get_cart_products(self) -> None: + cart = Cart(self.request) - cart.add(product1) - cart.add(product2) + cart.add(self.product1) + cart.add(self.product2) cart_products = cart.get_cart_products() self.assertEqual(len(cart_products), 2) - self.assertIn(product1, cart_products) - self.assertIn(product2, cart_products) - - def test_get_subtotal_price(self): - request = self.factory.get('/') - cart = Cart(request) + self.assertIn(self.product1, cart_products) + self.assertIn(self.product2, cart_products) - product1 = Mock(spec=Product) - product1.price = Decimal('9.99') + def test_get_subtotal_price(self) -> None: + cart = Cart(self.request) - product2 = Mock(spec=Product) - product2.price = Decimal('19.99') - - cart.add(product1) - cart.add(product2, quantity=2) + cart.add(self.product1) + cart.add(self.product2, quantity=2) subtotal_price = cart.get_subtotal_price() - self.assertEqual(subtotal_price, Decimal('49.97')) + self.assertEqual(subtotal_price, Decimal("49.97")) - def test_get_shipping_cost(self): - request = self.factory.get('/') - cart = Cart(request) + def test_get_shipping_cost(self) -> None: + cart = Cart(self.request) # Mocking the get_shipping_cost method - expected_shipping_cost = Decimal('10.00') - with patch.object(cart, 'get_shipping_cost', return_value=expected_shipping_cost): + expected_shipping_cost = Decimal("10.00") + with patch.object( + cart, + "get_shipping_cost", + return_value=expected_shipping_cost, + ): shipping_cost = cart.get_shipping_cost() self.assertEqual(shipping_cost, expected_shipping_cost) - def test_cart_iteration(self): - request = self.factory.get('/') - cart = Cart(request) - - product1 = Mock(spec=Product) - product1.title = 'Product 1' - product1.price = Decimal('9.99') + def test_cart_iteration(self) -> None: + cart = Cart(self.request) - product2 = Mock(spec=Product) - product2.title = 'Product 2' - product2.price = Decimal('19.99') - - cart.add(product1) - cart.add(product2, quantity=2) + cart.add(self.product1) + cart.add(self.product2, quantity=2) cart_items = list(cart) self.assertEqual(len(cart_items), 2) - self.assertEqual(cart_items[0]['product'], product1) - self.assertEqual(cart_items[0]['quantity'], 1) - self.assertEqual(cart_items[0]['price'], Decimal('9.99')) - self.assertEqual(cart_items[0]['total_price'], Decimal('9.99')) - - self.assertEqual(cart_items[1]['product'], product2) - self.assertEqual(cart_items[1]['quantity'], 2) - self.assertEqual(cart_items[1]['price'], Decimal('19.99')) - self.assertEqual(cart_items[1]['total_price'], Decimal('39.98')) - -if __name__ == '__main__': - unittest.main() + self.assertEqual(cart_items[0]["product"], self.product1) + self.assertEqual(cart_items[0]["quantity"], 1) + self.assertEqual(cart_items[0]["price"], Decimal("9.99")) + self.assertEqual(cart_items[0]["total_price"], Decimal("9.99")) + + self.assertEqual(cart_items[1]["product"], self.product2) + self.assertEqual(cart_items[1]["quantity"], 2) + self.assertEqual(cart_items[1]["price"], Decimal("19.99")) + self.assertEqual(cart_items[1]["total_price"], Decimal("39.98")) + + def tearDown(self) -> None: + # delete all pages + Page.objects.all().delete() + + # delete product1 and product2 + # self.product1.delete() + # self.product2.delete() + + return super().tearDown() From c164f53800c48beff2937021f28d9021d8975d17 Mon Sep 17 00:00:00 2001 From: Brylie Christopher Oxley Date: Fri, 16 Jun 2023 10:39:03 +0300 Subject: [PATCH 6/6] Cleanup --- cart/tests.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/cart/tests.py b/cart/tests.py index ac2d1851e..12cd6163f 100644 --- a/cart/tests.py +++ b/cart/tests.py @@ -20,10 +20,6 @@ def setUp(self) -> None: # get Site Root root_page = Page.objects.get(id=1) - # try: - # root_page = Page.objects.get(id=1) - # except Page.DoesNotExist: - # root_page = Page(id=1).save() # Create HomePage home_page = HomePage( @@ -57,21 +53,6 @@ def setUp(self) -> None: product_index_page.add_child(instance=self.product1) product_index_page.add_child(instance=self.product2) - # self.product1 = Product.objects.create( - # id=1, title="Product 1", price=Decimal("9.99") - # ) - # self.product2 = Product.objects.create( - # id=2, title="Product 2", price=Decimal("19.99") - # ) - - # self.product1 = Mock(spec=Product, id=1) - # self.product1.title = "Product 1" - # self.product1.price = Decimal("9.99") - - # self.product2: Product = Mock(spec=Product, id=2) - # self.product2.title = "Product 2" - # self.product2.price = Decimal("19.99") - def test_cart_initialization(self) -> None: cart = Cart(self.request) @@ -163,8 +144,4 @@ def tearDown(self) -> None: # delete all pages Page.objects.all().delete() - # delete product1 and product2 - # self.product1.delete() - # self.product2.delete() - return super().tearDown()