You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_condition.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. from decimal import Decimal as D
  2. from unittest import mock
  3. import pytest
  4. from django.test import TestCase
  5. from oscar.apps.basket.models import Basket
  6. from oscar.apps.offer import custom, models
  7. from oscar.test import factories
  8. from oscar.test.basket import add_product
  9. from tests._site.model_tests_app.models import BasketOwnerCalledBarry
  10. @pytest.fixture
  11. def products_some():
  12. return [factories.create_product(), factories.create_product()]
  13. @pytest.fixture()
  14. def range():
  15. return factories.RangeFactory()
  16. @pytest.fixture
  17. def range_all():
  18. return factories.RangeFactory(
  19. name="All products range", includes_all_products=True
  20. )
  21. @pytest.fixture
  22. def range_some(products_some):
  23. return factories.RangeFactory(
  24. name="Some products", products=products_some
  25. )
  26. @pytest.fixture
  27. def count_condition(range_all):
  28. return models.CountCondition(range=range_all, type="Count", value=2)
  29. @pytest.fixture
  30. def value_condition(range_all):
  31. return models.ValueCondition(range=range_all, type="Value", value=D("10.00"))
  32. @pytest.fixture
  33. def coverage_condition(range_some):
  34. return models.CoverageCondition(range=range_some, type="Coverage", value=2)
  35. @pytest.fixture
  36. def empty_basket():
  37. return factories.create_basket(empty=True)
  38. @pytest.fixture
  39. def partial_basket(empty_basket):
  40. basket = empty_basket
  41. add_product(basket)
  42. return basket
  43. @pytest.fixture
  44. def mock_offer():
  45. return mock.Mock()
  46. @pytest.mark.django_db
  47. class TestCountCondition:
  48. @pytest.fixture(autouse=True)
  49. def setUp(self, mock_offer):
  50. self.offer = mock_offer
  51. def test_description(self, count_condition):
  52. assert count_condition.description
  53. def test_is_not_satisfied_by_empty_basket(self, count_condition, empty_basket):
  54. assert count_condition.is_satisfied(self.offer, empty_basket) is False
  55. def test_not_discountable_product_fails_condition(
  56. self, count_condition, empty_basket
  57. ):
  58. basket = empty_basket
  59. prod1, prod2 = factories.create_product(), factories.create_product()
  60. prod2.is_discountable = False
  61. prod2.save()
  62. add_product(basket, product=prod1)
  63. add_product(basket, product=prod2)
  64. assert count_condition.is_satisfied(self.offer, basket) is False
  65. def test_empty_basket_fails_partial_condition(self, count_condition, empty_basket):
  66. assert count_condition.is_partially_satisfied(self.offer, empty_basket) is False
  67. def test_smaller_quantity_basket_passes_partial_condition(
  68. self, count_condition, empty_basket
  69. ):
  70. basket = empty_basket
  71. add_product(basket)
  72. assert count_condition.is_partially_satisfied(self.offer, basket)
  73. assert count_condition._num_matches == 1
  74. def test_smaller_quantity_basket_upsell_message(
  75. self, count_condition, empty_basket
  76. ):
  77. basket = empty_basket
  78. add_product(basket)
  79. assert "Buy 1 more product from " in count_condition.get_upsell_message(
  80. self.offer, basket
  81. )
  82. def test_matching_quantity_basket_fails_partial_condition(
  83. self, count_condition, empty_basket
  84. ):
  85. basket = empty_basket
  86. add_product(basket, quantity=2)
  87. assert count_condition.is_partially_satisfied(self.offer, basket) is False
  88. def test_matching_quantity_basket_passes_condition(
  89. self, count_condition, empty_basket
  90. ):
  91. basket = empty_basket
  92. add_product(basket, quantity=2)
  93. assert count_condition.is_satisfied(self.offer, basket)
  94. def test_greater_quantity_basket_passes_condition(
  95. self, count_condition, empty_basket
  96. ):
  97. basket = empty_basket
  98. add_product(basket, quantity=3)
  99. assert count_condition.is_satisfied(self.offer, basket)
  100. def test_consumption(self, count_condition, empty_basket):
  101. basket = empty_basket
  102. add_product(basket, quantity=3)
  103. count_condition.consume_items(self.offer, basket, [])
  104. assert 1 == basket.all_lines()[0].quantity_without_discount
  105. def test_is_satisfied_accounts_for_consumed_items(
  106. self, count_condition, empty_basket
  107. ):
  108. basket = empty_basket
  109. add_product(basket, quantity=3)
  110. count_condition.consume_items(self.offer, basket, [])
  111. assert count_condition.is_satisfied(self.offer, basket) is False
  112. @pytest.mark.django_db
  113. class TestValueCondition:
  114. @pytest.fixture(autouse=True)
  115. def setUp(self, empty_basket, value_condition, mock_offer):
  116. self.basket = empty_basket
  117. self.condition = value_condition
  118. self.offer = mock_offer
  119. self.item = factories.create_product(price=D("5.00"))
  120. self.expensive_item = factories.create_product(price=D("15.00"))
  121. def test_description(self, value_condition):
  122. assert value_condition.description
  123. def test_empty_basket_fails_condition(self):
  124. assert self.condition.is_satisfied(self.offer, self.basket) is False
  125. def test_empty_basket_fails_partial_condition(self):
  126. assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
  127. def test_less_value_basket_fails_condition(self):
  128. add_product(self.basket, D("5"))
  129. assert self.condition.is_satisfied(self.offer, self.basket) is False
  130. def test_not_discountable_item_fails_condition(self):
  131. product = factories.create_product(is_discountable=False)
  132. add_product(self.basket, D("15"), product=product)
  133. assert self.condition.is_satisfied(self.offer, self.basket) is False
  134. def test_upsell_message(self):
  135. add_product(self.basket, D("5"))
  136. assert "Spend" in self.condition.get_upsell_message(self.offer, self.basket)
  137. def test_matching_basket_fails_partial_condition(self):
  138. add_product(self.basket, D("5"), 2)
  139. assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
  140. def test_less_value_basket_passes_partial_condition(self):
  141. add_product(self.basket, D("5"), 1)
  142. assert self.condition.is_partially_satisfied(self.offer, self.basket)
  143. def test_matching_basket_passes_condition(self):
  144. add_product(self.basket, D("5"), 2)
  145. assert self.condition.is_satisfied(self.offer, self.basket)
  146. def test_greater_than_basket_passes_condition(self):
  147. add_product(self.basket, D("5"), 3)
  148. assert self.condition.is_satisfied(self.offer, self.basket)
  149. def test_consumption(self):
  150. add_product(self.basket, D("5"), 3)
  151. self.condition.consume_items(self.offer, self.basket, [])
  152. assert 1 == self.basket.all_lines()[0].quantity_without_discount
  153. def test_consumption_with_high_value_product(self):
  154. add_product(self.basket, D("15"), 1)
  155. self.condition.consume_items(self.offer, self.basket, [])
  156. assert 0 == self.basket.all_lines()[0].quantity_without_discount
  157. def test_is_consumed_respects_quantity_consumed(self):
  158. add_product(self.basket, D("15"), 1)
  159. assert self.condition.is_satisfied(self.offer, self.basket)
  160. self.condition.consume_items(self.offer, self.basket, [])
  161. assert self.condition.is_satisfied(self.offer, self.basket) is False
  162. @pytest.mark.django_db
  163. class TestCoverageCondition:
  164. @pytest.fixture(autouse=True)
  165. def setUp(self, range_some, products_some, empty_basket, coverage_condition):
  166. self.products = products_some
  167. self.range = range_some
  168. self.basket = empty_basket
  169. self.condition = coverage_condition
  170. self.offer = mock.Mock()
  171. def test_empty_basket_fails(self):
  172. assert self.condition.is_satisfied(self.offer, self.basket) is False
  173. def test_empty_basket_fails_partial_condition(self):
  174. assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
  175. def test_single_item_fails(self):
  176. add_product(self.basket, product=self.products[0])
  177. assert self.condition.is_satisfied(self.offer, self.basket) is False
  178. def test_not_discountable_item_fails(self):
  179. self.products[0].is_discountable = False
  180. self.products[0].save()
  181. add_product(self.basket, product=self.products[0])
  182. add_product(self.basket, product=self.products[1])
  183. assert self.condition.is_satisfied(self.offer, self.basket) is False
  184. def test_single_item_passes_partial_condition(self):
  185. add_product(self.basket, product=self.products[0])
  186. assert self.condition.is_partially_satisfied(self.offer, self.basket)
  187. def test_upsell_message(self):
  188. add_product(self.basket, product=self.products[0])
  189. assert "Buy 1 more" in self.condition.get_upsell_message(
  190. self.offer, self.basket
  191. )
  192. def test_duplicate_item_fails(self):
  193. add_product(self.basket, quantity=2, product=self.products[0])
  194. assert self.condition.is_satisfied(self.offer, self.basket) is False
  195. def test_duplicate_item_passes_partial_condition(self):
  196. add_product(self.basket, quantity=2, product=self.products[0])
  197. assert self.condition.is_partially_satisfied(self.offer, self.basket)
  198. def test_covering_items_pass(self):
  199. add_product(self.basket, product=self.products[0])
  200. add_product(self.basket, product=self.products[1])
  201. assert self.condition.is_satisfied(self.offer, self.basket)
  202. def test_covering_items_fail_partial_condition(self):
  203. add_product(self.basket, product=self.products[0])
  204. add_product(self.basket, product=self.products[1])
  205. assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
  206. def test_covering_items_are_consumed(self):
  207. add_product(self.basket, product=self.products[0])
  208. add_product(self.basket, product=self.products[1])
  209. self.condition.consume_items(self.offer, self.basket, [])
  210. assert 0 == self.basket.num_items_without_discount
  211. def test_consumed_items_checks_affected_items(self):
  212. # Create new offer
  213. range = models.Range.objects.create(
  214. name="All products", includes_all_products=True
  215. )
  216. cond = models.CoverageCondition(range=range, type="Coverage", value=2)
  217. # Get 4 distinct products in the basket
  218. self.products.extend([factories.create_product(), factories.create_product()])
  219. for product in self.products:
  220. add_product(self.basket, product=product)
  221. assert cond.is_satisfied(self.offer, self.basket)
  222. cond.consume_items(self.offer, self.basket, [])
  223. assert 2 == self.basket.num_items_without_discount
  224. assert cond.is_satisfied(self.offer, self.basket)
  225. cond.consume_items(self.offer, self.basket, [])
  226. assert 0 == self.basket.num_items_without_discount
  227. @pytest.mark.django_db
  228. class TestConditionProxyModels(object):
  229. def test_name_and_description(self, range):
  230. """
  231. Tests that the condition proxy classes all return a name and
  232. description. Unfortunately, the current implementations means
  233. a valid range and value are required.
  234. """
  235. for type, __ in models.Condition.TYPE_CHOICES:
  236. condition = models.Condition(type=type, range=range, value=5)
  237. assert all([condition.name, condition.description, str(condition)])
  238. def test_proxy(self, range):
  239. for type, __ in models.Condition.TYPE_CHOICES:
  240. condition = models.Condition(type=type, range=range, value=5)
  241. proxy = condition.proxy()
  242. assert condition.type == proxy.type
  243. assert condition.range == proxy.range
  244. assert condition.value == proxy.value
  245. class TestCustomCondition(TestCase):
  246. def setUp(self):
  247. self.condition = custom.create_condition(BasketOwnerCalledBarry)
  248. self.offer = models.ConditionalOffer(condition=self.condition)
  249. self.basket = Basket()
  250. def test_is_not_satisfied_by_non_match(self):
  251. self.basket.owner = factories.UserFactory(first_name="Alan")
  252. assert self.offer.is_condition_satisfied(self.basket) is False
  253. def test_is_satisfied_by_match(self):
  254. self.basket.owner = factories.UserFactory(first_name="Barry")
  255. assert self.offer.is_condition_satisfied(self.basket)