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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import pytest
  2. from decimal import Decimal as D
  3. from unittest import mock
  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)