import datetime from decimal import Decimal as D from http import client as http_client from http.cookies import _unquote import django from django.contrib.messages.storage import cookie from django.core import signing from django.test import TestCase, override_settings from django.urls import reverse from django.utils.translation import gettext from oscar.apps.basket import reports from oscar.apps.basket.models import Basket from oscar.apps.partner import strategy from oscar.core.compat import get_user_model from oscar.test import factories from oscar.test.basket import add_product from oscar.test.factories import OptionFactory, create_product from oscar.test.testcases import WebTestCase User = get_user_model() class TestBasketMerging(TestCase): def setUp(self): self.product = create_product(num_in_stock=10) self.user_basket = Basket() self.user_basket.strategy = strategy.Default() add_product(self.user_basket, product=self.product) self.cookie_basket = Basket() self.cookie_basket.strategy = strategy.Default() add_product(self.cookie_basket, quantity=2, product=self.product) self.user_basket.merge(self.cookie_basket, add_quantities=False) def test_cookie_basket_has_status_set(self): self.assertEqual(Basket.MERGED, self.cookie_basket.status) def test_lines_are_moved_across(self): self.assertEqual(1, self.user_basket.lines.all().count()) def test_merge_line_takes_max_quantity(self): line = self.user_basket.lines.get(product=self.product) self.assertEqual(2, line.quantity) class AnonAddToBasketViewTests(WebTestCase): csrf_checks = False def setUp(self): self.product = create_product(price=D("10.00"), num_in_stock=10) url = reverse("basket:add", kwargs={"pk": self.product.pk}) post_params = {"product_id": self.product.id, "action": "add", "quantity": 1} self.response = self.app.post(url, params=post_params) def test_cookie_is_created(self): self.assertTrue("oscar_open_basket" in self.response.test_app.cookies) def test_price_is_recorded(self): oscar_open_basket_cookie = _unquote( self.response.test_app.cookies["oscar_open_basket"] ) basket_id = oscar_open_basket_cookie.split(":")[0] basket = Basket.objects.get(id=basket_id) line = basket.lines.get(product=self.product) stockrecord = self.product.stockrecords.all()[0] self.assertEqual(stockrecord.price, line.price_excl_tax) class BasketSummaryViewTests(WebTestCase): def setUp(self): url = reverse("basket:summary") self.response = self.app.get(url) def test_shipping_method_in_context(self): self.assertTrue("shipping_method" in self.response.context) def test_order_total_in_context(self): self.assertTrue("order_total" in self.response.context) def test_view_does_not_error(self): self.assertEqual(http_client.OK, self.response.status_code) def test_basket_in_context(self): self.assertTrue("basket" in self.response.context) def test_basket_is_empty(self): basket = self.response.context["basket"] self.assertEqual(0, basket.num_lines) class BasketThresholdTest(WebTestCase): csrf_checks = False @override_settings(OSCAR_MAX_BASKET_QUANTITY_THRESHOLD=3) def test_adding_more_than_threshold_raises(self): dummy_product = create_product(price=D("10.00"), num_in_stock=10) url = reverse("basket:add", kwargs={"pk": dummy_product.pk}) post_params = {"product_id": dummy_product.id, "action": "add", "quantity": 2} response = self.app.post(url, params=post_params) # pylint: disable=no-member self.assertIn("oscar_open_basket", response.test_app.cookies) post_params = {"product_id": dummy_product.id, "action": "add", "quantity": 2} response = self.app.post(url, params=post_params) expected = gettext( "Due to technical limitations we are not able to ship more " "than %(threshold)d items in one order. Your basket currently " "has %(basket)d items." ) % ({"threshold": 3, "basket": 2}) if django.VERSION < (3, 2): self.assertIn(expected, response.test_app.cookies["messages"]) else: signer = signing.get_cookie_signer(salt="django.contrib.messages") message_strings = [ m.message # pylint: disable=no-member for m in signer.unsign_object( response.test_app.cookies["messages"], serializer=cookie.MessageSerializer, ) ] self.assertIn(expected, message_strings) class BasketReportTests(TestCase): def test_open_report_doesnt_error(self): data = { "start_date": datetime.date(2012, 5, 1), "end_date": datetime.date(2012, 5, 17), "formatter": "CSV", } generator = reports.OpenBasketReportGenerator(**data) generator.generate() def test_submitted_report_doesnt_error(self): data = { "start_date": datetime.date(2012, 5, 1), "end_date": datetime.date(2012, 5, 17), "formatter": "CSV", } generator = reports.SubmittedBasketReportGenerator(**data) generator.generate() class SavedBasketTests(WebTestCase): csrf_checks = False def test_moving_to_saved_basket_creates_new(self): self.user = factories.UserFactory() product = factories.ProductFactory() basket = factories.BasketFactory(owner=self.user) basket.add_product(product) response = self.get(reverse("basket:summary")) formset = response.context["formset"] form = formset.forms[0] data = { formset.add_prefix("INITIAL_FORMS"): 1, formset.add_prefix("TOTAL_FORMS"): 1, formset.add_prefix("MIN_FORMS"): 0, formset.add_prefix("MAX_NUM_FORMS"): 1, form.add_prefix("id"): form.instance.pk, form.add_prefix("quantity"): form.initial["quantity"], form.add_prefix("save_for_later"): True, } response = self.post(reverse("basket:summary"), params=data) self.assertRedirects(response, reverse("basket:summary")) self.assertFalse(Basket.open.get(pk=basket.pk).lines.exists()) self.assertEqual( Basket.saved.get(owner=self.user).lines.get(product=product).quantity, 1 ) def test_moving_to_saved_basket_updates_existing(self): self.user = factories.UserFactory() product = factories.ProductFactory() basket = factories.BasketFactory(owner=self.user) basket.add_product(product) saved_basket = factories.BasketFactory(owner=self.user, status=Basket.SAVED) saved_basket.add_product(product) response = self.get(reverse("basket:summary")) formset = response.context["formset"] form = formset.forms[0] data = { formset.add_prefix("INITIAL_FORMS"): 1, formset.add_prefix("TOTAL_FORMS"): 1, formset.add_prefix("MIN_FORMS"): 0, formset.add_prefix("MAX_NUM_FORMS"): 1, form.add_prefix("id"): form.instance.pk, form.add_prefix("quantity"): form.initial["quantity"], form.add_prefix("save_for_later"): True, } response = self.post(reverse("basket:summary"), params=data) self.assertRedirects(response, reverse("basket:summary")) self.assertFalse(Basket.open.get(pk=basket.pk).lines.exists()) self.assertEqual( Basket.saved.get(pk=saved_basket.pk).lines.get(product=product).quantity, 2 ) def test_moving_from_saved_basket(self): self.user = User.objects.create_user( username="test", password="pass", email="test@example.com" ) product = create_product(price=D("10.00"), num_in_stock=2) basket = factories.create_basket(empty=True) basket.owner = self.user basket.save() add_product(basket, product=product) saved_basket, _ = Basket.saved.get_or_create(owner=self.user) saved_basket.strategy = basket.strategy add_product(saved_basket, product=product) response = self.get(reverse("basket:summary")) saved_formset = response.context["saved_formset"] saved_form = saved_formset.forms[0] data = { saved_formset.add_prefix("INITIAL_FORMS"): 1, saved_formset.add_prefix("MAX_NUM_FORMS"): 1, saved_formset.add_prefix("TOTAL_FORMS"): 1, saved_form.add_prefix("id"): saved_form.initial["id"], saved_form.add_prefix("move_to_basket"): True, } response = self.post(reverse("basket:saved"), params=data) self.assertEqual( Basket.open.get(id=basket.id).lines.get(product=product).quantity, 2 ) self.assertRedirects(response, reverse("basket:summary")) def test_moving_from_saved_basket_more_than_stocklevel_raises(self): self.user = User.objects.create_user( username="test", password="pass", email="test@example.com" ) product = create_product(price=D("10.00"), num_in_stock=1) basket, _ = Basket.open.get_or_create(owner=self.user) add_product(basket, product=product) saved_basket, _ = Basket.saved.get_or_create(owner=self.user) add_product(saved_basket, product=product) response = self.get(reverse("basket:summary")) saved_formset = response.context["saved_formset"] saved_form = saved_formset.forms[0] data = { saved_formset.add_prefix("INITIAL_FORMS"): 1, saved_formset.add_prefix("MAX_NUM_FORMS"): 1, saved_formset.add_prefix("TOTAL_FORMS"): 1, saved_form.add_prefix("id"): saved_form.initial["id"], saved_form.add_prefix("move_to_basket"): True, } response = self.post(reverse("basket:saved"), params=data) # we can't add more than stock level into basket self.assertEqual( Basket.open.get(id=basket.id).lines.get(product=product).quantity, 1 ) self.assertRedirects(response, reverse("basket:summary")) class BasketFormSetTests(WebTestCase): csrf_checks = False def test_formset_with_removed_line(self): products = [create_product() for i in range(3)] basket = factories.create_basket(empty=True) basket.owner = self.user basket.save() add_product(basket, product=products[0]) add_product(basket, product=products[1]) add_product(basket, product=products[2]) response = self.get(reverse("basket:summary")) formset = response.context["formset"] self.assertEqual(len(formset.forms), 3) basket.lines.filter(product=products[0]).delete() management_form = formset.management_form data = { formset.add_prefix("INITIAL_FORMS"): management_form.initial[ "INITIAL_FORMS" ], formset.add_prefix("MAX_NUM_FORMS"): management_form.initial[ "MAX_NUM_FORMS" ], formset.add_prefix("TOTAL_FORMS"): management_form.initial["TOTAL_FORMS"], "form-0-quantity": 1, "form-0-id": formset.forms[0].instance.id, "form-1-quantity": 2, "form-1-id": formset.forms[1].instance.id, "form-2-quantity": 2, "form-2-id": formset.forms[2].instance.id, } response = self.post(reverse("basket:summary"), params=data) self.assertEqual(response.status_code, 302) formset = response.follow().context["formset"] self.assertEqual(len(formset.forms), 2) self.assertEqual(len(formset.forms_with_instances), 2) self.assertEqual(basket.lines.all()[0].quantity, 2) self.assertEqual(basket.lines.all()[1].quantity, 2) def test_invalid_formset_with_removed_line(self): products = [create_product() for i in range(3)] basket = factories.create_basket(empty=True) basket.owner = self.user basket.save() add_product(basket, product=products[0]) add_product(basket, product=products[1]) add_product(basket, product=products[2]) response = self.get(reverse("basket:summary")) formset = response.context["formset"] self.assertEqual(len(formset.forms), 3) basket.lines.filter(product=products[0]).delete() stockrecord = products[1].stockrecords.first() stockrecord.num_in_stock = 0 stockrecord.save() management_form = formset.management_form data = { formset.add_prefix("INITIAL_FORMS"): management_form.initial[ "INITIAL_FORMS" ], formset.add_prefix("MIN_NUM_FORMS"): management_form.initial[ "MIN_NUM_FORMS" ], formset.add_prefix("MAX_NUM_FORMS"): management_form.initial[ "MAX_NUM_FORMS" ], formset.add_prefix("TOTAL_FORMS"): management_form.initial["TOTAL_FORMS"], "form-0-quantity": 1, "form-0-id": formset.forms[0].instance.id, "form-1-quantity": 2, "form-1-id": formset.forms[1].instance.id, "form-2-quantity": 2, "form-2-id": formset.forms[2].instance.id, } response = self.post(reverse("basket:summary"), params=data) self.assertEqual(response.status_code, 200) formset = response.context["formset"] self.assertEqual(len(formset.forms), 2) self.assertEqual(len(formset.forms_with_instances), 2) self.assertEqual(basket.lines.all()[0].quantity, 1) self.assertEqual(basket.lines.all()[1].quantity, 1) def test_deleting_valid_line_with_other_valid_line(self): product_1 = create_product() product_2 = create_product() basket = factories.create_basket(empty=True) basket.owner = self.user basket.save() add_product(basket, product=product_1) add_product(basket, product=product_2) response = self.get(reverse("basket:summary")) formset = response.context["formset"] self.assertEqual(len(formset.forms), 2) data = { formset.add_prefix("TOTAL_FORMS"): formset.management_form.initial[ "TOTAL_FORMS" ], formset.add_prefix("INITIAL_FORMS"): formset.management_form.initial[ "INITIAL_FORMS" ], formset.add_prefix("MIN_NUM_FORMS"): formset.management_form.initial[ "MIN_NUM_FORMS" ], formset.add_prefix("MAX_NUM_FORMS"): formset.management_form.initial[ "MAX_NUM_FORMS" ], formset.forms[0].add_prefix("id"): formset.forms[0].instance.pk, formset.forms[0].add_prefix("quantity"): formset.forms[0].instance.quantity, formset.forms[0].add_prefix("DELETE"): "on", formset.forms[1].add_prefix("id"): formset.forms[1].instance.pk, formset.forms[1].add_prefix("quantity"): formset.forms[1].instance.quantity, } response = self.post(reverse("basket:summary"), params=data, xhr=True) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context["formset"].forms), 1) self.assertFalse( response.context["formset"].is_bound ) # new formset is rendered self.assertEqual(basket.lines.count(), 1) self.assertEqual(basket.lines.all()[0].quantity, 1) def test_deleting_valid_line_with_other_invalid_line(self): product_1 = create_product() product_2 = create_product() basket = factories.create_basket(empty=True) basket.owner = self.user basket.save() add_product(basket, product=product_1) add_product(basket, product=product_2) response = self.get(reverse("basket:summary")) formset = response.context["formset"] self.assertEqual(len(formset.forms), 2) # Render product for other line out of stock product_2.stockrecords.update(num_in_stock=0) data = { formset.add_prefix("TOTAL_FORMS"): formset.management_form.initial[ "TOTAL_FORMS" ], formset.add_prefix("INITIAL_FORMS"): formset.management_form.initial[ "INITIAL_FORMS" ], formset.add_prefix("MIN_NUM_FORMS"): formset.management_form.initial[ "MIN_NUM_FORMS" ], formset.add_prefix("MAX_NUM_FORMS"): formset.management_form.initial[ "MAX_NUM_FORMS" ], formset.forms[0].add_prefix("id"): formset.forms[0].instance.pk, formset.forms[0].add_prefix("quantity"): formset.forms[0].instance.quantity, formset.forms[0].add_prefix("DELETE"): "on", formset.forms[1].add_prefix("id"): formset.forms[1].instance.pk, formset.forms[1].add_prefix("quantity"): formset.forms[1].instance.quantity, } response = self.post(reverse("basket:summary"), params=data, xhr=True) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context["formset"].forms), 1) self.assertTrue( response.context["formset"].is_bound ) # formset with errors is rendered self.assertFalse(response.context["formset"].forms[0].is_valid()) self.assertEqual(basket.lines.count(), 1) self.assertEqual(basket.lines.all()[0].quantity, 1) def test_deleting_invalid_line_with_other_valid_line(self): product_1 = create_product() product_2 = create_product() basket = factories.create_basket(empty=True) basket.owner = self.user basket.save() add_product(basket, product=product_1) add_product(basket, product=product_2) response = self.get(reverse("basket:summary")) formset = response.context["formset"] self.assertEqual(len(formset.forms), 2) # Render product for to-be-deleted line out of stock product_1.stockrecords.update(num_in_stock=0) data = { formset.add_prefix("TOTAL_FORMS"): formset.management_form.initial[ "TOTAL_FORMS" ], formset.add_prefix("INITIAL_FORMS"): formset.management_form.initial[ "INITIAL_FORMS" ], formset.add_prefix("MIN_NUM_FORMS"): formset.management_form.initial[ "MIN_NUM_FORMS" ], formset.add_prefix("MAX_NUM_FORMS"): formset.management_form.initial[ "MAX_NUM_FORMS" ], formset.forms[0].add_prefix("id"): formset.forms[0].instance.pk, formset.forms[0].add_prefix("quantity"): formset.forms[0].instance.quantity, formset.forms[0].add_prefix("DELETE"): "on", formset.forms[1].add_prefix("id"): formset.forms[1].instance.pk, formset.forms[1].add_prefix("quantity"): formset.forms[1].instance.quantity, } response = self.post(reverse("basket:summary"), params=data, xhr=True) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context["formset"].forms), 1) self.assertFalse( response.context["formset"].is_bound ) # new formset is rendered self.assertEqual(basket.lines.count(), 1) self.assertEqual(basket.lines.all()[0].quantity, 1) def test_deleting_invalid_line_with_other_invalid_line(self): product_1 = create_product() product_2 = create_product() basket = factories.create_basket(empty=True) basket.owner = self.user basket.save() add_product(basket, product=product_1) add_product(basket, product=product_2) response = self.get(reverse("basket:summary")) formset = response.context["formset"] self.assertEqual(len(formset.forms), 2) # Render products for both lines out of stock product_1.stockrecords.update(num_in_stock=0) product_2.stockrecords.update(num_in_stock=0) data = { formset.add_prefix("TOTAL_FORMS"): formset.management_form.initial[ "TOTAL_FORMS" ], formset.add_prefix("INITIAL_FORMS"): formset.management_form.initial[ "INITIAL_FORMS" ], formset.add_prefix("MIN_NUM_FORMS"): formset.management_form.initial[ "MIN_NUM_FORMS" ], formset.add_prefix("MAX_NUM_FORMS"): formset.management_form.initial[ "MAX_NUM_FORMS" ], formset.forms[0].add_prefix("id"): formset.forms[0].instance.pk, formset.forms[0].add_prefix("quantity"): formset.forms[0].instance.quantity, formset.forms[0].add_prefix("DELETE"): "on", formset.forms[1].add_prefix("id"): formset.forms[1].instance.pk, formset.forms[1].add_prefix("quantity"): formset.forms[1].instance.quantity, } response = self.post(reverse("basket:summary"), params=data, xhr=True) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context["formset"].forms), 1) self.assertTrue( response.context["formset"].is_bound ) # formset with errors is rendered self.assertFalse(response.context["formset"].forms[0].is_valid()) self.assertEqual(basket.lines.count(), 1) self.assertEqual(basket.lines.all()[0].quantity, 1) def test_formset_quantity_update_with_options(self): product = create_product(num_in_stock=2) option = OptionFactory() # Add the option to the product class product.get_product_class().options.add(option) basket = factories.create_basket(empty=True) basket.owner = self.user basket.save() basket.add_product(product, options=[{"option": option, "value": "Test 1"}]) basket.add_product(product, options=[{"option": option, "value": "Test 2"}]) response = self.get(reverse("basket:summary")) formset = response.context["formset"] self.assertEqual(len(formset.forms), 2) # Now update one of the quantities to 2 data = { formset.add_prefix("TOTAL_FORMS"): formset.management_form.initial[ "TOTAL_FORMS" ], formset.add_prefix("INITIAL_FORMS"): formset.management_form.initial[ "INITIAL_FORMS" ], formset.add_prefix("MIN_NUM_FORMS"): formset.management_form.initial[ "MIN_NUM_FORMS" ], formset.add_prefix("MAX_NUM_FORMS"): formset.management_form.initial[ "MAX_NUM_FORMS" ], formset.forms[0].add_prefix("id"): formset.forms[0].instance.pk, formset.forms[0].add_prefix("quantity"): 2, formset.forms[1].add_prefix("id"): formset.forms[1].instance.pk, formset.forms[1].add_prefix("quantity"): formset.forms[1].instance.quantity, } response = self.post(reverse("basket:summary"), params=data, xhr=True) self.assertEqual(response.status_code, 200) self.assertFalse(response.context["formset"].forms[0].is_valid()) self.assertIn( "Available stock is only %s, which has been exceeded because multiple lines contain the same product." % 2, str(response.context["formset"].forms[0].errors), )