Skip to content

Commit faa05c2

Browse files
VianpyroCopilot
andauthored
Add unit tests (#32)
* Add password validation tests and update devcontainer to run tests on start * Add test to ensure tampered password fails verification * Nest unit tests and rename `utility.py` -> `utils.py` * Add initial routes module for organizing route handlers * Add GitHub Actions workflow for testing across multiple Python versions and OS * Refactor GitHub Actions workflow to combine dependency installation and testing steps * Remove redundant test command from GitHub Actions workflow * Update GitHub Actions workflow to run tests on multiple OS environments * Refactor GitHub Actions workflow to consolidate OS matrix for testing * Refactor GitHub Actions workflow to specify branch for push events and streamline test job configuration * Rename workflow from "Python package" to "Pytest CI" and remove branch specification for push events * Add Python 3.x to the testing matrix in GitHub Actions workflow * Simplify Python version specification in GitHub Actions workflow to use "3.x" * Update Python version specification to use a list format in GitHub Actions workflow * Add descriptive comment for OS and Python versions in GitHub Actions workflow * Revert "Add descriptive comment for OS and Python versions in GitHub Actions workflow" This reverts commit 4eb0780. * Add branch filter for main in GitHub Actions workflow * Refactor imports in person.py and define __all__ in utils.py for better module management * Add branch filter for main in Pytest CI workflow * Add RunOnSave extension and configure pytest command for test files * Update RunOnSave configuration to improve test file matching and output handling * Add file exclusion settings for __pycache__ and .pytest_cache in VSCode * Refactor utility imports and reorganize test structure * Remove obsolete test for index route in test_home.py * Refactor index route test to use a pytest fixture for the test client * Add tests for email encryption and hashing utilities; refactor password hashing tests * Refactor decrypt_email tests to separate type and value assertions * Remove unused import of register_routes in test_routes initialization * Refactor index route test to separate status code and JSON response assertions * Update image upload folder path and ensure directory creation for new location * Update picture route to use Config.IMAGES_FOLDER for file handling * Rename upload folder from 'images' to 'pictures' in .gitignore * Add tests for user registration with missing fields * Update postStartCommand in devcontainer.json for verbose pytest output * Add email validation to registration and update utility functions * Add tests for user registration and login with missing fields * Fix email validation regex and add comprehensive tests for valid and invalid email formats * Add additional tests for email validation to cover edge cases * Add docstring to extract_error_message for clarity on functionality * Update email validation regex and add git fetch command in VSCode settings * Add email masking functionality and corresponding tests * Refactor authentication tests to use fixtures for sample data and improve test clarity * Remove git fetch command from VSCode settings to streamline run-on-save functionality * Remove verbosity from postStartCommand in devcontainer configuration * Refactor JWT helper functions and add comprehensive tests for refresh token functionality * Update GitHub Actions workflow to define permissions for test job * Run Prettier * Add utility tests for email and password handling, including hashing and masking * Remove unused imports in refresh token test file * Start adding tests for access token generation and create test fixtures * Add tests for access token decoding and expiration validation * Add tests for extracting and generating JWT refresh tokens * Add fixtures and tests for access and refresh token handling * Remove redundant tests for token extraction and validation * Remove unused access token generation imports from test files * Refactor imports in test_verify_token.py to remove unused functions * Update routes/picture.py Co-authored-by: Copilot <[email protected]> * Fix test for missing username in registration by updating test data * Enhance GitHub Actions workflow by adding descriptive job name for Pytest * Fix test comment * Update tests/test_jwt/test_refresh_access_token.py Co-authored-by: Copilot <[email protected]> * Improve JWT token tests * Update tests/test_routes/test_authentication/test_login.py Co-authored-by: Copilot <[email protected]> * Fix test for missing password by changing username to email in test_login.py * Refactor refresh token tests: consolidate and enhance test coverage --------- Co-authored-by: Copilot <[email protected]>
1 parent d910cbd commit faa05c2

37 files changed

+823
-52
lines changed

.devcontainer/devcontainer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
"extensions": [
1010
"ms-python.black-formatter",
1111
"ms-python.python",
12-
"ms-python.isort"
12+
"ms-python.isort",
13+
"emeraldwalk.RunOnSave"
1314
]
1415
}
1516
},
16-
"postStartCommand": "pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt --break-system-packages",
17+
"postStartCommand": "pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt --break-system-packages && python3 -m pytest tests",
1718
"remoteUser": "vscode"
1819
}

