From 33c4865243e9f95dc58ffcdf3f7200bb8ca511c7 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Wed, 26 Mar 2025 15:28:23 +0000 Subject: [PATCH 01/65] Add password validation tests and update devcontainer to run tests on start --- .devcontainer/devcontainer.json | 2 +- requirements.txt | 1 + tests/hashed_password.py | 46 ++++++++++++++++++++++++++ tests/validate_password.py | 58 +++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 tests/hashed_password.py create mode 100644 tests/validate_password.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e9531e2..c4201c7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,6 +13,6 @@ ] } }, - "postStartCommand": "pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt --break-system-packages", + "postStartCommand": "pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt --break-system-packages && python3 -m pytest tests/*", "remoteUser": "vscode" } diff --git a/requirements.txt b/requirements.txt index cf9054e..1973d5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ flask-cors>=4.0.1 PyMySQL>=1.1.1 requests>=2.32.3 waitress>=3.0.0 +pytest>=8.3.5 python-dotenv>=1.0.1 diff --git a/tests/hashed_password.py b/tests/hashed_password.py new file mode 100644 index 0000000..53842b7 --- /dev/null +++ b/tests/hashed_password.py @@ -0,0 +1,46 @@ +import pytest +from argon2 import PasswordHasher +from argon2.exceptions import VerificationError, VerifyMismatchError + +from utility import hash_password, verify_password + +ph = PasswordHasher() + + +@pytest.fixture +def sample_password(): + return "SecurePass123!" + + +def test_hash_password(sample_password): + """Ensure hash_password returns a hashed password as a non-empty string""" + hashed_password = hash_password(sample_password) + assert isinstance(hashed_password, str) + assert len(hashed_password) > 0 + + +def test_verify_password(sample_password): + """Verify correct password""" + hashed_password = hash_password(sample_password) + assert verify_password(sample_password, hashed_password) is True + + +def test_verify_wrong_password(sample_password): + """Ensure wrong password fails""" + hashed_password = hash_password(sample_password) + with pytest.raises(VerifyMismatchError): + verify_password("WrongPass123!", hashed_password) is False + + +def test_verify_empty_password(): + """Ensure empty password fails""" + hashed_password = hash_password("SomePass123!") + with pytest.raises(VerifyMismatchError): + verify_password("", hashed_password) is False + + +def test_verify_password_tampered(): + hashed_password = hash_password("AnotherPass!123") + tampered_hash = hashed_password[:-5] + "xyz" # Modify the hash slightly + with pytest.raises(VerificationError): + assert verify_password("AnotherPass!123", tampered_hash) diff --git a/tests/validate_password.py b/tests/validate_password.py new file mode 100644 index 0000000..bfd331a --- /dev/null +++ b/tests/validate_password.py @@ -0,0 +1,58 @@ +from utility import validate_password + + +def test_valid_passwords(): + """Verify valid passwords pass""" + assert validate_password("StrongPass1!") is True + assert validate_password("Another$ecure123") is True + assert validate_password("ValidPass99@#") is True + + +def test_too_short_password(): + """Ensure passwords that are too short fail""" + assert validate_password("Short1!") is False + + +def test_missing_uppercase(): + """Ensure passwords missing uppercase letters fail""" + assert validate_password("weakpassword1!") is False + + +def test_missing_lowercase(): + """Ensure passwords missing lowercase letters fail""" + assert validate_password("WEAKPASSWORD1!") is False + + +def test_missing_digit(): + """Ensure passwords missing digits fail""" + assert validate_password("NoDigitPass!") is False + + +def test_missing_special_character(): + """Ensure passwords missing special characters fail""" + assert validate_password("NoSpecial12345") is False + + +def test_only_special_characters(): + """Ensure passwords with only special characters fail""" + assert validate_password("!!!!!!@@@@@@") is False + + +def test_only_numbers(): + """Ensure passwords with only numbers fail""" + assert validate_password("123456789012") is False + + +def test_only_letters(): + """Ensure passwords with only letters fail""" + assert validate_password("JustLettersOnly") is False + + +def test_empty_string(): + """Ensure empty strings fail""" + assert validate_password("") is False + + +def test_whitespace_only(): + """Ensure whitespace-only strings fail""" + assert validate_password(" ") is False From fdd50e6aee5392c4b3962b91b15516da93c2127f Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Wed, 26 Mar 2025 15:31:55 +0000 Subject: [PATCH 02/65] Add test to ensure tampered password fails verification --- tests/hashed_password.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/hashed_password.py b/tests/hashed_password.py index 53842b7..48b5428 100644 --- a/tests/hashed_password.py +++ b/tests/hashed_password.py @@ -40,6 +40,7 @@ def test_verify_empty_password(): def test_verify_password_tampered(): + """Ensure tampered password fails""" hashed_password = hash_password("AnotherPass!123") tampered_hash = hashed_password[:-5] + "xyz" # Modify the hash slightly with pytest.raises(VerificationError): From 635350c2e4dc17892104682e2d7b178112de9310 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Thu, 27 Mar 2025 14:23:56 +0000 Subject: [PATCH 03/65] Nest unit tests and rename `utility.py` -> `utils.py` --- .devcontainer/devcontainer.json | 2 +- app.py | 2 +- routes/authentication.py | 8 +------- routes/comment.py | 2 +- routes/ingredient.py | 2 +- routes/language.py | 2 +- routes/person.py | 7 +------ routes/picture.py | 2 +- routes/recipe.py | 2 +- routes/recipe_engagement.py | 2 +- tests/utility/__init__.py | 0 tests/utility/test_hash_password.py | 18 ++++++++++++++++++ .../test_validate_password.py} | 12 +++++++++++- .../test_verify_password.py} | 9 +-------- utility.py => utils.py | 0 15 files changed, 40 insertions(+), 30 deletions(-) create mode 100644 tests/utility/__init__.py create mode 100644 tests/utility/test_hash_password.py rename tests/{validate_password.py => utility/test_validate_password.py} (90%) rename tests/{hashed_password.py => utility/test_verify_password.py} (79%) rename utility.py => utils.py (100%) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c4201c7..ae557e3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,6 +13,6 @@ ] } }, - "postStartCommand": "pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt --break-system-packages && python3 -m pytest tests/*", + "postStartCommand": "pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt --break-system-packages && python3 -m pytest tests/**/test_*.py", "remoteUser": "vscode" } diff --git a/app.py b/app.py index 66b006c..7a0ae52 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,7 @@ from config import Config, limiter from routes import register_routes -from utility import extract_error_message +from utils import extract_error_message app = Flask(__name__) app.config.from_object(Config) diff --git a/routes/authentication.py b/routes/authentication.py index 60292b9..11e6a94 100644 --- a/routes/authentication.py +++ b/routes/authentication.py @@ -3,7 +3,6 @@ from pymysql import MySQLError from config import limiter - from jwt_helper import ( TokenError, extract_token_from_header, @@ -11,12 +10,7 @@ generate_refresh_token, verify_token, ) -from utility import ( - database_cursor, - hash_password, - validate_password, - verify_password, -) +from utils import database_cursor, hash_password, validate_password, verify_password authentication_blueprint = Blueprint("authentication", __name__) diff --git a/routes/comment.py b/routes/comment.py index aa66e4a..96dd82f 100644 --- a/routes/comment.py +++ b/routes/comment.py @@ -1,6 +1,6 @@ from flask import Blueprint, jsonify -from utility import database_cursor +from utils import database_cursor comment_blueprint = Blueprint("comment", __name__) diff --git a/routes/ingredient.py b/routes/ingredient.py index 2e05747..25be26b 100644 --- a/routes/ingredient.py +++ b/routes/ingredient.py @@ -1,6 +1,6 @@ from flask import Blueprint, jsonify -from utility import database_cursor +from utils import database_cursor ingredient_blueprint = Blueprint("ingredient", __name__) diff --git a/routes/language.py b/routes/language.py index ac71b38..db53ed7 100644 --- a/routes/language.py +++ b/routes/language.py @@ -1,6 +1,6 @@ from flask import Blueprint, jsonify -from utility import database_cursor +from utils import database_cursor language_blueprint = Blueprint("language", __name__) diff --git a/routes/person.py b/routes/person.py index 7273f88..fa7bff7 100644 --- a/routes/person.py +++ b/routes/person.py @@ -1,12 +1,7 @@ from argon2 import exceptions from flask import Blueprint, jsonify, request -from utility import ( - database_cursor, - hash_password, - validate_password, - verify_password, -) +from utils import database_cursor, hash_password, validate_password, verify_password person_blueprint = Blueprint("person", __name__) diff --git a/routes/picture.py b/routes/picture.py index f6a664a..4d89177 100644 --- a/routes/picture.py +++ b/routes/picture.py @@ -4,7 +4,7 @@ from config import PICTURE_FOLDER from jwt_helper import token_required -from utility import database_cursor +from utils import database_cursor picture_blueprint = Blueprint("picture", __name__) diff --git a/routes/recipe.py b/routes/recipe.py index bf6901e..eda359f 100644 --- a/routes/recipe.py +++ b/routes/recipe.py @@ -1,7 +1,7 @@ from flask import Blueprint, jsonify, request from config import DEFAULT_PAGE_SIZE -from utility import database_cursor +from utils import database_cursor recipe_blueprint = Blueprint("recipe", __name__) diff --git a/routes/recipe_engagement.py b/routes/recipe_engagement.py index 2ee9ec0..a44d64c 100644 --- a/routes/recipe_engagement.py +++ b/routes/recipe_engagement.py @@ -1,6 +1,6 @@ from flask import Blueprint, jsonify, request -from utility import database_cursor +from utils import database_cursor recipe_engagement_blueprint = Blueprint("recipe_engagement", __name__) diff --git a/tests/utility/__init__.py b/tests/utility/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utility/test_hash_password.py b/tests/utility/test_hash_password.py new file mode 100644 index 0000000..2d89cf8 --- /dev/null +++ b/tests/utility/test_hash_password.py @@ -0,0 +1,18 @@ +import pytest +from argon2 import PasswordHasher + +from utils import hash_password + +ph = PasswordHasher() + + +@pytest.fixture +def sample_password(): + return "SecurePass123!" + + +def test_hash_password(sample_password): + """Ensure hash_password returns a hashed password as a non-empty string""" + hashed_password = hash_password(sample_password) + assert isinstance(hashed_password, str) + assert len(hashed_password) > 0 diff --git a/tests/validate_password.py b/tests/utility/test_validate_password.py similarity index 90% rename from tests/validate_password.py rename to tests/utility/test_validate_password.py index bfd331a..42b93dc 100644 --- a/tests/validate_password.py +++ b/tests/utility/test_validate_password.py @@ -1,4 +1,14 @@ -from utility import validate_password +import pytest +from argon2 import PasswordHasher + +from utils import validate_password + +ph = PasswordHasher() + + +@pytest.fixture +def sample_password(): + return "SecurePass123!" def test_valid_passwords(): diff --git a/tests/hashed_password.py b/tests/utility/test_verify_password.py similarity index 79% rename from tests/hashed_password.py rename to tests/utility/test_verify_password.py index 48b5428..f336f29 100644 --- a/tests/hashed_password.py +++ b/tests/utility/test_verify_password.py @@ -2,7 +2,7 @@ from argon2 import PasswordHasher from argon2.exceptions import VerificationError, VerifyMismatchError -from utility import hash_password, verify_password +from utils import hash_password, verify_password ph = PasswordHasher() @@ -12,13 +12,6 @@ def sample_password(): return "SecurePass123!" -def test_hash_password(sample_password): - """Ensure hash_password returns a hashed password as a non-empty string""" - hashed_password = hash_password(sample_password) - assert isinstance(hashed_password, str) - assert len(hashed_password) > 0 - - def test_verify_password(sample_password): """Verify correct password""" hashed_password = hash_password(sample_password) diff --git a/utility.py b/utils.py similarity index 100% rename from utility.py rename to utils.py From 3c3e49836066e417419c253a9e94a543a9d0790d Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Thu, 27 Mar 2025 16:08:33 +0000 Subject: [PATCH 04/65] Add initial routes module for organizing route handlers --- tests/routes/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/routes/__init__.py diff --git a/tests/routes/__init__.py b/tests/routes/__init__.py new file mode 100644 index 0000000..e69de29 From 60549674d7fc4dd96bfe431463944e71380d3fbe Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 00:27:44 +0000 Subject: [PATCH 05/65] Add GitHub Actions workflow for testing across multiple Python versions and OS --- .github/workflows/pytest.yml | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/pytest.yml diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..4f7b1d4 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,38 @@ +name: Python package + +on: + push: + null + # branches: + # - main + pull_request: + null + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.10", "3.11", "3.12", "3.13"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + run: pip install -r requirements.txt + run: pip test + + - name: Test with pytest + run: | + pip install pytest pytest-cov + if [[ "${{ matrix.os }}" == "windows-latest" ]]; then + python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html + else + python3 -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html + fi From 903a4c158278c62803f11466c9cfa76ce2712691 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 00:28:58 +0000 Subject: [PATCH 06/65] Refactor GitHub Actions workflow to combine dependency installation and testing steps --- .github/workflows/pytest.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 4f7b1d4..0e0768a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -25,8 +25,11 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: 'pip' - run: pip install -r requirements.txt - run: pip test + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip test - name: Test with pytest run: | From f570a889d65eff5764823a9a57f7d0068ecf4947 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 00:29:41 +0000 Subject: [PATCH 07/65] Remove redundant test command from GitHub Actions workflow --- .github/workflows/pytest.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 0e0768a..822e442 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -29,7 +29,6 @@ jobs: - name: Install dependencies run: | pip install -r requirements.txt - pip test - name: Test with pytest run: | From f9c465235a1565e0c770f225de361bf14138e482 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 00:34:19 +0000 Subject: [PATCH 08/65] Update GitHub Actions workflow to run tests on multiple OS environments --- .github/workflows/pytest.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 822e442..563b46b 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -29,12 +29,13 @@ jobs: - name: Install dependencies run: | pip install -r requirements.txt - - - name: Test with pytest - run: | pip install pytest pytest-cov - if [[ "${{ matrix.os }}" == "windows-latest" ]]; then - python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html - else - python3 -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html - fi + + - matrix.os: ubuntu-latest + run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html + + - matrix.os: macos-latest + run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html + + - matrix.os: windows-latest + run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html From 01dddbb654ccf2ad444704acd67271d54590beb2 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 00:36:08 +0000 Subject: [PATCH 09/65] Refactor GitHub Actions workflow to consolidate OS matrix for testing --- .github/workflows/pytest.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 563b46b..e895366 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -31,10 +31,10 @@ jobs: pip install -r requirements.txt pip install pytest pytest-cov - - matrix.os: ubuntu-latest - run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html - - - matrix.os: macos-latest + - matrix: + os: + - ubuntu-latest + - macos-latest run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html - matrix.os: windows-latest From 9e1e6d9ae2fa729ed8e90e8a1cb5cf01b09075ce Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 00:36:52 +0000 Subject: [PATCH 10/65] Refactor GitHub Actions workflow to specify branch for push events and streamline test job configuration --- .github/workflows/pytest.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index e895366..b495a77 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -2,11 +2,9 @@ name: Python package on: push: - null - # branches: - # - main + branches: + - main pull_request: - null jobs: test: @@ -31,11 +29,10 @@ jobs: pip install -r requirements.txt pip install pytest pytest-cov - - matrix: - os: - - ubuntu-latest - - macos-latest + - name: Run Pytest (Linux/macOS) + if: runner.os != 'Windows' run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html - - matrix.os: windows-latest + - name: Run Pytest (Windows) + if: runner.os == 'Windows' run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html From 5ff6565335011542b91ca66791a18580ef27668f Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 00:37:36 +0000 Subject: [PATCH 11/65] Rename workflow from "Python package" to "Pytest CI" and remove branch specification for push events --- .github/workflows/pytest.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index b495a77..f5a9d28 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,9 +1,7 @@ -name: Python package +name: Pytest CI on: push: - branches: - - main pull_request: jobs: From e7e463d566a3fb70f73ee69cae07a87c4cc7f9db Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 00:39:51 +0000 Subject: [PATCH 12/65] Add Python 3.x to the testing matrix in GitHub Actions workflow --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index f5a9d28..70ec720 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.x"] steps: - name: Checkout code From c14917b5e5e31454f8fe4b5bd6293ae7b9befb17 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 00:40:59 +0000 Subject: [PATCH 13/65] Simplify Python version specification in GitHub Actions workflow to use "3.x" --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 70ec720..2badf1c 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12", "3.13", "3.x"] + python-version: "3.x" steps: - name: Checkout code From 7d99134be78549b0d38897bb59a5e2debb374b4a Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 00:41:29 +0000 Subject: [PATCH 14/65] Update Python version specification to use a list format in GitHub Actions workflow --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 2badf1c..47fddb7 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: "3.x" + python-version: ["3.x"] steps: - name: Checkout code From 4eb078092f2caccf423ed252abcb7a24e1cb8977 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 04:11:30 +0000 Subject: [PATCH 15/65] Add descriptive comment for OS and Python versions in GitHub Actions workflow --- .github/workflows/pytest.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 47fddb7..76b90d3 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -8,6 +8,7 @@ jobs: test: runs-on: ${{ matrix.os }} strategy: + - name: List of OS and Python versions to test for matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.x"] From 3eb3ac2b911fbb1cd25941737cb0be5f0701d528 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 04:13:34 +0000 Subject: [PATCH 16/65] Revert "Add descriptive comment for OS and Python versions in GitHub Actions workflow" This reverts commit 4eb078092f2caccf423ed252abcb7a24e1cb8977. --- .github/workflows/pytest.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 76b90d3..47fddb7 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -8,7 +8,6 @@ jobs: test: runs-on: ${{ matrix.os }} strategy: - - name: List of OS and Python versions to test for matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.x"] From 4170882bdb802ae13a508d698fa7330c407451c0 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 04:14:35 +0000 Subject: [PATCH 17/65] Add branch filter for main in GitHub Actions workflow --- .github/workflows/pytest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 47fddb7..c62938a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -2,6 +2,8 @@ name: Pytest CI on: push: + branches: + - main pull_request: jobs: From 0c874ce44a8e58f344e2b7ca6827e4d925a14d19 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 13:04:37 +0000 Subject: [PATCH 18/65] Refactor imports in person.py and define __all__ in utils.py for better module management --- .github/workflows/pytest.yml | 4 ++-- routes/person.py | 11 +---------- utils.py | 11 +++++++++++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c62938a..a8c69b3 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -2,8 +2,8 @@ name: Pytest CI on: push: - branches: - - main + # branches: + # - main pull_request: jobs: diff --git a/routes/person.py b/routes/person.py index 436362d..c64ecfb 100644 --- a/routes/person.py +++ b/routes/person.py @@ -1,16 +1,7 @@ from argon2 import exceptions from flask import Blueprint, jsonify, request -from utils import ( - database_cursor, - decrypt_email, - encrypt_email, - hash_email, - hash_password, - mask_email, - validate_password, - verify_password, -) +from utils import * person_blueprint = Blueprint("person", __name__) diff --git a/utils.py b/utils.py index b041eb9..9f061f9 100644 --- a/utils.py +++ b/utils.py @@ -12,6 +12,17 @@ from db import get_db_connection +__all__ = [ + "database_cursor", + "decrypt_email", + "encrypt_email", + "hash_email", + "hash_password", + "mask_email", + "validate_password", + "verify_password", +] + load_dotenv() ph = PasswordHasher() AES_KEY = bytes.fromhex(os.getenv("AES_SECRET_KEY", os.urandom(32).hex())) From a1b62cacad6fe512344ebcd4138e36cb42b6da0c Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 13:05:47 +0000 Subject: [PATCH 19/65] Add branch filter for main in Pytest CI workflow --- .github/workflows/pytest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index a8c69b3..c62938a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -2,8 +2,8 @@ name: Pytest CI on: push: - # branches: - # - main + branches: + - main pull_request: jobs: From e9cab749fe48b8a0e43e85189c53db43d02e4dc4 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 14:33:26 +0000 Subject: [PATCH 20/65] Add RunOnSave extension and configure pytest command for test files --- .devcontainer/devcontainer.json | 3 ++- .vscode/settings.json | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ae557e3..dc005c3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,7 +9,8 @@ "extensions": [ "ms-python.black-formatter", "ms-python.python", - "ms-python.isort" + "ms-python.isort", + "emeraldwalk.RunOnSave" ] } }, diff --git a/.vscode/settings.json b/.vscode/settings.json index 2134939..035b40a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,13 @@ "isort.args": [ "--profile", "black" - ] + ], + "emeraldwalk.runonsave": { + "commands": [ + { + "match": "tests/**/test_*.py", + "cmd": "python3 -m pytest '${fileBasename}' -v" + } + ] + } } From 8090e3fbc75a4f0037305be4a9ef1bf6c51765c2 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 14:53:29 +0000 Subject: [PATCH 21/65] Update RunOnSave configuration to improve test file matching and output handling --- .vscode/settings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 035b40a..5424060 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,8 +22,9 @@ "emeraldwalk.runonsave": { "commands": [ { - "match": "tests/**/test_*.py", - "cmd": "python3 -m pytest '${fileBasename}' -v" + "match": "tests[/\\\\](.*[/\\\\])?test_.*\\.py$", + "cmd": "python3 -m pytest '${relativeFile}' -v", + "autoShowOutputPanel": "error" } ] } From 70076e57b0277927eaaeab220a28a09b91526393 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 14:56:54 +0000 Subject: [PATCH 22/65] Add file exclusion settings for __pycache__ and .pytest_cache in VSCode --- .vscode/settings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5424060..ff70529 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,10 @@ "files.insertFinalNewline": true, "files.trimFinalNewlines": true, "files.trimTrailingWhitespace": true, + "files.exclude": { + "**/__pycache__": true, + "**/.pytest_cache": true + }, "[python]": { "editor.rulers": [ 88 From 314b5baf637570587405262134cb716b0444fe2c Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 15:03:10 +0000 Subject: [PATCH 23/65] Refactor utility imports and reorganize test structure --- app.py | 2 +- routes/authentication.py | 2 +- routes/comment.py | 2 +- routes/ingredient.py | 2 +- routes/language.py | 2 +- routes/person.py | 2 +- routes/picture.py | 2 +- routes/recipe.py | 2 +- routes/recipe_engagement.py | 2 +- tests/test_routes/__init__.py | 1 + tests/test_routes/test_home.py | 8 ++++++++ tests/{routes => test_utility}/__init__.py | 0 tests/{utility => test_utility}/test_hash_password.py | 2 +- tests/{utility => test_utility}/test_validate_password.py | 2 +- tests/{utility => test_utility}/test_verify_password.py | 2 +- tests/utility/__init__.py | 0 utils.py => utility.py | 0 17 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 tests/test_routes/__init__.py create mode 100644 tests/test_routes/test_home.py rename tests/{routes => test_utility}/__init__.py (100%) rename tests/{utility => test_utility}/test_hash_password.py (92%) rename tests/{utility => test_utility}/test_validate_password.py (97%) rename tests/{utility => test_utility}/test_verify_password.py (95%) delete mode 100644 tests/utility/__init__.py rename utils.py => utility.py (100%) diff --git a/app.py b/app.py index 8e2d213..c70b575 100644 --- a/app.py +++ b/app.py @@ -6,7 +6,7 @@ from config import Config, limiter from routes import register_routes -from utils import extract_error_message +from utility import extract_error_message app = Flask(__name__) app.config.from_object(Config) diff --git a/routes/authentication.py b/routes/authentication.py index 3165cfe..41171fd 100644 --- a/routes/authentication.py +++ b/routes/authentication.py @@ -10,7 +10,7 @@ generate_refresh_token, verify_token, ) -from utils import ( +from utility import ( database_cursor, encrypt_email, hash_email, diff --git a/routes/comment.py b/routes/comment.py index 96dd82f..aa66e4a 100644 --- a/routes/comment.py +++ b/routes/comment.py @@ -1,6 +1,6 @@ from flask import Blueprint, jsonify -from utils import database_cursor +from utility import database_cursor comment_blueprint = Blueprint("comment", __name__) diff --git a/routes/ingredient.py b/routes/ingredient.py index 25be26b..2e05747 100644 --- a/routes/ingredient.py +++ b/routes/ingredient.py @@ -1,6 +1,6 @@ from flask import Blueprint, jsonify -from utils import database_cursor +from utility import database_cursor ingredient_blueprint = Blueprint("ingredient", __name__) diff --git a/routes/language.py b/routes/language.py index db53ed7..ac71b38 100644 --- a/routes/language.py +++ b/routes/language.py @@ -1,6 +1,6 @@ from flask import Blueprint, jsonify -from utils import database_cursor +from utility import database_cursor language_blueprint = Blueprint("language", __name__) diff --git a/routes/person.py b/routes/person.py index c64ecfb..b1fb9de 100644 --- a/routes/person.py +++ b/routes/person.py @@ -1,7 +1,7 @@ from argon2 import exceptions from flask import Blueprint, jsonify, request -from utils import * +from utility import * person_blueprint = Blueprint("person", __name__) diff --git a/routes/picture.py b/routes/picture.py index 4d89177..f6a664a 100644 --- a/routes/picture.py +++ b/routes/picture.py @@ -4,7 +4,7 @@ from config import PICTURE_FOLDER from jwt_helper import token_required -from utils import database_cursor +from utility import database_cursor picture_blueprint = Blueprint("picture", __name__) diff --git a/routes/recipe.py b/routes/recipe.py index eda359f..bf6901e 100644 --- a/routes/recipe.py +++ b/routes/recipe.py @@ -1,7 +1,7 @@ from flask import Blueprint, jsonify, request from config import DEFAULT_PAGE_SIZE -from utils import database_cursor +from utility import database_cursor recipe_blueprint = Blueprint("recipe", __name__) diff --git a/routes/recipe_engagement.py b/routes/recipe_engagement.py index a44d64c..2ee9ec0 100644 --- a/routes/recipe_engagement.py +++ b/routes/recipe_engagement.py @@ -1,6 +1,6 @@ from flask import Blueprint, jsonify, request -from utils import database_cursor +from utility import database_cursor recipe_engagement_blueprint = Blueprint("recipe_engagement", __name__) diff --git a/tests/test_routes/__init__.py b/tests/test_routes/__init__.py new file mode 100644 index 0000000..5707c75 --- /dev/null +++ b/tests/test_routes/__init__.py @@ -0,0 +1 @@ +from routes import register_routes diff --git a/tests/test_routes/test_home.py b/tests/test_routes/test_home.py new file mode 100644 index 0000000..23150a0 --- /dev/null +++ b/tests/test_routes/test_home.py @@ -0,0 +1,8 @@ +from app import app + + +def test_index_route(): + response = app.test_client().get("/") + + assert response.status_code == 200 + assert response.data.decode("utf-8") != "" diff --git a/tests/routes/__init__.py b/tests/test_utility/__init__.py similarity index 100% rename from tests/routes/__init__.py rename to tests/test_utility/__init__.py diff --git a/tests/utility/test_hash_password.py b/tests/test_utility/test_hash_password.py similarity index 92% rename from tests/utility/test_hash_password.py rename to tests/test_utility/test_hash_password.py index 2d89cf8..13f2a45 100644 --- a/tests/utility/test_hash_password.py +++ b/tests/test_utility/test_hash_password.py @@ -1,7 +1,7 @@ import pytest from argon2 import PasswordHasher -from utils import hash_password +from utility import hash_password ph = PasswordHasher() diff --git a/tests/utility/test_validate_password.py b/tests/test_utility/test_validate_password.py similarity index 97% rename from tests/utility/test_validate_password.py rename to tests/test_utility/test_validate_password.py index 42b93dc..0a69a43 100644 --- a/tests/utility/test_validate_password.py +++ b/tests/test_utility/test_validate_password.py @@ -1,7 +1,7 @@ import pytest from argon2 import PasswordHasher -from utils import validate_password +from utility import validate_password ph = PasswordHasher() diff --git a/tests/utility/test_verify_password.py b/tests/test_utility/test_verify_password.py similarity index 95% rename from tests/utility/test_verify_password.py rename to tests/test_utility/test_verify_password.py index f336f29..9da87fa 100644 --- a/tests/utility/test_verify_password.py +++ b/tests/test_utility/test_verify_password.py @@ -2,7 +2,7 @@ from argon2 import PasswordHasher from argon2.exceptions import VerificationError, VerifyMismatchError -from utils import hash_password, verify_password +from utility import hash_password, verify_password ph = PasswordHasher() diff --git a/tests/utility/__init__.py b/tests/utility/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/utils.py b/utility.py similarity index 100% rename from utils.py rename to utility.py From a2c6af26b5ff536dd31d82446693bb1d4a1ba30c Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 15:22:01 +0000 Subject: [PATCH 24/65] Remove obsolete test for index route in test_home.py --- tests/test_routes/test_home.py | 8 -------- tests/test_routes/test_index.py | 9 +++++++++ 2 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 tests/test_routes/test_home.py create mode 100644 tests/test_routes/test_index.py diff --git a/tests/test_routes/test_home.py b/tests/test_routes/test_home.py deleted file mode 100644 index 23150a0..0000000 --- a/tests/test_routes/test_home.py +++ /dev/null @@ -1,8 +0,0 @@ -from app import app - - -def test_index_route(): - response = app.test_client().get("/") - - assert response.status_code == 200 - assert response.data.decode("utf-8") != "" diff --git a/tests/test_routes/test_index.py b/tests/test_routes/test_index.py new file mode 100644 index 0000000..cd18888 --- /dev/null +++ b/tests/test_routes/test_index.py @@ -0,0 +1,9 @@ +from app import app + + +def test_index_route(): + response = app.test_client().get("/") + expected_response = '{"data":{"message":"Hello there!"},"success":true}' + + assert response.status_code == 200 + assert response.data.decode("utf-8").strip() == expected_response From 971b16448a830a29381453dc614c8482f8843b38 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 15:32:02 +0000 Subject: [PATCH 25/65] Refactor index route test to use a pytest fixture for the test client --- tests/test_routes/test_index.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test_routes/test_index.py b/tests/test_routes/test_index.py index cd18888..d4b81d5 100644 --- a/tests/test_routes/test_index.py +++ b/tests/test_routes/test_index.py @@ -1,8 +1,17 @@ +import pytest + from app import app -def test_index_route(): - response = app.test_client().get("/") +@pytest.fixture +def client(): + """Create a test client for the Flask application""" + with app.test_client() as client: + yield client + + +def test_index_route(client): + response = client.get("/") expected_response = '{"data":{"message":"Hello there!"},"success":true}' assert response.status_code == 200 From 11a6f4892dae13654783e812016950a555570bb7 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 15:57:12 +0000 Subject: [PATCH 26/65] Add tests for email encryption and hashing utilities; refactor password hashing tests --- tests/test_utility/test_decrypt_email.py | 17 ++++++++++ tests/test_utility/test_encrypt_email.py | 40 ++++++++++++++++++++++++ tests/test_utility/test_hash_email.py | 32 +++++++++++++++++++ tests/test_utility/test_hash_password.py | 9 ++++-- 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 tests/test_utility/test_decrypt_email.py create mode 100644 tests/test_utility/test_encrypt_email.py create mode 100644 tests/test_utility/test_hash_email.py diff --git a/tests/test_utility/test_decrypt_email.py b/tests/test_utility/test_decrypt_email.py new file mode 100644 index 0000000..9cc6492 --- /dev/null +++ b/tests/test_utility/test_decrypt_email.py @@ -0,0 +1,17 @@ +import pytest + +from utility import decrypt_email, encrypt_email + + +@pytest.fixture +def sample_email(): + return "sample.email@example.com" + + +def test_decrypt_email(sample_email): + """Ensure decrypt_email returns the original email""" + encrypted_email = encrypt_email(sample_email) + decrypted_email = decrypt_email(encrypted_email) + + assert isinstance(decrypted_email, str) + assert decrypted_email == sample_email diff --git a/tests/test_utility/test_encrypt_email.py b/tests/test_utility/test_encrypt_email.py new file mode 100644 index 0000000..46f93fc --- /dev/null +++ b/tests/test_utility/test_encrypt_email.py @@ -0,0 +1,40 @@ +import pytest + +from utility import encrypt_email + + +@pytest.fixture +def sample_email(): + return "sample.email@example.com" + + +def test_encrypt_email(sample_email): + """Ensure encrypt_email returns an encrypted email""" + encrypted_email = encrypt_email(sample_email) + + assert isinstance(encrypted_email, str) + + +def test_encrypt_email_non_empty(sample_email): + """Ensure encrypt_email returns a non-empty string""" + encrypted_email = encrypt_email(sample_email) + assert len(encrypted_email) > 0 + + +def test_encrypt_email_different(sample_email): + """Ensure encrypt_email returns a different value than the original email""" + encrypted_email = encrypt_email(sample_email) + assert encrypted_email != sample_email + + +def test_encrypt_email_inconsistent(sample_email): + """Ensure encrypt_email returns different values for the same input""" + encrypted_email = encrypt_email(sample_email) + assert encrypted_email != encrypt_email(sample_email) + + +def test_characters_not_in_encrypted_email(sample_email): + """Ensure encrypt_email does not contain certain characters""" + encrypted_email = encrypt_email(sample_email) + assert "@" not in encrypted_email + assert "." not in encrypted_email diff --git a/tests/test_utility/test_hash_email.py b/tests/test_utility/test_hash_email.py new file mode 100644 index 0000000..7ad2bf6 --- /dev/null +++ b/tests/test_utility/test_hash_email.py @@ -0,0 +1,32 @@ +import pytest + +from utility import hash_email + + +@pytest.fixture +def sample_email(): + return "sample.email@example.com" + + +def test_hash_email_type(sample_email): + """Ensure hash_email returns a string""" + hashed_email = hash_email(sample_email) + assert isinstance(hashed_email, str) + + +def test_hash_email_non_empty(sample_email): + """Ensure hash_email returns a non-empty string""" + hashed_email = hash_email(sample_email) + assert len(hashed_email) > 0 + + +def test_hash_email_different(sample_email): + """Ensure hash_email returns a different value than the original email""" + hashed_email = hash_email(sample_email) + assert hashed_email != sample_email + + +def test_hash_email_consistent(sample_email): + """Ensure hash_email returns the same value for the same input""" + hashed_email = hash_email(sample_email) + assert hashed_email == hash_email(sample_email) diff --git a/tests/test_utility/test_hash_password.py b/tests/test_utility/test_hash_password.py index 13f2a45..fc2619c 100644 --- a/tests/test_utility/test_hash_password.py +++ b/tests/test_utility/test_hash_password.py @@ -11,8 +11,13 @@ def sample_password(): return "SecurePass123!" -def test_hash_password(sample_password): - """Ensure hash_password returns a hashed password as a non-empty string""" +def test_hash_password_type(sample_password): + """Ensure hash_password returns a string""" hashed_password = hash_password(sample_password) assert isinstance(hashed_password, str) + + +def test_hash_password_non_empty(sample_password): + """Ensure hash_password returns a non-empty string""" + hashed_password = hash_password(sample_password) assert len(hashed_password) > 0 From a9140e3ff7ba666d5a6352fa966c22ea2bf2ef0e Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 16:00:16 +0000 Subject: [PATCH 27/65] Refactor decrypt_email tests to separate type and value assertions --- tests/test_utility/test_decrypt_email.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_utility/test_decrypt_email.py b/tests/test_utility/test_decrypt_email.py index 9cc6492..3a91d3e 100644 --- a/tests/test_utility/test_decrypt_email.py +++ b/tests/test_utility/test_decrypt_email.py @@ -8,10 +8,15 @@ def sample_email(): return "sample.email@example.com" -def test_decrypt_email(sample_email): - """Ensure decrypt_email returns the original email""" +def test_decrypt_email_type(sample_email): + """Ensure decrypt_email returns a string""" encrypted_email = encrypt_email(sample_email) decrypted_email = decrypt_email(encrypted_email) - assert isinstance(decrypted_email, str) + + +def test_decrypt_email_value(sample_email): + """Ensure decrypt_email returns the original email""" + encrypted_email = encrypt_email(sample_email) + decrypted_email = decrypt_email(encrypted_email) assert decrypted_email == sample_email From d2c6b1556b604c4a63f2a94a8403aa5f21149bb5 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 16:21:03 +0000 Subject: [PATCH 28/65] Remove unused import of register_routes in test_routes initialization --- tests/test_routes/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_routes/__init__.py b/tests/test_routes/__init__.py index 5707c75..e69de29 100644 --- a/tests/test_routes/__init__.py +++ b/tests/test_routes/__init__.py @@ -1 +0,0 @@ -from routes import register_routes From 0b04e7cc5e34db87d56d58538203131617a6d70e Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 16:27:47 +0000 Subject: [PATCH 29/65] Refactor index route test to separate status code and JSON response assertions --- tests/test_routes/test_index.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/test_routes/test_index.py b/tests/test_routes/test_index.py index d4b81d5..49f26f5 100644 --- a/tests/test_routes/test_index.py +++ b/tests/test_routes/test_index.py @@ -10,9 +10,16 @@ def client(): yield client -def test_index_route(client): +def test_index_route_status_code(client): + """Ensure the index route returns a 200 status code""" response = client.get("/") - expected_response = '{"data":{"message":"Hello there!"},"success":true}' - assert response.status_code == 200 - assert response.data.decode("utf-8").strip() == expected_response + + +def test_index_route_json(client): + """Ensure the index route returns the correct JSON response""" + response = client.get("/") + expected_response = {"data": {"message": "Hello there!"}, "success": True} + + assert response.is_json + assert response.get_json() == expected_response From f9bc814efbc619307e451db2549f610d0b542420 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 16:29:38 +0000 Subject: [PATCH 30/65] Update image upload folder path and ensure directory creation for new location --- config.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/config.py b/config.py index cb41512..f7f6e80 100644 --- a/config.py +++ b/config.py @@ -23,22 +23,16 @@ class Config: JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30) # Image upload folder - IMAGES_FOLDER = os.path.join(os.getcwd(), "images") + IMAGES_FOLDER = os.path.join(os.getcwd(), "pictures") MAX_CONTENT_LENGTH = 1024 * 1024 # 1 MB -# Image upload folder -PICTURE_FOLDER = os.path.join(os.getcwd(), "picture") - -if not os.path.exists(PICTURE_FOLDER): - os.makedirs(PICTURE_FOLDER) +if not os.path.exists(Config.IMAGES_FOLDER): + os.makedirs(Config.IMAGES_FOLDER) # Pagination configuration DEFAULT_PAGE_SIZE = 10 -if not os.path.exists(Config.IMAGES_FOLDER): - os.makedirs(Config.IMAGES_FOLDER) - limiter = Limiter( key_func=get_remote_address, default_limits=["1000 per day", "200 per hour", "30 per minute", "3 per second"], From 7c7e9c7014a097502fe84313edfcad8f3e09ba93 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 16:31:17 +0000 Subject: [PATCH 31/65] Update picture route to use Config.IMAGES_FOLDER for file handling --- routes/picture.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/routes/picture.py b/routes/picture.py index f6a664a..b5a9ef0 100644 --- a/routes/picture.py +++ b/routes/picture.py @@ -2,7 +2,7 @@ from flask import Blueprint, jsonify, request, send_from_directory -from config import PICTURE_FOLDER +from config import Config from jwt_helper import token_required from utility import database_cursor @@ -50,7 +50,7 @@ def get_pictures_by_author(author_id): @picture_blueprint.route("/", methods=["GET"]) def get_picture(filename): - return send_from_directory(PICTURE_FOLDER, filename) + return send_from_directory(Config.IMAGES_FOLDER, filename) @picture_blueprint.route("", methods=["POST"]) @@ -80,8 +80,8 @@ def upload_picture(): with database_cursor() as cursor: cursor.callproc(procedure, (hexname, request.person_id)) - fullpath = os.path.normpath(os.path.join(PICTURE_FOLDER, hexname)) - if not fullpath.startswith(PICTURE_FOLDER): + fullpath = os.path.normpath(os.path.join(Config.IMAGES_FOLDER, hexname)) + if not fullpath.startswith(Config.IMAGES_FOLDER): return jsonify({"error": "Invalid file path"}), 400 file.save(fullpath) From 938a1e38f36fe096fc26ad110cfe42d299417dc0 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 16:39:20 +0000 Subject: [PATCH 32/65] Rename upload folder from 'images' to 'pictures' in .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6d04e70..51db7d6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.pem # Upload folder -images/ +pictures/ # Environment variables file .env From 5f95bcde0cb0f9b4fb2552a370da68b83472512e Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 16:52:28 +0000 Subject: [PATCH 33/65] Add tests for user registration with missing fields --- .../test_authentication/test_register.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/test_routes/test_authentication/test_register.py diff --git a/tests/test_routes/test_authentication/test_register.py b/tests/test_routes/test_authentication/test_register.py new file mode 100644 index 0000000..253d20b --- /dev/null +++ b/tests/test_routes/test_authentication/test_register.py @@ -0,0 +1,33 @@ +import pytest +from flask import Flask +from flask.testing import FlaskClient + +from routes.authentication import authentication_blueprint + + +@pytest.fixture +def app(): + """Create and configure a new Flask application instance for testing""" + app = Flask(__name__) + app.register_blueprint(authentication_blueprint) + return app + + +@pytest.fixture +def client(app: Flask): + """Create a test client for the Flask application""" + with app.test_client() as client: + yield client + + +def test_missing_fields(client: FlaskClient): + """Test registration with missing fields""" + data = { + "username": "newuser", + "email": "newuser@example.com", + "password": "", # Missing password + } + response = client.post("/register", json=data) + + assert response.status_code == 400 + assert response.json["message"] == "Username, email, and password are required" From 0d6f324020d3d07d402baac1602c071c53b00e86 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 16:57:49 +0000 Subject: [PATCH 34/65] Update postStartCommand in devcontainer.json for verbose pytest output --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dc005c3..2e5bf05 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,6 +14,6 @@ ] } }, - "postStartCommand": "pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt --break-system-packages && python3 -m pytest tests/**/test_*.py", + "postStartCommand": "pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt --break-system-packages && python3 -m pytest tests -v", "remoteUser": "vscode" } From 5bbe881eb047cb826eba468aba4230d88ad4cdaa Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 17:10:18 +0000 Subject: [PATCH 35/65] Add email validation to registration and update utility functions --- routes/authentication.py | 4 +++ routes/person.py | 11 ++++++- .../test_authentication/test_register.py | 13 ++++++++ utility.py | 30 +++++++++---------- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/routes/authentication.py b/routes/authentication.py index 41171fd..0c403bb 100644 --- a/routes/authentication.py +++ b/routes/authentication.py @@ -15,6 +15,7 @@ encrypt_email, hash_email, hash_password, + validate_email, validate_password, verify_password, ) @@ -47,6 +48,9 @@ def register(): if not name or not email or not password: return jsonify(message="Username, email, and password are required"), 400 + if not validate_email(email): + return jsonify(message="Invalid email address"), 400 + if not validate_password(password): return jsonify(message="Password does not meet security requirements"), 400 diff --git a/routes/person.py b/routes/person.py index b1fb9de..9b2f658 100644 --- a/routes/person.py +++ b/routes/person.py @@ -1,7 +1,16 @@ from argon2 import exceptions from flask import Blueprint, jsonify, request -from utility import * +from utility import ( + database_cursor, + decrypt_email, + encrypt_email, + hash_email, + hash_password, + mask_email, + validate_password, + verify_password, +) person_blueprint = Blueprint("person", __name__) diff --git a/tests/test_routes/test_authentication/test_register.py b/tests/test_routes/test_authentication/test_register.py index 253d20b..fafb60b 100644 --- a/tests/test_routes/test_authentication/test_register.py +++ b/tests/test_routes/test_authentication/test_register.py @@ -31,3 +31,16 @@ def test_missing_fields(client: FlaskClient): assert response.status_code == 400 assert response.json["message"] == "Username, email, and password are required" + + +def test_invalid_email(client: FlaskClient): + """Test registration with an invalid email format""" + data = { + "username": "newuser", + "email": "invalid-email", # Invalid email format + "password": "Passw0rd123!", + } + response = client.post("/register", json=data) + + assert response.status_code == 400 + assert response.json["message"] == "Invalid email address" diff --git a/utility.py b/utility.py index 9f061f9..d6cc8aa 100644 --- a/utility.py +++ b/utility.py @@ -12,17 +12,6 @@ from db import get_db_connection -__all__ = [ - "database_cursor", - "decrypt_email", - "encrypt_email", - "hash_email", - "hash_password", - "mask_email", - "validate_password", - "verify_password", -] - load_dotenv() ph = PasswordHasher() AES_KEY = bytes.fromhex(os.getenv("AES_SECRET_KEY", os.urandom(32).hex())) @@ -107,7 +96,19 @@ def mask_email(email: str) -> str: return f"{local_part}@{domain_name}.{domain_extension}" -def validate_password(password): +def validate_email(email: str) -> bool: + """ + Validates an email address using a regex pattern. + The pattern checks for: + - A valid local part (alphanumeric, '.', '_', '+', '-') + - A valid domain part (alphanumeric, '-', and '.' for subdomains) + - A valid top-level domain (2 or more alphabetic characters) + """ + pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" + return bool(match(pattern, email)) + + +def validate_password(password) -> bool: """ Validates a password based on the following criteria: - At least 12 characters long. @@ -116,9 +117,8 @@ def validate_password(password): - Contains at least one digit (0-9). - Contains at least one special character (any non-alphanumeric character). """ - return bool( - match(r"^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}$", password) - ) + pattern = r"^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}$" + return bool(match(pattern, password)) def verify_password(password, stored_password): From 01d525e29f8539033c7d9679bd777b8722aad965 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 17:20:53 +0000 Subject: [PATCH 36/65] Add tests for user registration and login with missing fields --- .../test_authentication/test_login.py | 44 +++++++++++++++++++ .../test_authentication/test_register.py | 30 ++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 tests/test_routes/test_authentication/test_login.py diff --git a/tests/test_routes/test_authentication/test_login.py b/tests/test_routes/test_authentication/test_login.py new file mode 100644 index 0000000..82750cd --- /dev/null +++ b/tests/test_routes/test_authentication/test_login.py @@ -0,0 +1,44 @@ +import pytest +from flask import Flask +from flask.testing import FlaskClient + +from routes.authentication import authentication_blueprint + + +@pytest.fixture +def app(): + """Create and configure a new Flask application instance for testing""" + app = Flask(__name__) + app.register_blueprint(authentication_blueprint) + return app + + +@pytest.fixture +def client(app: Flask): + """Create a test client for the Flask application""" + with app.test_client() as client: + yield client + + +def test_missing_username(client: FlaskClient): + """Test login with missing username""" + data = { + "username": "", # Missing username + "password": "Passw0rd123!", + } + response = client.post("/login", json=data) + + assert response.status_code == 400 + assert response.json["message"] == "Email and password are required" + + +def test_missing_password(client: FlaskClient): + """Test login with missing password""" + data = { + "username": "existinguser", + "password": "", # Missing password + } + response = client.post("/login", json=data) + + assert response.status_code == 400 + assert response.json["message"] == "Email and password are required" diff --git a/tests/test_routes/test_authentication/test_register.py b/tests/test_routes/test_authentication/test_register.py index fafb60b..edf8762 100644 --- a/tests/test_routes/test_authentication/test_register.py +++ b/tests/test_routes/test_authentication/test_register.py @@ -20,8 +20,34 @@ def client(app: Flask): yield client -def test_missing_fields(client: FlaskClient): - """Test registration with missing fields""" +def test_missing_username(client: FlaskClient): + """Test registration with missing username""" + data = { + "username": "", # Missing username + "email": "newuser@example.com", + "password": "", # Missing password + } + response = client.post("/register", json=data) + + assert response.status_code == 400 + assert response.json["message"] == "Username, email, and password are required" + + +def test_missing_email(client: FlaskClient): + """Test registration with missing email""" + data = { + "username": "newuser", + "email": "", # Missing email + "password": "Passw0rd123!", + } + response = client.post("/register", json=data) + + assert response.status_code == 400 + assert response.json["message"] == "Username, email, and password are required" + + +def test_missing_password(client: FlaskClient): + """Test registration with missing password""" data = { "username": "newuser", "email": "newuser@example.com", From d2cad3c063f5d458b66227465de799ada281a223 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 17:29:19 +0000 Subject: [PATCH 37/65] Fix email validation regex and add comprehensive tests for valid and invalid email formats --- tests/test_utility/test_validate_email.py | 26 +++++++++++++++++++++++ utility.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/test_utility/test_validate_email.py diff --git a/tests/test_utility/test_validate_email.py b/tests/test_utility/test_validate_email.py new file mode 100644 index 0000000..1be88aa --- /dev/null +++ b/tests/test_utility/test_validate_email.py @@ -0,0 +1,26 @@ +import pytest + +from utility import validate_email + + +@pytest.fixture +def sample_email(): + return "sample.email@example.com" + + +def test_valid_emails(): + """Verify valid emails pass""" + assert validate_email("good.email@example.com") is True + assert validate_email("good.with+subdomain@example.com") is True + assert validate_email("goodemail@mockda.ta") + + +def test_invalid_emails(): + """Ensure invalid emails fail""" + assert validate_email("plainaddress") is False + assert validate_email("@missingusername.com") is False + assert validate_email("username@.com") is False + assert validate_email("username@domain..com") is False + assert validate_email("username@domain.c") is False + assert validate_email("username@domain.c@om") is False + assert validate_email("username@domain.c#om") is False diff --git a/utility.py b/utility.py index d6cc8aa..dd2895e 100644 --- a/utility.py +++ b/utility.py @@ -104,7 +104,7 @@ def validate_email(email: str) -> bool: - A valid domain part (alphanumeric, '-', and '.' for subdomains) - A valid top-level domain (2 or more alphabetic characters) """ - pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" + pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$" return bool(match(pattern, email)) From 45d5b932adc46371c1197504b8d241973507f298 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 17:31:30 +0000 Subject: [PATCH 38/65] Add additional tests for email validation to cover edge cases --- tests/test_utility/test_validate_email.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_utility/test_validate_email.py b/tests/test_utility/test_validate_email.py index 1be88aa..eb07995 100644 --- a/tests/test_utility/test_validate_email.py +++ b/tests/test_utility/test_validate_email.py @@ -18,9 +18,18 @@ def test_valid_emails(): def test_invalid_emails(): """Ensure invalid emails fail""" assert validate_email("plainaddress") is False + assert validate_email("@") is False + assert validate_email("@.com") is False assert validate_email("@missingusername.com") is False assert validate_email("username@.com") is False assert validate_email("username@domain..com") is False assert validate_email("username@domain.c") is False assert validate_email("username@domain.c@om") is False assert validate_email("username@domain.c#om") is False + + +def test_empty_email(): + """Ensure empty email fails""" + assert validate_email("") is False + assert validate_email(" ") is False + assert validate_email(" ") is False From f2e59891d47b1bd95e1f257557e8395b8fbce154 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 18:13:09 +0000 Subject: [PATCH 39/65] Add docstring to extract_error_message for clarity on functionality --- utility.py | 1 + 1 file changed, 1 insertion(+) diff --git a/utility.py b/utility.py index dd2895e..14bb689 100644 --- a/utility.py +++ b/utility.py @@ -60,6 +60,7 @@ def encrypt_email(email: str) -> str: def extract_error_message(message): + """Extracts a user-friendly error message from a database error message.""" try: cleaner_message = message.split(", ")[1].strip("()'") return cleaner_message if "SQL" not in cleaner_message else "Database error" From 6c3db4f880dc340178b07deeb7f0e73444dc0d86 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 18:14:56 +0000 Subject: [PATCH 40/65] Update email validation regex and add git fetch command in VSCode settings --- .vscode/settings.json | 5 +++++ utility.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ff70529..065ce45 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,6 +25,11 @@ ], "emeraldwalk.runonsave": { "commands": [ + { + "match": ".*", + "cmd": "git fetch --prune", + "autoShowOutputPanel": "error" + }, { "match": "tests[/\\\\](.*[/\\\\])?test_.*\\.py$", "cmd": "python3 -m pytest '${relativeFile}' -v", diff --git a/utility.py b/utility.py index 14bb689..e830731 100644 --- a/utility.py +++ b/utility.py @@ -105,7 +105,7 @@ def validate_email(email: str) -> bool: - A valid domain part (alphanumeric, '-', and '.' for subdomains) - A valid top-level domain (2 or more alphabetic characters) """ - pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$" + pattern = r"^[a-zA-Z0-9._+-]+@[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$" return bool(match(pattern, email)) From d34cc5c84399698b76d025914474cf69d7ebaa8a Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 28 Mar 2025 19:49:49 +0000 Subject: [PATCH 41/65] Add email masking functionality and corresponding tests --- tests/test_utility/test_mask_email.py | 18 +++++++++++++ utility.py | 38 +++++++++++---------------- 2 files changed, 34 insertions(+), 22 deletions(-) create mode 100644 tests/test_utility/test_mask_email.py diff --git a/tests/test_utility/test_mask_email.py b/tests/test_utility/test_mask_email.py new file mode 100644 index 0000000..67b9f18 --- /dev/null +++ b/tests/test_utility/test_mask_email.py @@ -0,0 +1,18 @@ +import pytest + +from utility import mask_email + + +@pytest.mark.parametrize( + "input_email, expected_output", + [ + ("john.doe@example.com", "j**n.d*e@e*****e.c*m"), + ("a@b.com", "a@b.c*m"), # Short local and domain names remain unchanged + ("alice.smith@domain.org", "a***e.s***h@d****n.o*g"), + ("test1234@gmail.com", "t******4@g***l.c*m"), + ("user.name@sub.example.co.uk", "u**r.n**e@s*b.e*****e.co.uk"), + ("m@xyz.net", "m@x*z.n*t"), # Masked single-character domain name + ], +) +def test_mask_email_valid(input_email, expected_output): + assert mask_email(input_email) == expected_output diff --git a/utility.py b/utility.py index e830731..9bfc3f0 100644 --- a/utility.py +++ b/utility.py @@ -79,34 +79,28 @@ def hash_password(password: str) -> tuple[str, bytes]: def mask_email(email: str) -> str: - """Masks the email address to protect user privacy.""" - match = re.match(r"^([\w.+-]+)@([\w-]+)\.([a-zA-Z]{2,})$", email) - if not match: - raise ValueError("Invalid email format") + local, domain = email.split("@") + domain_parts = domain.split(".") - local_part, domain_name, domain_extension = match.groups() + def mask_part(part: str) -> str: + if len(part) <= 2: + return part + return part[0] + "*" * (len(part) - 2) + part[-1] - # Mask the local part - if len(local_part) > 2: - local_part = local_part[0] + "*" * (len(local_part) - 2) + local_part[-1] + masked_local = re.sub( + r"(\w)(\w+)(\w)", + lambda m: m.group(1) + "*" * len(m.group(2)) + m.group(3), + local, + ) + masked_domain = ".".join(mask_part(part) for part in domain_parts) - # Mask the domain name - if len(domain_name) > 2: - domain_name = domain_name[0] + "*" * (len(domain_name) - 2) + domain_name[-1] - - return f"{local_part}@{domain_name}.{domain_extension}" + return masked_local + "@" + masked_domain def validate_email(email: str) -> bool: - """ - Validates an email address using a regex pattern. - The pattern checks for: - - A valid local part (alphanumeric, '.', '_', '+', '-') - - A valid domain part (alphanumeric, '-', and '.' for subdomains) - - A valid top-level domain (2 or more alphabetic characters) - """ - pattern = r"^[a-zA-Z0-9._+-]+@[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$" - return bool(match(pattern, email)) + """Validates an email address using a regex pattern.""" + pattern = r"^[a-zA-Z0-9._+-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$" + return bool(re.match(pattern, email)) def validate_password(password) -> bool: From 9f01c888cf2df985b5fcc966998265d2b76bc880 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sun, 30 Mar 2025 16:04:27 +0000 Subject: [PATCH 42/65] Refactor authentication tests to use fixtures for sample data and improve test clarity --- .../test_authentication/test_login.py | 23 +++++++-- .../test_authentication/test_register.py | 49 ++++++++++++------- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/tests/test_routes/test_authentication/test_login.py b/tests/test_routes/test_authentication/test_login.py index 82750cd..18e10e6 100644 --- a/tests/test_routes/test_authentication/test_login.py +++ b/tests/test_routes/test_authentication/test_login.py @@ -20,11 +20,26 @@ def client(app: Flask): yield client -def test_missing_username(client: FlaskClient): +@pytest.fixture +def sample_email(): + return "sample.email@example.com" + + +@pytest.fixture +def sample_password(): + return "SecurePass123!" + + +@pytest.fixture +def sample_username(): + return "sampleuser" + + +def test_missing_username(client: FlaskClient, sample_password): """Test login with missing username""" data = { "username": "", # Missing username - "password": "Passw0rd123!", + "password": sample_password, } response = client.post("/login", json=data) @@ -32,10 +47,10 @@ def test_missing_username(client: FlaskClient): assert response.json["message"] == "Email and password are required" -def test_missing_password(client: FlaskClient): +def test_missing_password(client: FlaskClient, sample_username): """Test login with missing password""" data = { - "username": "existinguser", + "username": sample_username, "password": "", # Missing password } response = client.post("/login", json=data) diff --git a/tests/test_routes/test_authentication/test_register.py b/tests/test_routes/test_authentication/test_register.py index edf8762..a6640f2 100644 --- a/tests/test_routes/test_authentication/test_register.py +++ b/tests/test_routes/test_authentication/test_register.py @@ -20,12 +20,27 @@ def client(app: Flask): yield client -def test_missing_username(client: FlaskClient): - """Test registration with missing username""" +@pytest.fixture +def sample_email(): + return "sample.email@example.com" + + +@pytest.fixture +def sample_password(): + return "SecurePass123!" + + +@pytest.fixture +def sample_username(): + return "sampleuser" + + +def test_missing_email(client: FlaskClient, sample_username, sample_password): + """Test registration with missing email""" data = { - "username": "", # Missing username - "email": "newuser@example.com", - "password": "", # Missing password + "username": sample_username, + "email": "", # Missing email + "password": sample_password, } response = client.post("/register", json=data) @@ -33,12 +48,12 @@ def test_missing_username(client: FlaskClient): assert response.json["message"] == "Username, email, and password are required" -def test_missing_email(client: FlaskClient): - """Test registration with missing email""" +def test_missing_password(client: FlaskClient, sample_username, sample_email): + """Test registration with missing password""" data = { - "username": "newuser", - "email": "", # Missing email - "password": "Passw0rd123!", + "username": sample_username, + "email": sample_email, + "password": "", # Missing password } response = client.post("/register", json=data) @@ -46,11 +61,11 @@ def test_missing_email(client: FlaskClient): assert response.json["message"] == "Username, email, and password are required" -def test_missing_password(client: FlaskClient): - """Test registration with missing password""" +def test_missing_username(client: FlaskClient, sample_username, sample_email): + """Test registration with missing username""" data = { - "username": "newuser", - "email": "newuser@example.com", + "username": sample_username, # Missing username + "email": sample_email, "password": "", # Missing password } response = client.post("/register", json=data) @@ -59,12 +74,12 @@ def test_missing_password(client: FlaskClient): assert response.json["message"] == "Username, email, and password are required" -def test_invalid_email(client: FlaskClient): +def test_invalid_email(client: FlaskClient, sample_username, sample_password): """Test registration with an invalid email format""" data = { - "username": "newuser", + "username": sample_username, "email": "invalid-email", # Invalid email format - "password": "Passw0rd123!", + "password": sample_password, } response = client.post("/register", json=data) From 1aad2eabbc50db83f4d930101aa0e8bdad8782ac Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sun, 30 Mar 2025 16:14:50 +0000 Subject: [PATCH 43/65] Remove git fetch command from VSCode settings to streamline run-on-save functionality --- .vscode/settings.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 065ce45..ff70529 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,11 +25,6 @@ ], "emeraldwalk.runonsave": { "commands": [ - { - "match": ".*", - "cmd": "git fetch --prune", - "autoShowOutputPanel": "error" - }, { "match": "tests[/\\\\](.*[/\\\\])?test_.*\\.py$", "cmd": "python3 -m pytest '${relativeFile}' -v", From 5e631776969b3ba371119f25c84ea01d53beb781 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sun, 30 Mar 2025 18:01:15 +0000 Subject: [PATCH 44/65] Remove verbosity from postStartCommand in devcontainer configuration --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2e5bf05..f36c256 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,6 +14,6 @@ ] } }, - "postStartCommand": "pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt --break-system-packages && python3 -m pytest tests -v", + "postStartCommand": "pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt --break-system-packages && python3 -m pytest tests", "remoteUser": "vscode" } From 3aa80a509921c9851ef7104da90e148e5da28fad Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 14:34:10 +0000 Subject: [PATCH 45/65] Refactor JWT helper functions and add comprehensive tests for refresh token functionality --- jwt_helper.py | 24 ++--- .../test_authentication/test_refresh_token.py | 88 +++++++++++++++++++ 2 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 tests/test_routes/test_authentication/test_refresh_token.py diff --git a/jwt_helper.py b/jwt_helper.py index 9d5705f..f9f4d9e 100644 --- a/jwt_helper.py +++ b/jwt_helper.py @@ -11,9 +11,7 @@ class TokenError(Exception): - """ - Custom exception for token-related errors. - """ + """Custom exception for token-related errors.""" def __init__(self, message, status_code): super().__init__(message) @@ -22,9 +20,7 @@ def __init__(self, message, status_code): def generate_access_token(person_id: int) -> str: - """ - Generate a short-lived JWT access token for a user. - """ + """Generate a short-lived JWT access token for a user.""" payload = { "person_id": person_id, "exp": datetime.now(timezone.utc) + JWT_ACCESS_TOKEN_EXPIRY, # Expiration @@ -35,9 +31,7 @@ def generate_access_token(person_id: int) -> str: def generate_refresh_token(person_id: int) -> str: - """ - Generate a long-lived refresh token for a user. - """ + """Generate a long-lived refresh token for a user.""" payload = { "person_id": person_id, "exp": datetime.now(timezone.utc) + JWT_REFRESH_TOKEN_EXPIRY, @@ -48,9 +42,7 @@ def generate_refresh_token(person_id: int) -> str: def extract_token_from_header() -> str: - """ - Extract the Bearer token from the Authorization header. - """ + """Extract the Bearer token from the Authorization header.""" auth_header = request.headers.get("Authorization") if not auth_header or not auth_header.startswith("Bearer "): raise TokenError("Token is missing or improperly formatted", 401) @@ -58,9 +50,7 @@ def extract_token_from_header() -> str: def verify_token(token: str, required_type: str) -> dict: - """ - Verify and decode a JWT token. - """ + """Verify and decode a JWT token.""" try: decoded = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"]) if decoded.get("token_type") != required_type: @@ -73,9 +63,7 @@ def verify_token(token: str, required_type: str) -> dict: def token_required(f): - """ - Decorator to protect routes by requiring a valid token. - """ + """Decorator to protect routes by requiring a valid token.""" @wraps(f) def decorated(*args, **kwargs): diff --git a/tests/test_routes/test_authentication/test_refresh_token.py b/tests/test_routes/test_authentication/test_refresh_token.py new file mode 100644 index 0000000..4241b30 --- /dev/null +++ b/tests/test_routes/test_authentication/test_refresh_token.py @@ -0,0 +1,88 @@ +import datetime + +import jwt +import pytest +from flask import Flask +from flask.testing import FlaskClient + +from jwt_helper import JWT_SECRET_KEY, generate_access_token, generate_refresh_token +from routes.authentication import authentication_blueprint + + +@pytest.fixture +def app(): + """Create and configure a new Flask application instance for testing""" + app = Flask(__name__) + app.register_blueprint(authentication_blueprint) + app.config["TESTING"] = True + return app + + +@pytest.fixture +def client(app: Flask): + """Create a test client for the Flask application""" + with app.test_client() as client: + yield client + + +@pytest.fixture +def sample_person_id() -> int: + """Provide a sample person ID for testing""" + return 12345 + + +@pytest.fixture +def sample_refresh_token(sample_person_id: int) -> str: + """Provide a sample refresh token for testing""" + return generate_refresh_token(sample_person_id) + + +@pytest.fixture +def sample_expired_token(sample_person_id) -> str: + """Generate a deliberately expired JWT refresh token.""" + payload = { + "person_id": sample_person_id, + "exp": datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(seconds=1), # Already expired + "iat": datetime.datetime.now(datetime.timezone.utc) + - datetime.timedelta(hours=1), + "token_type": "refresh", + } + return jwt.encode(payload, JWT_SECRET_KEY, algorithm="HS256") + + +def test_refresh_token_success(client, sample_refresh_token, sample_person_id): + """Test the refresh token endpoint with a valid token""" + headers = {"Authorization": f"Bearer {sample_refresh_token}"} + response = client.post("/refresh", headers=headers) + + assert response.status_code == 200 + assert "access_token" in response.json + assert response.json["access_token"] == generate_access_token(sample_person_id) + + +def test_refresh_token_invalid_token(client: FlaskClient): + """Test the refresh token endpoint with an invalid token""" + headers = {"Authorization": "Bearer invalid_token"} + response = client.post("/refresh", headers=headers) + + assert response.status_code == 401 + assert response.json == {"message": "Invalid token"} + + +def test_refresh_token_missing_token(client: FlaskClient): + """Test the refresh token endpoint with a missing token""" + response = client.post("/refresh") + + assert response.status_code == 401 + assert response.json == {"message": "Token is missing or improperly formatted"} + + +def test_refresh_token_expired_token(client: FlaskClient, sample_expired_token): + """Test the refresh token endpoint with an expired token""" + headers = {"Authorization": f"Bearer {sample_expired_token}"} + + response = client.post("/refresh", headers=headers) + + assert response.status_code == 401 + assert response.json == {"message": "Token has expired"} From ff46e790ad2a18a9ae09f40564230715996233a0 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 14:46:41 +0000 Subject: [PATCH 46/65] Update GitHub Actions workflow to define permissions for test job --- .github/workflows/pytest.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c62938a..6c3fcd3 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -6,9 +6,17 @@ on: - main pull_request: +permissions: {} + jobs: test: runs-on: ${{ matrix.os }} + + permissions: + contents: read + packages: read + statuses: write + strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] From 80a7388c532facda59395cf3bd96ec56ac5dffe6 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 10:50:02 -0400 Subject: [PATCH 47/65] Run Prettier --- .github/workflows/pytest.yml | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 6c3fcd3..17ca5a8 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -23,24 +23,24 @@ jobs: python-version: ["3.x"] steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - - - name: Install dependencies - run: | - pip install -r requirements.txt - pip install pytest pytest-cov - - - name: Run Pytest (Linux/macOS) - if: runner.os != 'Windows' - run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html - - - name: Run Pytest (Windows) - if: runner.os == 'Windows' - run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install pytest pytest-cov + + - name: Run Pytest (Linux/macOS) + if: runner.os != 'Windows' + run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html + + - name: Run Pytest (Windows) + if: runner.os == 'Windows' + run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html From cfe5f792206d6f0ce08eb04092fa0bd97d3755eb Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 15:02:24 +0000 Subject: [PATCH 48/65] Add utility tests for email and password handling, including hashing and masking --- tests/test_routes/conftest.py | 9 +++++ .../test_authentication/conftest.py | 27 +++++++++++++++ .../test_authentication/test_login.py | 34 ------------------- .../test_authentication/test_refresh_token.py | 16 --------- .../test_authentication/test_register.py | 34 ------------------- tests/test_utility/test_email/__init__.py | 0 tests/test_utility/test_email/conftest.py | 6 ++++ .../{ => test_email}/test_decrypt_email.py | 7 ---- .../{ => test_email}/test_encrypt_email.py | 7 ---- .../{ => test_email}/test_hash_email.py | 7 ---- .../{ => test_email}/test_mask_email.py | 0 .../{ => test_email}/test_validate_email.py | 7 ---- tests/test_utility/test_password/__init__.py | 0 tests/test_utility/test_password/conftest.py | 6 ++++ .../{ => test_password}/test_hash_password.py | 6 ---- .../test_validate_password.py | 6 ---- .../test_verify_password.py | 5 --- 17 files changed, 48 insertions(+), 129 deletions(-) create mode 100644 tests/test_routes/conftest.py create mode 100644 tests/test_routes/test_authentication/conftest.py create mode 100644 tests/test_utility/test_email/__init__.py create mode 100644 tests/test_utility/test_email/conftest.py rename tests/test_utility/{ => test_email}/test_decrypt_email.py (85%) rename tests/test_utility/{ => test_email}/test_encrypt_email.py (92%) rename tests/test_utility/{ => test_email}/test_hash_email.py (89%) rename tests/test_utility/{ => test_email}/test_mask_email.py (100%) rename tests/test_utility/{ => test_email}/test_validate_email.py (91%) create mode 100644 tests/test_utility/test_password/__init__.py create mode 100644 tests/test_utility/test_password/conftest.py rename tests/test_utility/{ => test_password}/test_hash_password.py (85%) rename tests/test_utility/{ => test_password}/test_validate_password.py (95%) rename tests/test_utility/{ => test_password}/test_verify_password.py (94%) diff --git a/tests/test_routes/conftest.py b/tests/test_routes/conftest.py new file mode 100644 index 0000000..9172282 --- /dev/null +++ b/tests/test_routes/conftest.py @@ -0,0 +1,9 @@ +import pytest +from flask import Flask + + +@pytest.fixture +def client(app: Flask): + """Create a test client for the Flask application""" + with app.test_client() as client: + yield client diff --git a/tests/test_routes/test_authentication/conftest.py b/tests/test_routes/test_authentication/conftest.py new file mode 100644 index 0000000..f0154e8 --- /dev/null +++ b/tests/test_routes/test_authentication/conftest.py @@ -0,0 +1,27 @@ +import pytest +from flask import Flask + +from routes.authentication import authentication_blueprint + + +@pytest.fixture +def app(): + """Create and configure a new Flask application instance for testing""" + app = Flask(__name__) + app.register_blueprint(authentication_blueprint) + return app + + +@pytest.fixture +def sample_email(): + return "sample.email@example.com" + + +@pytest.fixture +def sample_password(): + return "SecurePass123!" + + +@pytest.fixture +def sample_username(): + return "sampleuser" diff --git a/tests/test_routes/test_authentication/test_login.py b/tests/test_routes/test_authentication/test_login.py index 18e10e6..b72b59e 100644 --- a/tests/test_routes/test_authentication/test_login.py +++ b/tests/test_routes/test_authentication/test_login.py @@ -1,39 +1,5 @@ -import pytest -from flask import Flask from flask.testing import FlaskClient -from routes.authentication import authentication_blueprint - - -@pytest.fixture -def app(): - """Create and configure a new Flask application instance for testing""" - app = Flask(__name__) - app.register_blueprint(authentication_blueprint) - return app - - -@pytest.fixture -def client(app: Flask): - """Create a test client for the Flask application""" - with app.test_client() as client: - yield client - - -@pytest.fixture -def sample_email(): - return "sample.email@example.com" - - -@pytest.fixture -def sample_password(): - return "SecurePass123!" - - -@pytest.fixture -def sample_username(): - return "sampleuser" - def test_missing_username(client: FlaskClient, sample_password): """Test login with missing username""" diff --git a/tests/test_routes/test_authentication/test_refresh_token.py b/tests/test_routes/test_authentication/test_refresh_token.py index 4241b30..e5950cd 100644 --- a/tests/test_routes/test_authentication/test_refresh_token.py +++ b/tests/test_routes/test_authentication/test_refresh_token.py @@ -9,22 +9,6 @@ from routes.authentication import authentication_blueprint -@pytest.fixture -def app(): - """Create and configure a new Flask application instance for testing""" - app = Flask(__name__) - app.register_blueprint(authentication_blueprint) - app.config["TESTING"] = True - return app - - -@pytest.fixture -def client(app: Flask): - """Create a test client for the Flask application""" - with app.test_client() as client: - yield client - - @pytest.fixture def sample_person_id() -> int: """Provide a sample person ID for testing""" diff --git a/tests/test_routes/test_authentication/test_register.py b/tests/test_routes/test_authentication/test_register.py index a6640f2..b22ad1f 100644 --- a/tests/test_routes/test_authentication/test_register.py +++ b/tests/test_routes/test_authentication/test_register.py @@ -1,39 +1,5 @@ -import pytest -from flask import Flask from flask.testing import FlaskClient -from routes.authentication import authentication_blueprint - - -@pytest.fixture -def app(): - """Create and configure a new Flask application instance for testing""" - app = Flask(__name__) - app.register_blueprint(authentication_blueprint) - return app - - -@pytest.fixture -def client(app: Flask): - """Create a test client for the Flask application""" - with app.test_client() as client: - yield client - - -@pytest.fixture -def sample_email(): - return "sample.email@example.com" - - -@pytest.fixture -def sample_password(): - return "SecurePass123!" - - -@pytest.fixture -def sample_username(): - return "sampleuser" - def test_missing_email(client: FlaskClient, sample_username, sample_password): """Test registration with missing email""" diff --git a/tests/test_utility/test_email/__init__.py b/tests/test_utility/test_email/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_utility/test_email/conftest.py b/tests/test_utility/test_email/conftest.py new file mode 100644 index 0000000..214d700 --- /dev/null +++ b/tests/test_utility/test_email/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture +def sample_email(): + return "sample.email@example.com" diff --git a/tests/test_utility/test_decrypt_email.py b/tests/test_utility/test_email/test_decrypt_email.py similarity index 85% rename from tests/test_utility/test_decrypt_email.py rename to tests/test_utility/test_email/test_decrypt_email.py index 3a91d3e..7c150ef 100644 --- a/tests/test_utility/test_decrypt_email.py +++ b/tests/test_utility/test_email/test_decrypt_email.py @@ -1,13 +1,6 @@ -import pytest - from utility import decrypt_email, encrypt_email -@pytest.fixture -def sample_email(): - return "sample.email@example.com" - - def test_decrypt_email_type(sample_email): """Ensure decrypt_email returns a string""" encrypted_email = encrypt_email(sample_email) diff --git a/tests/test_utility/test_encrypt_email.py b/tests/test_utility/test_email/test_encrypt_email.py similarity index 92% rename from tests/test_utility/test_encrypt_email.py rename to tests/test_utility/test_email/test_encrypt_email.py index 46f93fc..60709e7 100644 --- a/tests/test_utility/test_encrypt_email.py +++ b/tests/test_utility/test_email/test_encrypt_email.py @@ -1,13 +1,6 @@ -import pytest - from utility import encrypt_email -@pytest.fixture -def sample_email(): - return "sample.email@example.com" - - def test_encrypt_email(sample_email): """Ensure encrypt_email returns an encrypted email""" encrypted_email = encrypt_email(sample_email) diff --git a/tests/test_utility/test_hash_email.py b/tests/test_utility/test_email/test_hash_email.py similarity index 89% rename from tests/test_utility/test_hash_email.py rename to tests/test_utility/test_email/test_hash_email.py index 7ad2bf6..5803673 100644 --- a/tests/test_utility/test_hash_email.py +++ b/tests/test_utility/test_email/test_hash_email.py @@ -1,13 +1,6 @@ -import pytest - from utility import hash_email -@pytest.fixture -def sample_email(): - return "sample.email@example.com" - - def test_hash_email_type(sample_email): """Ensure hash_email returns a string""" hashed_email = hash_email(sample_email) diff --git a/tests/test_utility/test_mask_email.py b/tests/test_utility/test_email/test_mask_email.py similarity index 100% rename from tests/test_utility/test_mask_email.py rename to tests/test_utility/test_email/test_mask_email.py diff --git a/tests/test_utility/test_validate_email.py b/tests/test_utility/test_email/test_validate_email.py similarity index 91% rename from tests/test_utility/test_validate_email.py rename to tests/test_utility/test_email/test_validate_email.py index eb07995..3420025 100644 --- a/tests/test_utility/test_validate_email.py +++ b/tests/test_utility/test_email/test_validate_email.py @@ -1,13 +1,6 @@ -import pytest - from utility import validate_email -@pytest.fixture -def sample_email(): - return "sample.email@example.com" - - def test_valid_emails(): """Verify valid emails pass""" assert validate_email("good.email@example.com") is True diff --git a/tests/test_utility/test_password/__init__.py b/tests/test_utility/test_password/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_utility/test_password/conftest.py b/tests/test_utility/test_password/conftest.py new file mode 100644 index 0000000..7472ce0 --- /dev/null +++ b/tests/test_utility/test_password/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture +def sample_password(): + return "SecurePass123!" diff --git a/tests/test_utility/test_hash_password.py b/tests/test_utility/test_password/test_hash_password.py similarity index 85% rename from tests/test_utility/test_hash_password.py rename to tests/test_utility/test_password/test_hash_password.py index fc2619c..b624330 100644 --- a/tests/test_utility/test_hash_password.py +++ b/tests/test_utility/test_password/test_hash_password.py @@ -1,4 +1,3 @@ -import pytest from argon2 import PasswordHasher from utility import hash_password @@ -6,11 +5,6 @@ ph = PasswordHasher() -@pytest.fixture -def sample_password(): - return "SecurePass123!" - - def test_hash_password_type(sample_password): """Ensure hash_password returns a string""" hashed_password = hash_password(sample_password) diff --git a/tests/test_utility/test_validate_password.py b/tests/test_utility/test_password/test_validate_password.py similarity index 95% rename from tests/test_utility/test_validate_password.py rename to tests/test_utility/test_password/test_validate_password.py index 0a69a43..077f2bf 100644 --- a/tests/test_utility/test_validate_password.py +++ b/tests/test_utility/test_password/test_validate_password.py @@ -1,4 +1,3 @@ -import pytest from argon2 import PasswordHasher from utility import validate_password @@ -6,11 +5,6 @@ ph = PasswordHasher() -@pytest.fixture -def sample_password(): - return "SecurePass123!" - - def test_valid_passwords(): """Verify valid passwords pass""" assert validate_password("StrongPass1!") is True diff --git a/tests/test_utility/test_verify_password.py b/tests/test_utility/test_password/test_verify_password.py similarity index 94% rename from tests/test_utility/test_verify_password.py rename to tests/test_utility/test_password/test_verify_password.py index 9da87fa..dbfa3a9 100644 --- a/tests/test_utility/test_verify_password.py +++ b/tests/test_utility/test_password/test_verify_password.py @@ -7,11 +7,6 @@ ph = PasswordHasher() -@pytest.fixture -def sample_password(): - return "SecurePass123!" - - def test_verify_password(sample_password): """Verify correct password""" hashed_password = hash_password(sample_password) From 3645c1907a96e444e89f5ea9f314878957fa71b2 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 16:45:38 +0000 Subject: [PATCH 49/65] Remove unused imports in refresh token test file --- tests/test_routes/test_authentication/test_refresh_token.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_routes/test_authentication/test_refresh_token.py b/tests/test_routes/test_authentication/test_refresh_token.py index e5950cd..ad7bc59 100644 --- a/tests/test_routes/test_authentication/test_refresh_token.py +++ b/tests/test_routes/test_authentication/test_refresh_token.py @@ -2,11 +2,9 @@ import jwt import pytest -from flask import Flask from flask.testing import FlaskClient from jwt_helper import JWT_SECRET_KEY, generate_access_token, generate_refresh_token -from routes.authentication import authentication_blueprint @pytest.fixture From 0a75e226aac5710f6758634ef14318f0e620f07c Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 20:09:47 +0000 Subject: [PATCH 50/65] Start adding tests for access token generation and create test fixtures --- tests/test_jwt/__init__.py | 0 tests/test_jwt/conftest.py | 15 ++++++++++ tests/test_jwt/test_generate_access_token.py | 31 ++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 tests/test_jwt/__init__.py create mode 100644 tests/test_jwt/conftest.py create mode 100644 tests/test_jwt/test_generate_access_token.py diff --git a/tests/test_jwt/__init__.py b/tests/test_jwt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_jwt/conftest.py b/tests/test_jwt/conftest.py new file mode 100644 index 0000000..5f9b97c --- /dev/null +++ b/tests/test_jwt/conftest.py @@ -0,0 +1,15 @@ +import pytest +from flask import Flask + + +@pytest.fixture +def client(app: Flask): + """Create a test client for the Flask application""" + with app.test_client() as client: + yield client + + +@pytest.fixture +def sample_person_id() -> int: + """Provide a sample person ID for testing""" + return 12345 diff --git a/tests/test_jwt/test_generate_access_token.py b/tests/test_jwt/test_generate_access_token.py new file mode 100644 index 0000000..3e3edb1 --- /dev/null +++ b/tests/test_jwt/test_generate_access_token.py @@ -0,0 +1,31 @@ +import jwt +import pytest + +from jwt_helper import JWT_ACCESS_TOKEN_EXPIRY, JWT_SECRET_KEY, generate_access_token + + +@pytest.fixture +def sample_access_token(sample_person_id): + """Provide a sample access token for testing""" + return generate_access_token(sample_person_id) + + +def test_generate_access_token_type(sample_access_token): + """Ensure generate_access_token returns a string""" + assert isinstance(sample_access_token, str) + + +def test_generate_access_token_todo(): + # Decode the token to verify its contents + decoded_payload = jwt.decode( + sample_access_token, JWT_SECRET_KEY, algorithms=["HS256"] + ) + + # Check if the payload contains the correct person ID + assert decoded_payload["person_id"] == sample_person_id + + # Check if the token has an expiration time + assert "exp" in decoded_payload + + # Check if the token type is 'access' + assert decoded_payload["token_type"] == "access" From 9b4f6428878d8ba9f4cb6aee42b53686015c6ce4 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 20:22:13 +0000 Subject: [PATCH 51/65] Add tests for access token decoding and expiration validation --- tests/test_jwt/test_generate_access_token.py | 30 +++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/test_jwt/test_generate_access_token.py b/tests/test_jwt/test_generate_access_token.py index 3e3edb1..1c083fb 100644 --- a/tests/test_jwt/test_generate_access_token.py +++ b/tests/test_jwt/test_generate_access_token.py @@ -15,17 +15,33 @@ def test_generate_access_token_type(sample_access_token): assert isinstance(sample_access_token, str) -def test_generate_access_token_todo(): - # Decode the token to verify its contents +def test_generate_access_token_decoded(sample_person_id, sample_access_token): + """ + Ensure the generated access token can be decoded and contains the correct payload + - Check if the payload contains the correct person ID + - Check if the token has an expiration time + - Check if the token type is 'access' + """ decoded_payload = jwt.decode( sample_access_token, JWT_SECRET_KEY, algorithms=["HS256"] ) - # Check if the payload contains the correct person ID assert decoded_payload["person_id"] == sample_person_id - - # Check if the token has an expiration time assert "exp" in decoded_payload - - # Check if the token type is 'access' assert decoded_payload["token_type"] == "access" + + +def test_generate_access_token_expiration(sample_access_token): + """ + Ensure the generated access token has a valid expiration time + - Check if the expiration time is greater than 0 + - Check if the expiration time is greater than the issued at time + - Check if the token is not expired + """ + decoded_payload = jwt.decode( + sample_access_token, JWT_SECRET_KEY, algorithms=["HS256"] + ) + + assert decoded_payload["exp"] > 0 + assert decoded_payload["exp"] > decoded_payload["iat"] + assert decoded_payload["exp"] > JWT_ACCESS_TOKEN_EXPIRY.total_seconds() From b669e2faa5701743a7edff4aa5ed56dfe5245d36 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 21:16:53 +0000 Subject: [PATCH 52/65] Add tests for extracting and generating JWT refresh tokens --- .../test_extract_token_from_header.py | 48 +++++++++++++++++++ tests/test_jwt/test_refresh_access_token.py | 47 ++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 tests/test_jwt/test_extract_token_from_header.py create mode 100644 tests/test_jwt/test_refresh_access_token.py diff --git a/tests/test_jwt/test_extract_token_from_header.py b/tests/test_jwt/test_extract_token_from_header.py new file mode 100644 index 0000000..aede604 --- /dev/null +++ b/tests/test_jwt/test_extract_token_from_header.py @@ -0,0 +1,48 @@ +import pytest +from flask import Flask + +from jwt_helper import TokenError, extract_token_from_header + +app = Flask(__name__) + + +@pytest.fixture +def sample_token(): + """Provide a sample token for testing""" + return "mock_token_123" + + +def test_extract_token_valid(sample_token): + """Test extracting a valid Bearer token.""" + with app.test_request_context(headers={"Authorization": f"Bearer {sample_token}"}): + assert extract_token_from_header() == sample_token + + +def test_extract_token_missing(): + """Test missing Authorization header.""" + with app.test_request_context(headers={}): + with pytest.raises( + TokenError, match="Token is missing or improperly formatted" + ) as excinfo: + extract_token_from_header() + assert excinfo.value.status_code == 401 + + +def test_extract_token_invalid_format(): + """Test an improperly formatted Authorization header.""" + with app.test_request_context(headers={"Authorization": "InvalidTokenFormat"}): + with pytest.raises( + TokenError, match="Token is missing or improperly formatted" + ) as excinfo: + extract_token_from_header() + assert excinfo.value.status_code == 401 + + +def test_extract_token_no_bearer(sample_token): + """Test Authorization header without 'Bearer ' prefix.""" + with app.test_request_context(headers={"Authorization": f"Basic {sample_token}"}): + with pytest.raises( + TokenError, match="Token is missing or improperly formatted" + ) as excinfo: + extract_token_from_header() + assert excinfo.value.status_code == 401 diff --git a/tests/test_jwt/test_refresh_access_token.py b/tests/test_jwt/test_refresh_access_token.py new file mode 100644 index 0000000..bd4d4d7 --- /dev/null +++ b/tests/test_jwt/test_refresh_access_token.py @@ -0,0 +1,47 @@ +import jwt +import pytest + +from jwt_helper import JWT_REFRESH_TOKEN_EXPIRY, JWT_SECRET_KEY, generate_refresh_token + + +@pytest.fixture +def sample_refresh_token(sample_person_id): + """Provide a sample refresh token for testing""" + return generate_refresh_token(sample_person_id) + + +def test_generate_refresh_token_type(sample_refresh_token): + """Ensure generate_refresh_token returns a string""" + assert isinstance(sample_refresh_token, str) + + +def test_generate_refresh_token_decoded(sample_person_id, sample_refresh_token): + """ + Ensure the generated refresh token can be decoded and contains the correct payload + - Check if the payload contains the correct person ID + - Check if the token has an expiration time + - Check if the token type is 'refresh' + """ + decoded_payload = jwt.decode( + sample_refresh_token, JWT_SECRET_KEY, algorithms=["HS256"] + ) + + assert decoded_payload["person_id"] == sample_person_id + assert "exp" in decoded_payload + assert decoded_payload["token_type"] == "refresh" + + +def test_generate_refresh_token_expiration(sample_refresh_token): + """ + Ensure the generated refresh token has a valid expiration time + - Check if the expiration time is greater than 0 + - Check if the expiration time is greater than the issued at time + - Check if the token is not expired + """ + decoded_payload = jwt.decode( + sample_refresh_token, JWT_SECRET_KEY, algorithms=["HS256"] + ) + + assert decoded_payload["exp"] > 0 + assert decoded_payload["exp"] > decoded_payload["iat"] + assert decoded_payload["exp"] > JWT_REFRESH_TOKEN_EXPIRY.total_seconds() From a241c2e6bfc908d6857195d402c6426433c226e5 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 21:35:41 +0000 Subject: [PATCH 53/65] Add fixtures and tests for access and refresh token handling --- tests/test_jwt/conftest.py | 14 +++ .../test_extract_token_from_header.py | 6 -- tests/test_jwt/test_generate_access_token.py | 15 +-- tests/test_jwt/test_refresh_access_token.py | 6 +- tests/test_jwt/test_token_required.py | 43 +++++++++ tests/test_jwt/test_verify_token.py | 95 +++++++++++++++++++ .../test_authentication/conftest.py | 13 +++ .../test_authentication/test_refresh_token.py | 4 +- 8 files changed, 174 insertions(+), 22 deletions(-) create mode 100644 tests/test_jwt/test_token_required.py create mode 100644 tests/test_jwt/test_verify_token.py diff --git a/tests/test_jwt/conftest.py b/tests/test_jwt/conftest.py index 5f9b97c..72a5cf3 100644 --- a/tests/test_jwt/conftest.py +++ b/tests/test_jwt/conftest.py @@ -1,6 +1,8 @@ import pytest from flask import Flask +from jwt_helper import generate_access_token + @pytest.fixture def client(app: Flask): @@ -13,3 +15,15 @@ def client(app: Flask): def sample_person_id() -> int: """Provide a sample person ID for testing""" return 12345 + + +@pytest.fixture +def sample_token(): + """Provide a sample token for testing""" + return "mock_token_123" + + +@pytest.fixture +def sample_access_token(sample_person_id): + """Provide a sample access token for testing""" + return generate_access_token(sample_person_id) diff --git a/tests/test_jwt/test_extract_token_from_header.py b/tests/test_jwt/test_extract_token_from_header.py index aede604..e83eb92 100644 --- a/tests/test_jwt/test_extract_token_from_header.py +++ b/tests/test_jwt/test_extract_token_from_header.py @@ -6,12 +6,6 @@ app = Flask(__name__) -@pytest.fixture -def sample_token(): - """Provide a sample token for testing""" - return "mock_token_123" - - def test_extract_token_valid(sample_token): """Test extracting a valid Bearer token.""" with app.test_request_context(headers={"Authorization": f"Bearer {sample_token}"}): diff --git a/tests/test_jwt/test_generate_access_token.py b/tests/test_jwt/test_generate_access_token.py index 1c083fb..5be8110 100644 --- a/tests/test_jwt/test_generate_access_token.py +++ b/tests/test_jwt/test_generate_access_token.py @@ -1,21 +1,14 @@ import jwt -import pytest -from jwt_helper import JWT_ACCESS_TOKEN_EXPIRY, JWT_SECRET_KEY, generate_access_token +from jwt_helper import JWT_ACCESS_TOKEN_EXPIRY, JWT_SECRET_KEY -@pytest.fixture -def sample_access_token(sample_person_id): - """Provide a sample access token for testing""" - return generate_access_token(sample_person_id) - - -def test_generate_access_token_type(sample_access_token): +def test_access_token_type(sample_access_token): """Ensure generate_access_token returns a string""" assert isinstance(sample_access_token, str) -def test_generate_access_token_decoded(sample_person_id, sample_access_token): +def test_decoded_access_token(sample_person_id, sample_access_token): """ Ensure the generated access token can be decoded and contains the correct payload - Check if the payload contains the correct person ID @@ -31,7 +24,7 @@ def test_generate_access_token_decoded(sample_person_id, sample_access_token): assert decoded_payload["token_type"] == "access" -def test_generate_access_token_expiration(sample_access_token): +def test_access_token_expiration(sample_access_token): """ Ensure the generated access token has a valid expiration time - Check if the expiration time is greater than 0 diff --git a/tests/test_jwt/test_refresh_access_token.py b/tests/test_jwt/test_refresh_access_token.py index bd4d4d7..77d2cf4 100644 --- a/tests/test_jwt/test_refresh_access_token.py +++ b/tests/test_jwt/test_refresh_access_token.py @@ -10,12 +10,12 @@ def sample_refresh_token(sample_person_id): return generate_refresh_token(sample_person_id) -def test_generate_refresh_token_type(sample_refresh_token): +def test_refresh_token_type(sample_refresh_token): """Ensure generate_refresh_token returns a string""" assert isinstance(sample_refresh_token, str) -def test_generate_refresh_token_decoded(sample_person_id, sample_refresh_token): +def test_decoded_refresh_token_decoded(sample_person_id, sample_refresh_token): """ Ensure the generated refresh token can be decoded and contains the correct payload - Check if the payload contains the correct person ID @@ -31,7 +31,7 @@ def test_generate_refresh_token_decoded(sample_person_id, sample_refresh_token): assert decoded_payload["token_type"] == "refresh" -def test_generate_refresh_token_expiration(sample_refresh_token): +def test_refresh_token_expiration(sample_refresh_token): """ Ensure the generated refresh token has a valid expiration time - Check if the expiration time is greater than 0 diff --git a/tests/test_jwt/test_token_required.py b/tests/test_jwt/test_token_required.py new file mode 100644 index 0000000..b11bbbf --- /dev/null +++ b/tests/test_jwt/test_token_required.py @@ -0,0 +1,43 @@ +from flask import Flask, jsonify + +from jwt_helper import generate_access_token, token_required + +app = Flask(__name__) + + +def test_token_required_valid(sample_access_token): + """Test the token_required decorator with a valid token.""" + + @token_required + def protected_route(): + return jsonify(message="Success"), 200 + + with app.test_request_context( + headers={"Authorization": f"Bearer {sample_access_token}"} + ): + response = protected_route() + assert response[1] == 200 + + +def test_token_required_invalid_token(): + """Test the token_required decorator with an invalid token.""" + with app.test_request_context(headers={"Authorization": "Bearer invalid_token"}): + + @token_required + def protected_route(): + return jsonify(message="Success"), 200 + + response = protected_route() + assert response[1] == 401 + + +def test_token_required_missing_token(): + """Test the token_required decorator when no token is provided.""" + with app.test_request_context(headers={}): + + @token_required + def protected_route(): + return jsonify(message="Success"), 200 + + response = protected_route() + assert response[1] == 401 diff --git a/tests/test_jwt/test_verify_token.py b/tests/test_jwt/test_verify_token.py new file mode 100644 index 0000000..4b5b4be --- /dev/null +++ b/tests/test_jwt/test_verify_token.py @@ -0,0 +1,95 @@ +from datetime import datetime, timedelta + +import jwt +import pytest +from flask import Flask + +from jwt_helper import ( + JWT_SECRET_KEY, + TokenError, + extract_token_from_header, + verify_token, +) + +app = Flask(__name__) + + +def test_extract_token_valid(sample_token): + """Test extracting a valid Bearer token.""" + with app.test_request_context(headers={"Authorization": f"Bearer {sample_token}"}): + assert extract_token_from_header() == sample_token + + +def test_extract_token_missing(): + """Test missing Authorization header.""" + with app.test_request_context(headers={}): + with pytest.raises( + TokenError, match="Token is missing or improperly formatted" + ) as excinfo: + extract_token_from_header() + assert excinfo.value.status_code == 401 + + +def test_extract_token_invalid_format(): + """Test an improperly formatted Authorization header.""" + with app.test_request_context(headers={"Authorization": "InvalidTokenFormat"}): + with pytest.raises( + TokenError, match="Token is missing or improperly formatted" + ) as excinfo: + extract_token_from_header() + assert excinfo.value.status_code == 401 + + +def test_extract_token_no_bearer(sample_token): + """Test Authorization header without 'Bearer ' prefix.""" + with app.test_request_context(headers={"Authorization": f"Basic {sample_token}"}): + with pytest.raises( + TokenError, match="Token is missing or improperly formatted" + ) as excinfo: + extract_token_from_header() + assert excinfo.value.status_code == 401 + + +def test_verify_valid_access_token(): + """Test verifying a valid access token.""" + access_token = jwt.encode( + {"token_type": "access"}, JWT_SECRET_KEY, algorithm="HS256" + ) + decoded = verify_token(access_token, "access") + assert decoded["token_type"] == "access" + + +def test_verify_valid_refresh_token(): + """Test verifying a valid refresh token.""" + refresh_token = jwt.encode( + {"token_type": "refresh"}, JWT_SECRET_KEY, algorithm="HS256" + ) + decoded = verify_token(refresh_token, "refresh") + assert decoded["token_type"] == "refresh" + + +def test_verify_token_invalid_type(): + """Test verifying a token with an incorrect type.""" + token = jwt.encode({"token_type": "invalid"}, JWT_SECRET_KEY, algorithm="HS256") + with pytest.raises(TokenError, match="Invalid token") as excinfo: + verify_token(token, "access") + assert excinfo.value.status_code == 401 + + +def test_verify_expired_token(): + """Test verifying an expired token.""" + expired_token = jwt.encode( + {"token_type": "access", "exp": datetime.now() - timedelta(seconds=1)}, + JWT_SECRET_KEY, + algorithm="HS256", + ) + with pytest.raises(TokenError, match="Token has expired") as excinfo: + verify_token(expired_token, "access") + assert excinfo.value.status_code == 401 + + +def test_verify_invalid_token(): + """Test verifying an invalid token.""" + with pytest.raises(TokenError, match="Invalid token") as excinfo: + verify_token("invalid_token", "access") + assert excinfo.value.status_code == 401 diff --git a/tests/test_routes/test_authentication/conftest.py b/tests/test_routes/test_authentication/conftest.py index f0154e8..d5897ca 100644 --- a/tests/test_routes/test_authentication/conftest.py +++ b/tests/test_routes/test_authentication/conftest.py @@ -1,6 +1,7 @@ import pytest from flask import Flask +from jwt_helper import generate_access_token from routes.authentication import authentication_blueprint @@ -25,3 +26,15 @@ def sample_password(): @pytest.fixture def sample_username(): return "sampleuser" + + +@pytest.fixture +def sample_token(): + """Provide a sample token for testing""" + return "mock_token_123" + + +@pytest.fixture +def sample_access_token(sample_person_id): + """Provide a sample access token for testing""" + return generate_access_token(sample_person_id) diff --git a/tests/test_routes/test_authentication/test_refresh_token.py b/tests/test_routes/test_authentication/test_refresh_token.py index ad7bc59..c65231c 100644 --- a/tests/test_routes/test_authentication/test_refresh_token.py +++ b/tests/test_routes/test_authentication/test_refresh_token.py @@ -33,14 +33,14 @@ def sample_expired_token(sample_person_id) -> str: return jwt.encode(payload, JWT_SECRET_KEY, algorithm="HS256") -def test_refresh_token_success(client, sample_refresh_token, sample_person_id): +def test_refresh_token_success(client, sample_refresh_token, sample_access_token): """Test the refresh token endpoint with a valid token""" headers = {"Authorization": f"Bearer {sample_refresh_token}"} response = client.post("/refresh", headers=headers) assert response.status_code == 200 assert "access_token" in response.json - assert response.json["access_token"] == generate_access_token(sample_person_id) + assert response.json["access_token"] == sample_access_token def test_refresh_token_invalid_token(client: FlaskClient): From da1f16a2756bacfb6ef0eb01c1b0ffc4e2f0db53 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 21:42:44 +0000 Subject: [PATCH 54/65] Remove redundant tests for token extraction and validation --- tests/test_jwt/test_verify_token.py | 36 ----------------------------- 1 file changed, 36 deletions(-) diff --git a/tests/test_jwt/test_verify_token.py b/tests/test_jwt/test_verify_token.py index 4b5b4be..f1a285e 100644 --- a/tests/test_jwt/test_verify_token.py +++ b/tests/test_jwt/test_verify_token.py @@ -14,42 +14,6 @@ app = Flask(__name__) -def test_extract_token_valid(sample_token): - """Test extracting a valid Bearer token.""" - with app.test_request_context(headers={"Authorization": f"Bearer {sample_token}"}): - assert extract_token_from_header() == sample_token - - -def test_extract_token_missing(): - """Test missing Authorization header.""" - with app.test_request_context(headers={}): - with pytest.raises( - TokenError, match="Token is missing or improperly formatted" - ) as excinfo: - extract_token_from_header() - assert excinfo.value.status_code == 401 - - -def test_extract_token_invalid_format(): - """Test an improperly formatted Authorization header.""" - with app.test_request_context(headers={"Authorization": "InvalidTokenFormat"}): - with pytest.raises( - TokenError, match="Token is missing or improperly formatted" - ) as excinfo: - extract_token_from_header() - assert excinfo.value.status_code == 401 - - -def test_extract_token_no_bearer(sample_token): - """Test Authorization header without 'Bearer ' prefix.""" - with app.test_request_context(headers={"Authorization": f"Basic {sample_token}"}): - with pytest.raises( - TokenError, match="Token is missing or improperly formatted" - ) as excinfo: - extract_token_from_header() - assert excinfo.value.status_code == 401 - - def test_verify_valid_access_token(): """Test verifying a valid access token.""" access_token = jwt.encode( From 2700328b6e101e3376545c2425b9b245467a926c Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 21:43:53 +0000 Subject: [PATCH 55/65] Remove unused access token generation imports from test files --- tests/test_jwt/test_token_required.py | 2 +- tests/test_routes/test_authentication/test_refresh_token.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_jwt/test_token_required.py b/tests/test_jwt/test_token_required.py index b11bbbf..c74862b 100644 --- a/tests/test_jwt/test_token_required.py +++ b/tests/test_jwt/test_token_required.py @@ -1,6 +1,6 @@ from flask import Flask, jsonify -from jwt_helper import generate_access_token, token_required +from jwt_helper import token_required app = Flask(__name__) diff --git a/tests/test_routes/test_authentication/test_refresh_token.py b/tests/test_routes/test_authentication/test_refresh_token.py index c65231c..d630d21 100644 --- a/tests/test_routes/test_authentication/test_refresh_token.py +++ b/tests/test_routes/test_authentication/test_refresh_token.py @@ -4,7 +4,7 @@ import pytest from flask.testing import FlaskClient -from jwt_helper import JWT_SECRET_KEY, generate_access_token, generate_refresh_token +from jwt_helper import JWT_SECRET_KEY, generate_refresh_token @pytest.fixture From 969cb84c772e4c14ec505d0a2283a3a1f835e179 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Mon, 31 Mar 2025 21:52:16 +0000 Subject: [PATCH 56/65] Refactor imports in test_verify_token.py to remove unused functions --- tests/test_jwt/test_verify_token.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test_jwt/test_verify_token.py b/tests/test_jwt/test_verify_token.py index f1a285e..ca97280 100644 --- a/tests/test_jwt/test_verify_token.py +++ b/tests/test_jwt/test_verify_token.py @@ -4,12 +4,7 @@ import pytest from flask import Flask -from jwt_helper import ( - JWT_SECRET_KEY, - TokenError, - extract_token_from_header, - verify_token, -) +from jwt_helper import JWT_SECRET_KEY, TokenError, verify_token app = Flask(__name__) From 88f6194a0f2d456f6cd918d051d4c13380287fc6 Mon Sep 17 00:00:00 2001 From: Vianney Veremme <10519369+Vianpyro@users.noreply.github.com> Date: Tue, 1 Apr 2025 07:53:29 -0400 Subject: [PATCH 57/65] Update routes/picture.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- routes/picture.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/picture.py b/routes/picture.py index b5a9ef0..16597f8 100644 --- a/routes/picture.py +++ b/routes/picture.py @@ -80,8 +80,8 @@ def upload_picture(): with database_cursor() as cursor: cursor.callproc(procedure, (hexname, request.person_id)) - fullpath = os.path.normpath(os.path.join(Config.IMAGES_FOLDER, hexname)) - if not fullpath.startswith(Config.IMAGES_FOLDER): + fullpath = os.path.abspath(os.path.join(Config.IMAGES_FOLDER, hexname)) + if os.path.commonpath([fullpath, Config.IMAGES_FOLDER]) != Config.IMAGES_FOLDER: return jsonify({"error": "Invalid file path"}), 400 file.save(fullpath) From b6fbf7e46e4aca826dd2bd1b45deb3a06cccf364 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Tue, 1 Apr 2025 07:53:38 -0400 Subject: [PATCH 58/65] Fix test for missing username in registration by updating test data --- tests/test_routes/test_authentication/test_register.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_routes/test_authentication/test_register.py b/tests/test_routes/test_authentication/test_register.py index b22ad1f..242000e 100644 --- a/tests/test_routes/test_authentication/test_register.py +++ b/tests/test_routes/test_authentication/test_register.py @@ -27,12 +27,12 @@ def test_missing_password(client: FlaskClient, sample_username, sample_email): assert response.json["message"] == "Username, email, and password are required" -def test_missing_username(client: FlaskClient, sample_username, sample_email): +def test_missing_username(client: FlaskClient, sample_email, sample_password): """Test registration with missing username""" data = { - "username": sample_username, # Missing username + "username": "", # Missing username "email": sample_email, - "password": "", # Missing password + "password": sample_password, # Missing password } response = client.post("/register", json=data) From 9eb9963e821f44af8d530e8bf6cb6e88ed4ccef2 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Tue, 1 Apr 2025 12:39:24 +0000 Subject: [PATCH 59/65] Enhance GitHub Actions workflow by adding descriptive job name for Pytest --- .github/workflows/pytest.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 17ca5a8..05737e5 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -10,6 +10,7 @@ permissions: {} jobs: test: + name: Run Pytest on ${{ matrix.os }} with Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }} permissions: From 2eb916333fe945ef7e40baaa485c7cb7b7762f51 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Tue, 1 Apr 2025 12:43:32 +0000 Subject: [PATCH 60/65] Fix test comment --- .github/workflows/pytest.yml | 2 +- tests/test_routes/test_authentication/test_register.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 05737e5..146e6b1 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -10,7 +10,7 @@ permissions: {} jobs: test: - name: Run Pytest on ${{ matrix.os }} with Python ${{ matrix.python-version }} + name: Pytest on ${{ matrix.os }} with Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }} permissions: diff --git a/tests/test_routes/test_authentication/test_register.py b/tests/test_routes/test_authentication/test_register.py index 242000e..117448e 100644 --- a/tests/test_routes/test_authentication/test_register.py +++ b/tests/test_routes/test_authentication/test_register.py @@ -32,7 +32,7 @@ def test_missing_username(client: FlaskClient, sample_email, sample_password): data = { "username": "", # Missing username "email": sample_email, - "password": sample_password, # Missing password + "password": sample_password, } response = client.post("/register", json=data) From f6a42e7a4e0fc7c1d5479e389bf49ca50756e2b8 Mon Sep 17 00:00:00 2001 From: Vianney Veremme <10519369+Vianpyro@users.noreply.github.com> Date: Tue, 1 Apr 2025 08:45:51 -0400 Subject: [PATCH 61/65] Update tests/test_jwt/test_refresh_access_token.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_jwt/test_refresh_access_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_jwt/test_refresh_access_token.py b/tests/test_jwt/test_refresh_access_token.py index 77d2cf4..6cf14aa 100644 --- a/tests/test_jwt/test_refresh_access_token.py +++ b/tests/test_jwt/test_refresh_access_token.py @@ -44,4 +44,4 @@ def test_refresh_token_expiration(sample_refresh_token): assert decoded_payload["exp"] > 0 assert decoded_payload["exp"] > decoded_payload["iat"] - assert decoded_payload["exp"] > JWT_REFRESH_TOKEN_EXPIRY.total_seconds() + assert (decoded_payload["exp"] - decoded_payload["iat"]) > JWT_REFRESH_TOKEN_EXPIRY.total_seconds() From 340ca5d35d90fe5962a9cca4eca6a85d8041a665 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Tue, 1 Apr 2025 12:52:21 +0000 Subject: [PATCH 62/65] Improve JWT token tests --- tests/test_jwt/test_generate_access_token.py | 20 ++++++++------------ tests/test_jwt/test_refresh_access_token.py | 20 ++++++++------------ 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/tests/test_jwt/test_generate_access_token.py b/tests/test_jwt/test_generate_access_token.py index 5be8110..ab0c57d 100644 --- a/tests/test_jwt/test_generate_access_token.py +++ b/tests/test_jwt/test_generate_access_token.py @@ -15,13 +15,11 @@ def test_decoded_access_token(sample_person_id, sample_access_token): - Check if the token has an expiration time - Check if the token type is 'access' """ - decoded_payload = jwt.decode( - sample_access_token, JWT_SECRET_KEY, algorithms=["HS256"] - ) + payload = jwt.decode(sample_access_token, JWT_SECRET_KEY, algorithms=["HS256"]) - assert decoded_payload["person_id"] == sample_person_id - assert "exp" in decoded_payload - assert decoded_payload["token_type"] == "access" + assert payload["person_id"] == sample_person_id + assert "exp" in payload + assert payload["token_type"] == "access" def test_access_token_expiration(sample_access_token): @@ -31,10 +29,8 @@ def test_access_token_expiration(sample_access_token): - Check if the expiration time is greater than the issued at time - Check if the token is not expired """ - decoded_payload = jwt.decode( - sample_access_token, JWT_SECRET_KEY, algorithms=["HS256"] - ) + payload = jwt.decode(sample_access_token, JWT_SECRET_KEY, algorithms=["HS256"]) - assert decoded_payload["exp"] > 0 - assert decoded_payload["exp"] > decoded_payload["iat"] - assert decoded_payload["exp"] > JWT_ACCESS_TOKEN_EXPIRY.total_seconds() + assert payload["exp"] > 0 + assert payload["exp"] > payload["iat"] + assert (payload["exp"] - payload["iat"]) == JWT_ACCESS_TOKEN_EXPIRY.total_seconds() diff --git a/tests/test_jwt/test_refresh_access_token.py b/tests/test_jwt/test_refresh_access_token.py index 6cf14aa..ffd9a4a 100644 --- a/tests/test_jwt/test_refresh_access_token.py +++ b/tests/test_jwt/test_refresh_access_token.py @@ -22,13 +22,11 @@ def test_decoded_refresh_token_decoded(sample_person_id, sample_refresh_token): - Check if the token has an expiration time - Check if the token type is 'refresh' """ - decoded_payload = jwt.decode( - sample_refresh_token, JWT_SECRET_KEY, algorithms=["HS256"] - ) + payload = jwt.decode(sample_refresh_token, JWT_SECRET_KEY, algorithms=["HS256"]) - assert decoded_payload["person_id"] == sample_person_id - assert "exp" in decoded_payload - assert decoded_payload["token_type"] == "refresh" + assert payload["person_id"] == sample_person_id + assert "exp" in payload + assert payload["token_type"] == "refresh" def test_refresh_token_expiration(sample_refresh_token): @@ -38,10 +36,8 @@ def test_refresh_token_expiration(sample_refresh_token): - Check if the expiration time is greater than the issued at time - Check if the token is not expired """ - decoded_payload = jwt.decode( - sample_refresh_token, JWT_SECRET_KEY, algorithms=["HS256"] - ) + payload = jwt.decode(sample_refresh_token, JWT_SECRET_KEY, algorithms=["HS256"]) - assert decoded_payload["exp"] > 0 - assert decoded_payload["exp"] > decoded_payload["iat"] - assert (decoded_payload["exp"] - decoded_payload["iat"]) > JWT_REFRESH_TOKEN_EXPIRY.total_seconds() + assert payload["exp"] > 0 + assert payload["exp"] > payload["iat"] + assert (payload["exp"] - payload["iat"]) == JWT_REFRESH_TOKEN_EXPIRY.total_seconds() From 2c24a10b34f3592ec8d30a433e9459c1c675b2a8 Mon Sep 17 00:00:00 2001 From: Vianney Veremme <10519369+Vianpyro@users.noreply.github.com> Date: Tue, 1 Apr 2025 08:55:19 -0400 Subject: [PATCH 63/65] Update tests/test_routes/test_authentication/test_login.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_routes/test_authentication/test_login.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_routes/test_authentication/test_login.py b/tests/test_routes/test_authentication/test_login.py index b72b59e..7ab7350 100644 --- a/tests/test_routes/test_authentication/test_login.py +++ b/tests/test_routes/test_authentication/test_login.py @@ -1,10 +1,10 @@ from flask.testing import FlaskClient -def test_missing_username(client: FlaskClient, sample_password): - """Test login with missing username""" +def test_missing_email(client: FlaskClient, sample_password): + """Test login with missing email""" data = { - "username": "", # Missing username + "email": "", # Missing email "password": sample_password, } response = client.post("/login", json=data) From 8310ae158e38e08225584d22f2a3d11ade2ecf00 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Tue, 1 Apr 2025 12:56:55 +0000 Subject: [PATCH 64/65] Fix test for missing password by changing username to email in test_login.py --- tests/test_routes/test_authentication/test_login.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_routes/test_authentication/test_login.py b/tests/test_routes/test_authentication/test_login.py index 7ab7350..c972f16 100644 --- a/tests/test_routes/test_authentication/test_login.py +++ b/tests/test_routes/test_authentication/test_login.py @@ -13,10 +13,10 @@ def test_missing_email(client: FlaskClient, sample_password): assert response.json["message"] == "Email and password are required" -def test_missing_password(client: FlaskClient, sample_username): +def test_missing_password(client: FlaskClient, sample_email): """Test login with missing password""" data = { - "username": sample_username, + "email": sample_email, "password": "", # Missing password } response = client.post("/login", json=data) From e917b987987eb20ec17a34f72086211506ce3e8a Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Tue, 1 Apr 2025 13:08:34 +0000 Subject: [PATCH 65/65] Refactor refresh token tests: consolidate and enhance test coverage --- ..._refresh_access_token.py => test_refresh_token.py} | 0 .../{test_refresh_token.py => test_refresh.py} | 11 +++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) rename tests/test_jwt/{test_refresh_access_token.py => test_refresh_token.py} (100%) rename tests/test_routes/test_authentication/{test_refresh_token.py => test_refresh.py} (87%) diff --git a/tests/test_jwt/test_refresh_access_token.py b/tests/test_jwt/test_refresh_token.py similarity index 100% rename from tests/test_jwt/test_refresh_access_token.py rename to tests/test_jwt/test_refresh_token.py diff --git a/tests/test_routes/test_authentication/test_refresh_token.py b/tests/test_routes/test_authentication/test_refresh.py similarity index 87% rename from tests/test_routes/test_authentication/test_refresh_token.py rename to tests/test_routes/test_authentication/test_refresh.py index d630d21..a8e5b83 100644 --- a/tests/test_routes/test_authentication/test_refresh_token.py +++ b/tests/test_routes/test_authentication/test_refresh.py @@ -33,14 +33,17 @@ def sample_expired_token(sample_person_id) -> str: return jwt.encode(payload, JWT_SECRET_KEY, algorithm="HS256") -def test_refresh_token_success(client, sample_refresh_token, sample_access_token): +def test_refresh_token_success(client, sample_refresh_token): """Test the refresh token endpoint with a valid token""" headers = {"Authorization": f"Bearer {sample_refresh_token}"} response = client.post("/refresh", headers=headers) + encoded_token = response.json["access_token"] + token = jwt.decode(encoded_token, JWT_SECRET_KEY, algorithms=["HS256"]) - assert response.status_code == 200 - assert "access_token" in response.json - assert response.json["access_token"] == sample_access_token + assert "person_id" in token + assert "exp" in token + assert "iat" in token + assert token["token_type"] == "access" def test_refresh_token_invalid_token(client: FlaskClient):