diff --git a/setup.cfg b/setup.cfg index 2556e7c..8f1edfc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,8 @@ norecursedirs = *.egg .eggs dist build docs .tox .git __pycache__ # Strict `@xfail` by default: xfail_strict = true +DJANGO_SETTINGS_MODULE = autodonate.settings + # Extra options: addopts = --strict-markers diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..feb0dbc --- /dev/null +++ b/tests/api/__init__.py @@ -0,0 +1 @@ +"""Package for the tests which test API app.""" diff --git a/tests/api/conftest.py b/tests/api/conftest.py new file mode 100644 index 0000000..95c53e5 --- /dev/null +++ b/tests/api/conftest.py @@ -0,0 +1,16 @@ +"""Shared fixtures for tests in ``api/`` folder.""" +from django.core.management import call_command +from pytest import fixture + +from autodonate.models import Config +from tests import api + + +@fixture(scope="session", autouse=True) +def django_db_setup(django_db_setup, django_db_blocker): + """Additional setup DB steps.""" + with django_db_blocker.unblock(): + api.test_config.set_up() + api.test_product.set_up() + api.test_player.set_up() + api.test_donation.set_up() diff --git a/tests/api/test_config.py b/tests/api/test_config.py new file mode 100644 index 0000000..778169b --- /dev/null +++ b/tests/api/test_config.py @@ -0,0 +1,86 @@ +"""Tests for ``/api/config/`` path.""" +from json import loads + +from pytest import fixture, mark +from rest_framework.response import Response +from rest_framework.test import APIClient +from structlog import get_logger + +from autodonate.models import Config + +log = get_logger() + +response: Response = None + + +def set_up() -> None: + """Set up some variables for config testing. + + This called once only in ``conftest.py``, do not touch. + + Raises: + NameError: When ``test_config.response`` variable already set, + so this function called second time. + """ + Config.set("something1", "1", True) + Config.set("something2", "2", True) + Config.set("something3", "3", False) + + global response + if response is not None: + raise NameError("This function must be called once!") + response = APIClient().get("/api/config/") + + +class TestConfig: + """Tests for ``/api/config/`` path.""" + + @fixture(scope="session") + def data(self): + """Parse and return dict with answer.""" + return loads(response.content) + + def test_status_code(self) -> None: + """``status_code`` must be 200 - okay.""" + assert 200 == response.status_code + + @mark.parametrize( + "key,value", + [ + ("something1", '"1"'), + ("something2", '"2"'), + ], + ) + def test_exist_object(self, data, key: str, value: str) -> None: + """Is object in response data.""" + assert {"key": key, "value": value, "read_only": False} in data + + @mark.parametrize( + "key,value", + [("something3", '"3"')], + ) + def test_object_not_exist(self, data, key: str, value: str) -> None: + """Is object not in response data.""" + assert {"key": key, "value": value, "read_only": False} not in data + + def test_no_public_field_in_api_response(self, data) -> None: + """User shouldn't see ``public`` field in API response, because it is always True.""" + for row in data: + assert "public" not in row.keys() + + +@mark.skip("Feature for this test didn't implemented.") +class TestConfigNotImplemented: + """Tests for features that was planned, but didn't implemented. + + Todo: + Implement features and move this tests to ``TestConfig``. + """ + + def test_value_returning_string(self, data) -> None: + """Check that we returning string, not string in string. + + Example: + Return ``'abc'`` instead of ``"'abc'"``. + """ + assert {"key": "something1", "value": "1", "read_only": False} in data diff --git a/tests/api/test_donation.py b/tests/api/test_donation.py new file mode 100644 index 0000000..b729620 --- /dev/null +++ b/tests/api/test_donation.py @@ -0,0 +1,98 @@ +"""Tests for ``/api/donation/`` path.""" +from json import loads + +from pytest import fixture, mark +from rest_framework.response import Response +from rest_framework.test import APIClient +from structlog import get_logger + +from autodonate.models import Config, Donation, Player, Product + +log = get_logger() + +response: Response = None + + +def set_up() -> None: + """Set up some variables for donation testing. + + This called once only in ``conftest.py``, do not touch. + + Raises: + NameError: When ``test_donation.response`` variable already set, + so this function called second time. + """ + products, players = [], [] + for num in range(1, 7): + product = Product(name=str(num), price=num) + player = Player(nickname="Steve" + str(num)) + + product.save() + player.save() + + products.append(product) + players.append(player) + + Donation(product=product, player=player).save() + + global response + if response is not None: + raise NameError("This function must be called once!") + response = APIClient().get("/api/donation/") + + for product in products: + product.delete() + for player in players: + player.delete() + + +class TestDonation: + """Tests for ``/api/donation/`` path.""" + + @fixture(scope="session") + def data(self): + """Parse and return dict with answer.""" + return loads(response.content) + + def test_status_code(self) -> None: + """``status_code`` must be 200 - okay.""" + assert 200 == response.status_code + + def test_response_len_6(self, data) -> None: + """API must return only first 6 records.""" + assert len(data) == 6 + + def test_correct_order(self, data) -> None: + """Suring about correct order in response.""" + for i, record in enumerate(data): + i += 1 # start with 1, not 0 + assert record["product"]["name"] == str(i) + assert record["product"]["price"] == i + assert record["player"]["nickname"] == "Steve" + str(i) + + @mark.skip("This is a bug") + @mark.django_db + def test_do_not_show_not_enabled_products(self) -> None: + """If ``product`` is not enabled, it should be hidden.""" + Donation.objects.all().delete() + + products, players = [], [] + for num in range(3): + product = Product(name=str(num), price=num) + player = Player(nickname="Steve" + str(num)) + + product.save() + player.save() + + products.append(product) + players.append(player) + + Donation(product=product, player=player).save() + + response = APIClient().get("/api/donation/") + assert loads(response.content) == [] + + for product in products: + product.delete() + for player in players: + player.delete() diff --git a/tests/api/test_player.py b/tests/api/test_player.py new file mode 100644 index 0000000..a146fb8 --- /dev/null +++ b/tests/api/test_player.py @@ -0,0 +1,95 @@ +"""Tests for ``/api/player/`` path.""" +from json import loads +from typing import Any, Dict, List + +from pytest import fixture, mark +from rest_framework.response import Response +from rest_framework.test import APIClient +from structlog import get_logger + +from autodonate.models import Player + +log = get_logger() + +_response: List[Response] = [] + +parametrize_num: int + + +def set_up() -> None: + """Set up some variables for player testing. + + This called once only in ``conftest.py``, do not touch. + + Raises: + NameError: When ``test_player._response`` variable already set, + so this function called second time. + """ + global _response + if _response != []: + raise NameError("This function must be called once!") + + for player_nick in ["something1", "something2", "something3"]: + Player(nickname=player_nick).save() + + _response.append(APIClient().get(f"/api/player/{player_nick}/")) + + _response.append(APIClient().get(f"/api/player/")) + + +class TestPlayer: + """Tests for ``/api/player/`` path.""" + + @fixture(scope="session", params=[0, 1, 2]) + def response(self, request) -> Response: + """Response object for parametrizing tests.""" + global parametrize_num + parametrize_num = request.param + return _response[parametrize_num] + + def test_status_code(self, response) -> None: + """``status_code`` must be 200 - okay.""" + assert 200 == response.status_code + + def test_expected_output(self, response) -> None: + """Is object in response data.""" + assert {"nickname": "something" + str(parametrize_num + 1)} == response.data + + +class TestPlayerRootPath: + """Actually, root path of ``/api/player/`` should return 404. + + So in this way, pontential hacker can't get list with all players, he + will get some info, only if have players nickname, and setted it as + ``/api/player/{nickname}/``. + """ + + @fixture(scope="session") + def response(self) -> Response: + """Actually just return fourth element from ``_response``. + + Becouse fourth element in ``_response`` list is response to API + root. See ``set_up()`` method here. + + Returns: + Cached ``Response`` element. + """ + return _response[3] + + def test_status_code(self, response: Response) -> None: + """Status code should be 404 here. + + See description of the class. + """ + assert 404 == response.status_code + + @mark.skip("Feature for this test didn't implemented.") + def test_status_code_unathorized(self, response: Response) -> None: + """Status code should be 401 here. + + We should return 401 (Unathorized) instead of just 404 (Not found). + + Todo: + Implement this, and replace ``test_status_code`` with this test. + """ + assert 401 == response.status_code diff --git a/tests/api/test_product.py b/tests/api/test_product.py new file mode 100644 index 0000000..bcf9972 --- /dev/null +++ b/tests/api/test_product.py @@ -0,0 +1,102 @@ +"""Tests for ``/api/product/`` path.""" +from json import loads +from typing import Any, Dict, List + +from pytest import fixture, mark +from rest_framework.response import Response +from rest_framework.test import APIClient +from structlog import get_logger + +from autodonate.models import Product + +log = get_logger() + +response: Response = None + +products: List[Product] = [] + + +def get_fields(num: int, enabled: bool, returning: bool = False) -> Dict[str, Any]: # type: ignore[misc] # explicit any + """Return dict with keys which need to be passed in model. + + Args: + num: Custom unique number of row. + enabled: Is row enabled? + returning: Return dict which would be returned in API. + + Returns: + Dict which needs to be unpacked with **. + + Example: + .. code-block: python + + from autodonate.models import Product + Product(**get_fields(1, True)) + """ + to_return = { + "id": str(num), + "name": "something" + str(num), + "price": 123, + "long_description": "long description", + "group": "test", + "image": "image", + "max_in_cart": num, + "enabled": enabled, + } + + if returning: + to_return["image"] = "http://testserver/media/image" + to_return.pop("enabled") + + return to_return + + +def set_up() -> None: + """Set up some variables for product testing. + + This called once only in ``conftest.py``, do not touch. + + Raises: + NameError: When ``test_product.response`` variable already set, + so this function called second time. + """ + # Add some fields in DB + global products + for fields in [get_fields(1, True), get_fields(2, True), get_fields(3, False)]: + product = Product(**fields) + products.append(product) + product.save() + + # Get and cache `Response` object + global response + if response is not None: + raise NameError("This function must be called once!") + response = APIClient().get("/api/product/") + + +class TestProduct: + """Tests for ``/api/product/`` path.""" + + @fixture(scope="session") + def data(self): + """Parse and return dict with answer.""" + return loads(response.content) + + def test_status_code(self) -> None: + """``status_code`` must be 200 - okay.""" + assert 200 == response.status_code + + @mark.parametrize("num", [1, 2]) + def test_exist_object(self, data, num: int) -> None: + """Is object in response data.""" + assert get_fields(num, True, True) in data + + @mark.parametrize("num", [3]) + def test_object_not_exist(self, data, num: int) -> None: + """Is object not in response data.""" + assert get_fields(num, False, True) not in data + + def test_no_enabled_field_in_api_response(self, data) -> None: + """User shouldn't see ``enabled`` field in API response, because it is always True.""" + for row in data: + assert "enabled" not in row.keys() diff --git a/tests/test_true.py b/tests/test_true.py deleted file mode 100644 index 8b45756..0000000 --- a/tests/test_true.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Stub.""" - - -def test_true(): - """Stub.""" - assert True