.github/workflows/pytest.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Pytest CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
permissions: {}
10+
11+
jobs:
12+
test:
13+
name: Pytest on ${{ matrix.os }} with Python ${{ matrix.python-version }}
14+
runs-on: ${{ matrix.os }}
15+
16+
permissions:
17+
contents: read
18+
packages: read
19+
statuses: write
20+
21+
strategy:
22+
matrix:
23+
os: [ubuntu-latest, macos-latest, windows-latest]
24+
python-version: ["3.x"]
25+
26+
steps:
27+
- name: Checkout code
28+
uses: actions/checkout@v4
29+
30+
- name: Set up Python
31+
uses: actions/setup-python@v5
32+
with:
33+
python-version: ${{ matrix.python-version }}
34+
cache: "pip"
35+
36+
- name: Install dependencies
37+
run: |
38+
pip install -r requirements.txt
39+
pip install pytest pytest-cov
40+
41+
- name: Run Pytest (Linux/macOS)
42+
if: runner.os != 'Windows'
43+
run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html
44+
45+
- name: Run Pytest (Windows)
46+
if: runner.os == 'Windows'
47+
run: python -m pytest --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
*.pem
33

44
# Upload folder
5-
images/
5+
pictures/
66

77
# Environment variables file
88
.env

.vscode/settings.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
"files.insertFinalNewline": true,
66
"files.trimFinalNewlines": true,
77
"files.trimTrailingWhitespace": true,
8+
"files.exclude": {
9+
"**/__pycache__": true,
10+
"**/.pytest_cache": true
11+
},
812
"[python]": {
913
"editor.rulers": [
1014
88
@@ -18,5 +22,14 @@
1822
"isort.args": [
1923
"--profile",
2024
"black"
21-
]
25+
],
26+
"emeraldwalk.runonsave": {
27+
"commands": [
28+
{
29+
"match": "tests[/\\\\](.*[/\\\\])?test_.*\\.py$",
30+
"cmd": "python3 -m pytest '${relativeFile}' -v",
31+
"autoShowOutputPanel": "error"
32+
}
33+
]
34+
}
2235
}

config.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,16 @@ class Config:
2323
JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30)
2424

2525
# Image upload folder
26-
IMAGES_FOLDER = os.path.join(os.getcwd(), "images")
26+
IMAGES_FOLDER = os.path.join(os.getcwd(), "pictures")
2727
MAX_CONTENT_LENGTH = 1024 * 1024 # 1 MB
2828

2929

30-
# Image upload folder
31-
PICTURE_FOLDER = os.path.join(os.getcwd(), "picture")
32-
33-
if not os.path.exists(PICTURE_FOLDER):
34-
os.makedirs(PICTURE_FOLDER)
30+
if not os.path.exists(Config.IMAGES_FOLDER):
31+
os.makedirs(Config.IMAGES_FOLDER)
3532

3633
# Pagination configuration
3734
DEFAULT_PAGE_SIZE = 10
3835

