From 8fcba3db8a990d0034f31799c6835c0731adb686 Mon Sep 17 00:00:00 2001 From: TingYi Date: Mon, 5 Aug 2024 14:59:53 +0800 Subject: [PATCH] test (authentication): verification #7 --- .pre-commit-config.yaml | 4 +- poetry.lock | 41 ++++- .../tests/test_authentication.py | 157 ++++++++++++++++++ pyproject.toml | 1 + 4 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 projects/account_password_management/tests/test_authentication.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bc208cb..e13b07f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,8 +20,8 @@ repos: hooks: - id: ruff name: ruff - entry: ruff - args: ['check .'] + entry: ruff check + args: ['.'] language: system types: [python] - repo: local diff --git a/poetry.lock b/poetry.lock index 94e591f..55c5ee6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -184,6 +184,20 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] +[[package]] +name = "freezegun" +version = "1.5.1" +description = "Let your Python tests travel through time" +optional = false +python-versions = ">=3.7" +files = [ + {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, + {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + [[package]] name = "greenlet" version = "3.0.3" @@ -743,6 +757,20 @@ pytest = ">=7.0.0,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.1" @@ -844,6 +872,17 @@ files = [ {file = "ruff-0.5.6.tar.gz", hash = "sha256:07c9e3c2a8e1fe377dd460371c3462671a728c981c3205a5217291422209f642"}, ] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1011,4 +1050,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "4611e3ec78412d7c49627704a9f2ee728f58e2a97d773f51310ca8459e07be2a" +content-hash = "95b0b08c44ba34640b78e28f7cd800443fb6d3a39ad5521494b8048047e6ac20" diff --git a/projects/account_password_management/tests/test_authentication.py b/projects/account_password_management/tests/test_authentication.py new file mode 100644 index 0000000..e36633d --- /dev/null +++ b/projects/account_password_management/tests/test_authentication.py @@ -0,0 +1,157 @@ +from datetime import datetime, timedelta + +import pytest +from fastapi import status +from fastapi.testclient import TestClient +from freezegun import freeze_time +from src.api.authentication.controller import ( + BLOCK_VERIFICATION_TIME, MAX_FAILED_VERIFICATION_ATTEMPTS) +from src.main import ACCOUNT_API_ROUTE, AUTHENTICATION_API_ROUTE + +from lib.custom_response import failed_response, success_response + + +class TestVerification: + CREATE_ACCOUNT_API_ROUTE = ACCOUNT_API_ROUTE + VERIFICATION_API_ROUTE = f"{AUTHENTICATION_API_ROUTE}/verification" + + @pytest.mark.asyncio + async def test_verification(self, test_client: TestClient): + test_data = { + "username": "123", + "password": "Test1234", + } + test_client.post(self.CREATE_ACCOUNT_API_ROUTE, json=test_data) + + response = test_client.post(self.VERIFICATION_API_ROUTE, json=test_data) + + expected_response = success_response() + + assert response.status_code == status.HTTP_200_OK + assert response.json() == expected_response + + @pytest.mark.asyncio + async def test_verification_failed_because_username_not_exists( + self, test_client: TestClient + ): + test_data = { + "username": "123", + "password": "Test1234", + } + test_client.post(self.CREATE_ACCOUNT_API_ROUTE, json=test_data) + + failed_data = { + "username": "1234", + "password": "Test1234", + } + response = test_client.post(self.VERIFICATION_API_ROUTE, json=failed_data) + + expected_response = failed_response(reason="Username not exists") + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.json() == expected_response + + @pytest.mark.asyncio + async def test_verification_failed_because_password_not_correct( + self, test_client: TestClient + ): + test_data = { + "username": "123", + "password": "Test1234", + } + test_client.post(self.CREATE_ACCOUNT_API_ROUTE, json=test_data) + + failed_data = { + "username": "123", + "password": "not_correct", + } + response = test_client.post(self.VERIFICATION_API_ROUTE, json=failed_data) + + expected_response = failed_response(reason="Password is not correct") + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.json() == expected_response + + @pytest.mark.asyncio + async def test_verification_failed_because_too_many_attempts( + self, test_client: TestClient + ): + test_data = { + "username": "123", + "password": "Test1234", + } + test_client.post(self.CREATE_ACCOUNT_API_ROUTE, json=test_data) + + failed_data = { + "username": "123", + "password": "not_correct", + } + for _ in range(MAX_FAILED_VERIFICATION_ATTEMPTS + 1): + response = test_client.post(self.VERIFICATION_API_ROUTE, json=failed_data) + + expected_response = failed_response( + reason="Too many failed verification attempts, blocking one minutes" + ) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.json() == expected_response + + @pytest.mark.asyncio + async def test_verification_failed_because_password_not_correct_after_block_time( + self, test_client: TestClient + ): + test_data = { + "username": "123", + "password": "Test1234", + } + test_client.post(self.CREATE_ACCOUNT_API_ROUTE, json=test_data) + + failed_data = { + "username": "123", + "password": "not_correct", + } + initial_datetime = datetime( + year=2024, month=1, day=1, hour=0, minute=0, second=0 + ) + with freeze_time(initial_datetime) as frozen_datetime: + for _ in range(MAX_FAILED_VERIFICATION_ATTEMPTS + 1): + test_client.post(self.VERIFICATION_API_ROUTE, json=failed_data) + + frozen_datetime.move_to( + initial_datetime + BLOCK_VERIFICATION_TIME + timedelta(seconds=1) + ) + response = test_client.post(self.VERIFICATION_API_ROUTE, json=failed_data) + + expected_response = failed_response(reason="Password is not correct") + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.json() == expected_response + + @pytest.mark.asyncio + async def test_verification_after_block_time(self, test_client: TestClient): + test_data = { + "username": "123", + "password": "Test1234", + } + test_client.post(self.CREATE_ACCOUNT_API_ROUTE, json=test_data) + + failed_data = { + "username": "123", + "password": "not_correct", + } + initial_datetime = datetime( + year=2024, month=1, day=1, hour=0, minute=0, second=0 + ) + with freeze_time(initial_datetime) as frozen_datetime: + for _ in range(MAX_FAILED_VERIFICATION_ATTEMPTS + 1): + test_client.post(self.VERIFICATION_API_ROUTE, json=failed_data) + + frozen_datetime.move_to( + initial_datetime + BLOCK_VERIFICATION_TIME + timedelta(seconds=1) + ) + response = test_client.post(self.VERIFICATION_API_ROUTE, json=test_data) + + expected_response = success_response() + + assert response.status_code == status.HTTP_200_OK + assert response.json() == expected_response diff --git a/pyproject.toml b/pyproject.toml index 3b1dc8d..6b27e15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ pytest = "^8.3.2" pre-commit = "^3.8.0" httpx = "^0.27.0" pytest-asyncio = "^0.23.8" +freezegun = "^1.5.1" [build-system] requires = ["poetry-core"]