diff --git a/poetry.lock b/poetry.lock index bf924f9..b6a5fdc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -17,6 +17,22 @@ python-versions = ">=3.7" [package.extras] tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] +[[package]] +name = "async-generator" +version = "1.10" +description = "Async generators and context managers for Python 3.5+" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "attrs" version = "22.1.0" @@ -97,6 +113,17 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.3.1" @@ -296,6 +323,32 @@ python-versions = ">=3.6" [package.dependencies] Django = ">=3.2" +[[package]] +name = "factory-boy" +version = "3.2.1" +description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Faker = ">=0.7.0" + +[package.extras] +dev = ["coverage", "django", "flake8", "isort", "pillow", "sqlalchemy", "mongoengine", "wheel (>=0.32.0)", "tox", "zest.releaser"] +doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] + +[[package]] +name = "faker" +version = "14.2.1" +description = "Faker is a Python package that generates fake data for you." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +python-dateutil = ">=2.4" + [[package]] name = "filelock" version = "3.8.0" @@ -366,6 +419,14 @@ gevent = ["gevent (>=1.4.0)"] setproctitle = ["setproctitle"] tornado = ["tornado (>=0.2)"] +[[package]] +name = "h11" +version = "0.13.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "identify" version = "2.5.5" @@ -467,6 +528,17 @@ category = "main" optional = false python-versions = ">=3.8" +[[package]] +name = "outcome" +version = "1.2.0" +description = "Capture the outcome of Python function calls." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" + [[package]] name = "packaging" version = "21.3" @@ -567,6 +639,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pygments" version = "2.13.0" @@ -602,25 +682,46 @@ beautifulsoup4 = ">=4.5,<5.0" packaging = ">=20" requests = ">=2.20,<3.0" +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pytest" -version = "7.1.3" +version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" -tomli = ">=1.0.0" +toml = "*" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-base-url" +version = "2.0.0" +description = "pytest plugin for URL based testing" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +pytest = ">=3.0.0,<8.0.0" +requests = ">=2.9" [[package]] name = "pytest-cov" @@ -652,6 +753,65 @@ pytest = ">=5.4.0" docs = ["sphinx", "sphinx-rtd-theme"] testing = ["django", "django-configurations (>=2.0)"] +[[package]] +name = "pytest-html" +version = "3.1.1" +description = "pytest plugin for generating HTML reports" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pytest = ">=5.0,<6.0.0 || >6.0.0" +pytest-metadata = "*" + +[[package]] +name = "pytest-metadata" +version = "2.0.2" +description = "pytest plugin for test session metadata" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +pytest = ">=3.0.0,<8.0.0" + +[[package]] +name = "pytest-selenium" +version = "4.0.0" +description = "pytest plugin for Selenium" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +pytest = ">=6.0.0,<7.0.0" +pytest-base-url = ">=2.0.0,<3.0.0" +pytest-html = ">=2.0.0" +pytest-variables = ">=2.0.0,<3.0.0" +requests = ">=2.26.0,<3.0.0" +selenium = ">=4.0.0,<5.0.0" +tenacity = ">=6.0.0,<7.0.0" + +[package.extras] +appium = ["appium-python-client (>=2.0.0,<3.0.0)"] + +[[package]] +name = "pytest-variables" +version = "2.0.0" +description = "pytest plugin for providing variables to tests/fixtures" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +pytest = ">=3.0.0,<8.0.0" + +[package.extras] +toml = ["toml"] +yaml = ["pyyaml"] +hjson = ["hjson"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -724,6 +884,20 @@ python-versions = "*" [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +name = "selenium" +version = "4.4.3" +description = "" +category = "dev" +optional = false +python-versions = "~=3.7" + +[package.dependencies] +certifi = ">=2021.10.8" +trio = ">=0.17,<1.0" +trio-websocket = ">=0.9,<1.0" +urllib3 = {version = ">=1.26,<2.0", extras = ["socks"]} + [[package]] name = "setuptools-scm" version = "7.0.5" @@ -749,6 +923,22 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "soupsieve" version = "2.3.2.post1" @@ -765,6 +955,20 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "tenacity" +version = "6.3.1" +description = "Retry code until it succeeds" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + [[package]] name = "toml" version = "0.10.2" @@ -781,6 +985,36 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "trio" +version = "0.21.0" +description = "A friendly Python library for async concurrency and I/O" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +async-generator = ">=1.9" +attrs = ">=19.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +idna = "*" +outcome = "*" +sniffio = "*" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.9.2" +description = "WebSocket library for Trio" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +async-generator = ">=1.10" +trio = ">=0.11" +wsproto = ">=0.14" + [[package]] name = "typing-extensions" version = "4.3.0" @@ -805,6 +1039,9 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +[package.dependencies] +PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} + [package.extras] brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] @@ -849,10 +1086,21 @@ Brotli = {version = "*", optional = true, markers = "extra == \"brotli\""} [package.extras] brotli = ["brotli"] +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +category = "dev" +optional = false +python-versions = ">=3.7.0" + +[package.dependencies] +h11 = ">=0.9.0,<1" + [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "b0fdaacb7c4e3ecc472664d3091567919a34505639c79e6ecfe8f58a00020566" +content-hash = "a7f456302a8e5cd5023a9f6ced2930e9ec4a2e964daae04f24a486b2071fd3a0" [metadata.files] ansicon = [] @@ -860,6 +1108,8 @@ asgiref = [ {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, ] +async-generator = [] +atomicwrites = [] attrs = [] beautifulsoup4 = [ {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, @@ -932,6 +1182,7 @@ brotli = [ {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"}, ] certifi = [] +cffi = [] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, @@ -970,6 +1221,8 @@ django-environ = [ {file = "django_environ-0.9.0-py2.py3-none-any.whl", hash = "sha256:f21a5ef8cc603da1870bbf9a09b7e5577ab5f6da451b843dbcc721a7bca6b3d9"}, ] django-extensions = [] +factory-boy = [] +faker = [] filelock = [] fonttools = [] freezegun = [] @@ -978,6 +1231,7 @@ gunicorn = [ {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] +h11 = [] identify = [] idna = [] iniconfig = [ @@ -994,6 +1248,7 @@ matplotlib = [] model-bakery = [] nodeenv = [] numpy = [] +outcome = [] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -1014,13 +1269,19 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] +pycparser = [] pygments = [] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pypi-simple = [] -pytest = [] +pysocks = [] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +pytest-base-url = [] pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, @@ -1029,6 +1290,13 @@ pytest-django = [ {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"}, {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"}, ] +pytest-html = [ + {file = "pytest-html-3.1.1.tar.gz", hash = "sha256:3ee1cf319c913d19fe53aeb0bc400e7b0bc2dbeb477553733db1dad12eb75ee3"}, + {file = "pytest_html-3.1.1-py3-none-any.whl", hash = "sha256:b7f82f123936a3f4d2950bc993c2c1ca09ce262c9ae12f9ac763a2401380b455"}, +] +pytest-metadata = [] +pytest-selenium = [] +pytest-variables = [] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, @@ -1075,16 +1343,20 @@ requests-toolbelt = [ {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, ] +selenium = [] setuptools-scm = [] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +sniffio = [] +sortedcontainers = [] soupsieve = [ {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, ] sqlparse = [] +tenacity = [] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -1093,6 +1365,8 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +trio = [] +trio-websocket = [] typing-extensions = [] tzdata = [] urllib3 = [] @@ -1105,3 +1379,4 @@ whitenoise = [ {file = "whitenoise-6.2.0-py3-none-any.whl", hash = "sha256:8e9c600a5c18bd17655ef668ad55b5edf6c24ce9bdca5bf607649ca4b1e8e2c2"}, {file = "whitenoise-6.2.0.tar.gz", hash = "sha256:8fa943c6d4cd9e27673b70c21a07b0aa120873901e099cd46cab40f7cc96d567"}, ] +wsproto = [] diff --git a/pyproject.toml b/pyproject.toml index ab22630..7e96915 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,12 +24,15 @@ freezegun = "^1.2.1" [tool.poetry.dev-dependencies] pre-commit = "^2.7" -pytest = "^7.1" +pytest = "^6.0" pytest-django = "^4.5" model-bakery = "^1.1" pytest-cov = "^3.0" poetry-deps-scanner = "^2.0" invoke = "^1.7.0" +factory-boy = "^3.2.1" +pytest-selenium = "^4.0.0" +selenium = "^4.4.3" [tool.black] target-version = ['py310'] @@ -38,7 +41,7 @@ target-version = ['py310'] profile = "black" [tool.pytest.ini_options] -addopts = "--color=yes" +addopts = "--color=yes --driver Firefox" minversion = "6.0" DJANGO_SETTINGS_MODULE = "checkout.settings" testpaths = [ diff --git a/src/conftest.py b/src/conftest.py new file mode 100644 index 0000000..5e28cd3 --- /dev/null +++ b/src/conftest.py @@ -0,0 +1,19 @@ +import pytest +from django.core.management import call_command + + +@pytest.fixture(scope="session") +def _collectstatic(): + call_command("collectstatic", interactive=False, verbosity=0) + + +@pytest.fixture() +def live_server(settings, live_server): + settings.STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage" + return live_server + + +@pytest.fixture() +def selenium(selenium): + selenium.implicitly_wait(3) + return selenium diff --git a/src/purchase/tests.py b/src/purchase/tests.py deleted file mode 100644 index a39b155..0000000 --- a/src/purchase/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/src/purchase/tests/__init__.py b/src/purchase/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/purchase/tests/factories.py b/src/purchase/tests/factories.py new file mode 100644 index 0000000..5a47723 --- /dev/null +++ b/src/purchase/tests/factories.py @@ -0,0 +1,84 @@ +import random +from functools import partial + +import factory +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import Group, Permission + +from common.models import User +from purchase.models import Basket, BasketItem, PaymentMethod, Product + +USER_PASSWORD = "test_password" + + +class CashierFactory(factory.django.DjangoModelFactory): + class Meta: + model = User + + username = factory.Faker("user_name") + password = make_password(USER_PASSWORD) + is_active = True + is_staff = True + + @factory.post_generation + def groups(self, create, extracted, **kwargs): + if create: + self.groups.add(CashierGroupFactory()) + + +class ProductFactory(factory.django.DjangoModelFactory): + class Meta: + model = Product + + name = factory.Faker("text", max_nb_chars=80) + unit_price_cents = factory.LazyFunction(partial(random.randint, 80, 650)) + + +class PaymentMethodFactory(factory.django.DjangoModelFactory): + class Meta: + model = PaymentMethod + + name = factory.Faker("text", max_nb_chars=30) + + +class BasketWithItemsFactory(factory.django.DjangoModelFactory): + class Meta: + model = Basket + + payment_method = factory.Iterator(PaymentMethod.objects.all()) + + @factory.post_generation + def items(self, create, extracted, **kwargs): + if create: + products = Product.objects.order_by("?") + quantity = random.randint(1, len(products)) + for product in products[:quantity]: + BasketItem.objects.create( + product=product, + basket=self, + quantity=random.randint(1, 4), + unit_price_cents=product.unit_price_cents, + ) + + +class CashierGroupFactory(factory.django.DjangoModelFactory): + class Meta: + model = Group + + name = "Caissier" + + @factory.post_generation + def permissions(self, create, extracted, **kwargs): + if create: + self.permissions.add( + Permission.objects.get(codename="add_basket"), + Permission.objects.get(codename="change_basket"), + Permission.objects.get(codename="delete_basket"), + Permission.objects.get(codename="view_basket"), + Permission.objects.get(codename="add_basketitem"), + Permission.objects.get(codename="change_basketitem"), + Permission.objects.get(codename="delete_basketitem"), + Permission.objects.get(codename="view_basketitem"), + Permission.objects.get(codename="view_paymentmethod"), + Permission.objects.get(codename="view_product"), + ) diff --git a/src/purchase/tests/test_cashier_flow.py b/src/purchase/tests/test_cashier_flow.py new file mode 100644 index 0000000..43138b8 --- /dev/null +++ b/src/purchase/tests/test_cashier_flow.py @@ -0,0 +1,291 @@ +import time + +import freezegun +from django.urls import reverse +from pytest_django.live_server_helper import LiveServer +from selenium.webdriver import ActionChains, Keys +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.support.wait import WebDriverWait + +from common.models import User +from purchase.models import Basket +from purchase.tests.factories import ( + USER_PASSWORD, + BasketWithItemsFactory, + CashierFactory, + PaymentMethodFactory, + ProductFactory, +) + + +@freezegun.freeze_time("2022-09-24 19:01:00+0200") +def test_cashier_create_and_update_basket(live_server: LiveServer, selenium: WebDriver): + wait = WebDriverWait(selenium, 10) + assert Basket.objects.count() == 0 + + # Setup data + cashier = CashierFactory() + products = [ + ProductFactory(), + ProductFactory(), + ProductFactory(), + ] + payment_methods = [ + PaymentMethodFactory(), + PaymentMethodFactory(), + PaymentMethodFactory(), + ] + + login(live_server, selenium, cashier) + + # Assert products are displayed + redirect_url = live_reverse(live_server, "purchase:new") + wait.until(lambda driver: driver.current_url == redirect_url) + displayed_products = selenium.find_elements(By.CSS_SELECTOR, ".card.h-100") + assert len(displayed_products) == len(products) + for product, displayed_product in zip(products, displayed_products): + assert ( + product.name + == displayed_product.find_element(By.CLASS_NAME, "card-title").text + ) + + # Assert quantity of all products is 0 + for displayed_product in displayed_products: + quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput") + quantity = int(quantity_input.get_attribute("value")) + assert quantity == 0 + + # Click on - on product 1 + displayed_product = displayed_products[0] + displayed_product.find_element(By.CLASS_NAME, "btn-danger").click() + + # Assert quantity is still 0 + quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput") + quantity = int(quantity_input.get_attribute("value")) + assert quantity == 0 + + # Click two times on + on product 1 + button_plus = displayed_product.find_element(By.CLASS_NAME, "btn-success") + button_plus.click() + button_plus.click() + + # Assert quantity is 2 + quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput") + quantity = int(quantity_input.get_attribute("value")) + assert quantity == 2 + + # Adjust manually quantity for product 2: 4 + displayed_product = displayed_products[1] + quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput") + chain = ActionChains(selenium) + chain.double_click(quantity_input).perform() + quantity_input.send_keys("4") + + # Don't add payment method + # Save + selenium.find_element(By.ID, "submit-id-submit").click() + + # Assert entries saved in DB (new basket with proper products) + assert Basket.objects.count() == 1 + basket = Basket.objects.priced().first() + assert basket.payment_method is None + assert basket.items.count() == 2 + assert basket.items.get(product=products[0]).quantity == 2 + assert ( + basket.items.get(product=products[0]).unit_price_cents + == products[0].unit_price_cents + ) + assert basket.items.get(product=products[1]).quantity == 4 + assert ( + basket.items.get(product=products[1]).unit_price_cents + == products[1].unit_price_cents + ) + + # Assert redirected to basket update view + redirect_url = live_reverse(live_server, "purchase:update", pk=basket.pk) + wait.until(lambda driver: driver.current_url == redirect_url) + + # Assert message in green for successful basket creation + created_message = selenium.find_element(By.CSS_SELECTOR, ".messages .alert-success") + assert created_message.text == "Panier correctement créé." + + # Assert message in red for missing payment method + missing_payment = selenium.find_element(By.CSS_SELECTOR, ".alert.alert-danger") + assert missing_payment.text == "Moyen de paiement manquant." + + # Assert ID, price, date & product quantities + # Selected products have a green background + title = selenium.find_element(By.TAG_NAME, "h1") + assert title.text == f"Panier n°{basket.pk} {basket.price/100:.2f}€" + date = selenium.find_element(By.CLASS_NAME, "metadata") + assert date.text == "24 septembre 2022 19:01" + + displayed_products = selenium.find_elements(By.CSS_SELECTOR, ".card.h-100") + displayed_product = displayed_products[0] + assert "bg-success" in displayed_product.get_attribute("class") + quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput") + quantity = int(quantity_input.get_attribute("value")) + assert quantity == 2 + displayed_product = displayed_products[1] + assert "bg-success" in displayed_product.get_attribute("class") + quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput") + quantity = int(quantity_input.get_attribute("value")) + assert quantity == 4 + displayed_product = displayed_products[2] + assert "bg-success" not in displayed_product.get_attribute("class") + quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput") + quantity = int(quantity_input.get_attribute("value")) + assert quantity == 0 + + # Click on - on product 2 + displayed_product = displayed_products[1] + displayed_product.find_element(By.CLASS_NAME, "btn-danger").click() + + # Assert quantity is 3 + quantity_input = displayed_product.find_element(By.CLASS_NAME, "numberinput") + quantity = int(quantity_input.get_attribute("value")) + assert quantity == 3 + + # Add payment method + selenium.find_element(By.TAG_NAME, "html").send_keys(Keys.END) + time.sleep(1) + selenium.find_element(By.ID, f"id_payment_method_{payment_methods[1].pk}").click() + + # Save + selenium.find_element(By.ID, "submit-id-submit").click() + + # Assert changed in DB + assert Basket.objects.count() == 1 + basket = Basket.objects.priced().first() + assert basket.payment_method == payment_methods[1] + assert basket.items.count() == 2 + assert basket.items.get(product=products[0]).quantity == 2 + assert ( + basket.items.get(product=products[0]).unit_price_cents + == products[0].unit_price_cents + ) + assert basket.items.get(product=products[1]).quantity == 3 + assert ( + basket.items.get(product=products[1]).unit_price_cents + == products[1].unit_price_cents + ) + + # Assert redirected to same view + redirect_url = live_reverse(live_server, "purchase:update", pk=basket.pk) + wait.until(lambda driver: driver.current_url == redirect_url) + + # Assert message in green for successful basket update + created_message = selenium.find_element(By.CSS_SELECTOR, ".messages .alert-success") + assert created_message.text == "Panier correctement modifié." + + # Assert no more red message + missing_payment = selenium.find_elements(By.CSS_SELECTOR, ".alert.alert-danger") + assert len(missing_payment) == 0 + + +def login( + live_server: LiveServer, selenium: WebDriver, cashier: User, url: str = "/" +) -> None: + # Go to page + url = live_url(live_server, url) + selenium.get(url) + # Login + selenium.find_element(By.ID, "id_username").send_keys(cashier.username) + selenium.find_element(By.ID, "id_password").send_keys(USER_PASSWORD) + selenium.find_element(By.ID, "id_password").send_keys(Keys.RETURN) + + +@freezegun.freeze_time("2022-09-24 19:03:00+0200") +def test_baskets_list(live_server: LiveServer, selenium: WebDriver): + wait = WebDriverWait(selenium, 10) + + # Setup test data + cashier = CashierFactory() + _ = [ + ProductFactory(), + ProductFactory(), + ProductFactory(), + ] + _ = [ + PaymentMethodFactory(), + PaymentMethodFactory(), + PaymentMethodFactory(), + ] + with freezegun.freeze_time("2022-09-24 19:01:00+0200"): + basket_with_payment_method = BasketWithItemsFactory() + basket_with_payment_method = Basket.objects.priced().get( + pk=basket_with_payment_method.pk + ) + with freezegun.freeze_time("2022-09-24 19:02:00+0200"): + basket_no_payment_method = BasketWithItemsFactory(payment_method=None) + basket_no_payment_method = Basket.objects.priced().get( + pk=basket_no_payment_method.pk + ) + + # Login + url = reverse("purchase:list") + login(live_server, selenium, cashier, url) + + # Assert first basket (last created) has yellow background + # Assert basket info displayed + displayed_baskets = selenium.find_elements(By.CSS_SELECTOR, ".card.h-100") + first_basket = displayed_baskets[0] + assert "bg-warning" in first_basket.get_attribute("class") + text = first_basket.text.replace("\n", " ") + assert f"n°{basket_no_payment_method.pk} " in text + expected_articles_count = basket_no_payment_method.items.count() + assert f" {expected_articles_count} article" in text + expected_price = basket_no_payment_method.price / 100 + assert f" {expected_price:.2f}€" in text + expected_payment_method = "-" + assert f" {expected_payment_method} " in text + assert "19:02" in text + + # Assert second basket (first created) doesn't have yellow background + # Assert basket info displayed including payment method + second_basket = displayed_baskets[1] + assert "bg-warning" not in second_basket.get_attribute("class") + text = second_basket.text.replace("\n", " ") + assert f"n°{basket_with_payment_method.pk} " in text + expected_articles_count = basket_with_payment_method.items.count() + assert f" {expected_articles_count} article" in text + expected_price = basket_with_payment_method.price / 100 + assert f" {expected_price:.2f}€" in text + expected_payment_method = basket_with_payment_method.payment_method.name + assert f" {expected_payment_method} " in text + assert "19:01" in text + + # Click on delete on second basket + second_basket.find_element(By.CLASS_NAME, "btn-danger").click() + + # Confirm deletion + selenium.find_element(By.CLASS_NAME, "btn-danger").click() + + # Assert object deleted in DB + assert Basket.objects.count() == 1 + assert Basket.objects.first() == basket_no_payment_method + + # Assert redirected to list view + wait.until( + lambda driver: driver.current_url == live_reverse(live_server, "purchase:list") + ) + + # Click on edit on remaining basket + displayed_baskets = selenium.find_elements(By.CSS_SELECTOR, ".card.h-100") + displayed_baskets[0].find_element(By.CLASS_NAME, "btn-primary").click() + + # Assert redirected to edit view + redirect_url = live_reverse( + live_server, "purchase:update", pk=basket_no_payment_method.pk + ) + wait.until(lambda driver: driver.current_url == redirect_url) + + +def live_reverse(live_server: LiveServer, url_name: str, **kwargs) -> str: + path = reverse(url_name, kwargs=kwargs) + return live_url(live_server, path) + + +def live_url(live_server: LiveServer, path: str) -> str: + return live_server.url + path