39-
if not os.path.exists(Config.IMAGES_FOLDER):
40-
os.makedirs(Config.IMAGES_FOLDER)
41-
4236
limiter = Limiter(
4337
key_func=get_remote_address,
4438
default_limits=["1000 per day", "200 per hour", "30 per minute", "3 per second"],

jwt_helper.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
1111

1212

1313
class TokenError(Exception):
14-
"""
15-
Custom exception for token-related errors.
16-
"""
14+
"""Custom exception for token-related errors."""
1715

1816
def __init__(self, message, status_code):
1917
super().__init__(message)
@@ -22,9 +20,7 @@ def __init__(self, message, status_code):
2220

2321

2422
def generate_access_token(person_id: int) -> str:
25-
"""
26-
Generate a short-lived JWT access token for a user.
27-
"""
23+
"""Generate a short-lived JWT access token for a user."""
2824
payload = {
2925
"person_id": person_id,
3026
"exp": datetime.now(timezone.utc) + JWT_ACCESS_TOKEN_EXPIRY, # Expiration
@@ -35,9 +31,7 @@ def generate_access_token(person_id: int) -> str:
3531

3632

3733
def generate_refresh_token(person_id: int) -> str:
38-
"""
39-
Generate a long-lived refresh token for a user.
40-
"""
34+
"""Generate a long-lived refresh token for a user."""
4135
payload = {
4236
"person_id": person_id,
4337
"exp": datetime.now(timezone.utc) + JWT_REFRESH_TOKEN_EXPIRY,
@@ -48,19 +42,15 @@ def generate_refresh_token(person_id: int) -> str:
4842

4943

5044
def extract_token_from_header() -> str:
51-
"""
52-
Extract the Bearer token from the Authorization header.
53-
"""
45+
"""Extract the Bearer token from the Authorization header."""
5446
auth_header = request.headers.get("Authorization")
5547
if not auth_header or not auth_header.startswith("Bearer "):
5648
raise TokenError("Token is missing or improperly formatted", 401)
5749
return auth_header.split("Bearer ")[1]
5850

5951

6052
def verify_token(token: str, required_type: str) -> dict:
61-
"""
62-
Verify and decode a JWT token.
63-
"""
53+
"""Verify and decode a JWT token."""
6454
try:
6555
decoded = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
6656
if decoded.get("token_type") != required_type:
@@ -73,9 +63,7 @@ def verify_token(token: str, required_type: str) -> dict:
7363

7464

7565
def token_required(f):
76-
"""
77-
Decorator to protect routes by requiring a valid token.
78-
"""
66+
"""Decorator to protect routes by requiring a valid token."""
7967

8068
@wraps(f)
8169
def decorated(*args, **kwargs):

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ flask-cors>=4.0.1
77
PyMySQL>=1.1.1
88
requests>=2.32.3
99
waitress>=3.0.0
10+
pytest>=8.3.5
1011
python-dotenv>=1.0.1

routes/authentication.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
encrypt_email,
1616
hash_email,
1717
hash_password,
18+
validate_email,
1819
validate_password,
1920
verify_password,
2021
)
@@ -47,6 +48,9 @@ def register():
4748
if not name or not email or not password:
4849
return jsonify(message="Username, email, and password are required"), 400
4950

51+
if not validate_email(email):
52+
return jsonify(message="Invalid email address"), 400
53+
5054
if not validate_password(password):
5155
return jsonify(message="Password does not meet security requirements"), 400
5256

routes/picture.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from flask import Blueprint, jsonify, request, send_from_directory
44

5-
from config import PICTURE_FOLDER
5+
from config import Config
66
from jwt_helper import token_required
77
from utility import database_cursor
88

@@ -50,7 +50,7 @@ def get_pictures_by_author(author_id):
5050

5151
@picture_blueprint.route("/<path:filename>", methods=["GET"])
5252
def get_picture(filename):
53-
return send_from_directory(PICTURE_FOLDER, filename)
53+
return send_from_directory(Config.IMAGES_FOLDER, filename)
5454

5555

5656
@picture_blueprint.route("", methods=["POST"])
@@ -80,8 +80,8 @@ def upload_picture():
8080
with database_cursor() as cursor:
8181
cursor.callproc(procedure, (hexname, request.person_id))
8282

83-
fullpath = os.path.normpath(os.path.join(PICTURE_FOLDER, hexname))
84-
if not fullpath.startswith(PICTURE_FOLDER):
83+
fullpath = os.path.abspath(os.path.join(Config.IMAGES_FOLDER, hexname))
84+
if os.path.commonpath([fullpath, Config.IMAGES_FOLDER]) != Config.IMAGES_FOLDER:
8585
return jsonify({"error": "Invalid file path"}), 400
8686
file.save(fullpath)
8787

tests/test_jwt/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)