diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..555ce89 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,34 @@ +name: Run Flask Backend Tests + +# Trigger on PRs to master +on: + pull_request: + branches: + - master + +jobs: + tests: + runs-on: ubuntu-latest + + steps: + # Step 1: Checkout repo + - name: Checkout code + uses: actions/checkout@v4 + + # Step 2: Set up Python + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + # Step 3: Install dependencies + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r backend/requirements.txt + + # Step 4: Run tests + - name: Run pytest + working-directory: backend + run: | + pytest -v \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app.py b/backend/app.py index a32ca96..6a2ef72 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,17 +1,23 @@ -from config import Config -from extensions import db, jwt, migrate +from backend.config import Config +from backend.extensions import db, jwt, migrate from flask import Flask -def create_app(): +def create_app(config_object=None): app = Flask(__name__) + + # Default config app.config.from_object(Config) + # Override config (for tests) + if config_object: + app.config.update(config_object) + db.init_app(app) jwt.init_app(app) migrate.init_app(app, db) - from routes.auth_routes import auth_bp + from backend.routes.auth_routes import auth_bp app.register_blueprint(auth_bp) diff --git a/backend/models/user.py b/backend/models/user.py index cb96997..014f866 100644 --- a/backend/models/user.py +++ b/backend/models/user.py @@ -1,7 +1,7 @@ import string import uuid -from extensions import db +from backend.extensions import db from werkzeug.security import check_password_hash, generate_password_hash diff --git a/backend/models/user_stats.py b/backend/models/user_stats.py index 9391406..80a7cf3 100644 --- a/backend/models/user_stats.py +++ b/backend/models/user_stats.py @@ -1,4 +1,4 @@ -from extensions import db +from backend.extensions import db class UserStats(db.Model): diff --git a/backend/requirements.txt b/backend/requirements.txt index 1056346..3e91890 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,11 +3,13 @@ black==25.11.0 blinker==1.9.0 click==8.1.8 colorama==0.4.6 +exceptiongroup==1.3.1 Flask==3.1.2 Flask-JWT-Extended==4.7.1 Flask-Migrate==4.1.0 Flask-SQLAlchemy==3.1.1 importlib_metadata==8.7.1 +iniconfig==2.1.0 itsdangerous==2.2.0 Jinja2==3.1.6 Mako==1.3.10 @@ -16,7 +18,12 @@ mypy_extensions==1.1.0 packaging==26.0 pathspec==1.0.4 platformdirs==4.4.0 +pluggy==1.6.0 +psycopg2-binary==2.9.11 +Pygments==2.19.2 PyJWT==2.10.1 +pytest==8.4.2 +pytest-flask==1.3.0 python-dotenv==1.2.1 pytokens==0.4.1 SQLAlchemy==2.0.46 @@ -24,4 +31,3 @@ tomli==2.4.0 typing_extensions==4.15.0 Werkzeug==3.1.5 zipp==3.23.0 -psycopg2-binary diff --git a/backend/routes/auth_routes.py b/backend/routes/auth_routes.py index 6c98fb2..a41ecd4 100644 --- a/backend/routes/auth_routes.py +++ b/backend/routes/auth_routes.py @@ -1,8 +1,8 @@ import logging -from extensions import db +from backend.extensions import db from flask import Blueprint, jsonify, request -from models.user import User -from models.user_stats import UserStats +from backend.models.user import User +from backend.models.user_stats import UserStats from werkzeug.security import generate_password_hash # Auth Blueprint diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..ab5c239 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,32 @@ +import json +import pytest +from backend.app import create_app +from backend.extensions import db + + +@pytest.fixture(scope="session") +def test_data(): + with open("tests/data/users.json") as f: + return json.load(f) + + +@pytest.fixture +def app(): + app = create_app( + { + "TESTING": True, + "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:", + "SQLALCHEMY_TRACK_MODIFICATIONS": False, + } + ) + + with app.app_context(): + db.create_all() + yield app + db.session.remove() + db.drop_all() + + +@pytest.fixture +def client(app): + return app.test_client() diff --git a/backend/tests/data/users.json b/backend/tests/data/users.json new file mode 100644 index 0000000..10f9760 --- /dev/null +++ b/backend/tests/data/users.json @@ -0,0 +1,17 @@ +{ + "valid_user": { + "username": "testuser", + "email": "testuser@example.com", + "password": "StrongP@ssw0rd" + }, + "weak_password": { + "username": "weakuser", + "email": "weak@example.com", + "password": "weak" + }, + "missing_fields": { + "username": "", + "email": "missing@example.com", + "password": "" + } + } \ No newline at end of file diff --git a/backend/tests/test_auth_register.py b/backend/tests/test_auth_register.py new file mode 100644 index 0000000..d8108ae --- /dev/null +++ b/backend/tests/test_auth_register.py @@ -0,0 +1,30 @@ +def test_register_success(client, test_data): + res = client.post("/register", json=test_data["valid_user"]) + + assert res.status_code == 201 + data = res.get_json() + assert "User registered successfully" in data["msg"] + + +def test_register_weak_password(client, test_data): + res = client.post("/register", json=test_data["weak_password"]) + + assert res.status_code == 400 + data = res.get_json() + assert "password" in data["error"].lower() + + +def test_register_missing_fields(client, test_data): + res = client.post("/register", json=test_data["missing_fields"]) + + assert res.status_code == 400 + + +def test_register_duplicate_username(client, test_data): + client.post("/register", json=test_data["valid_user"]) + + res = client.post( + "/register", json={**test_data["valid_user"], "email": "another@example.com"} + ) + + assert res.status_code == 409