| """ |
| Security Tests for User Signup Flow |
| |
| Tests comprehensive signup scenarios including: |
| - Valid signup with proper password hashing |
| - Duplicate email prevention |
| - Weak password rejection |
| - Invalid email format validation |
| - Missing required fields |
| - SQL injection prevention |
| - XSS prevention in user input |
| """ |
|
|
| |
| import os |
| os.environ["TESTING"] = "1" |
|
|
| import pytest |
| from sqlalchemy.orm import Session |
| from core.models import User |
| from core.auth import get_password_hash, verify_password |
| from jose import jwt |
|
|
|
|
| class TestSignupWithValidData: |
| """Test user signup with valid data""" |
|
|
| def test_signup_with_valid_data(self, client, db_session: Session): |
| """Test user signup with valid data creates user and returns tokens""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "test@example.com", |
| "password": "SecurePass123!", |
| "first_name": "Test", |
| "last_name": "User" |
| } |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert "access_token" in data |
| assert "token_type" in data |
| assert data["token_type"] == "bearer" |
|
|
| |
| user = db_session.query(User).filter(User.email == "test@example.com").first() |
| assert user is not None |
| assert user.email == "test@example.com" |
| assert user.first_name == "Test" |
| assert user.last_name == "User" |
|
|
| |
| assert user.password_hash is not None |
| assert user.password_hash != "SecurePass123!" |
| assert verify_password("SecurePass123!", user.password_hash) is True |
|
|
| def test_signup_creates_jwt_token(self, client): |
| """Test signup creates valid JWT token""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "jwt@example.com", |
| "password": "SecurePass123!", |
| "first_name": "JWT", |
| "last_name": "Test" |
| } |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| token = data["access_token"] |
|
|
| |
| parts = token.split(".") |
| assert len(parts) == 3 |
|
|
| |
| from core.auth import SECRET_KEY |
| payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) |
| assert "sub" in payload |
| assert "exp" in payload |
|
|
|
|
| class TestSignupWithDuplicateEmail: |
| """Test signup fails with duplicate email""" |
|
|
| def test_signup_with_duplicate_email(self, client, db_session: Session): |
| """Test signup fails with duplicate email""" |
| |
| client.post( |
| "/api/auth/register", |
| json={ |
| "email": "duplicate@example.com", |
| "password": "SecurePass123!", |
| "first_name": "First", |
| "last_name": "User" |
| } |
| ) |
|
|
| |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "duplicate@example.com", |
| "password": "DifferentPass456!", |
| "first_name": "Second", |
| "last_name": "User" |
| } |
| ) |
|
|
| assert response.status_code == 400 |
| assert "email already registered" in response.json()["detail"].lower() |
|
|
| |
| users = db_session.query(User).filter(User.email == "duplicate@example.com").all() |
| assert len(users) == 1 |
|
|
|
|
| class TestSignupWithWeakPassword: |
| """Test signup fails with weak password""" |
|
|
| def test_signup_with_too_short_password(self, client): |
| """Test signup fails with too short password""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "weak@example.com", |
| "password": "short", |
| "first_name": "Weak", |
| "last_name": "Password" |
| } |
| ) |
|
|
| |
| |
| |
| assert response.status_code in [200, 422] |
|
|
| def test_signup_with_no_uppercase(self, client): |
| """Test signup with password lacking uppercase""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "nouppercase@example.com", |
| "password": "lowercase123!", |
| "first_name": "No", |
| "last_name": "Uppercase" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 422] |
|
|
| def test_signup_with_no_numbers(self, client): |
| """Test signup with password lacking numbers""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "nonumbers@example.com", |
| "password": "NoNumbersHere!", |
| "first_name": "No", |
| "last_name": "Numbers" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 422] |
|
|
|
|
| class TestSignupWithInvalidEmail: |
| """Test signup with invalid email format""" |
|
|
| def test_signup_with_missing_email(self, client): |
| """Test signup fails with missing email""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "password": "SecurePass123!", |
| "first_name": "No", |
| "last_name": "Email" |
| } |
| ) |
|
|
| assert response.status_code == 422 |
|
|
| def test_signup_with_invalid_email_format(self, client): |
| """Test signup fails with invalid email format""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "not-an-email", |
| "password": "SecurePass123!", |
| "first_name": "Invalid", |
| "last_name": "Email" |
| } |
| ) |
|
|
| |
| assert response.status_code == 422 |
|
|
| def test_signup_with_empty_email(self, client): |
| """Test signup fails with empty email""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "", |
| "password": "SecurePass123!", |
| "first_name": "Empty", |
| "last_name": "Email" |
| } |
| ) |
|
|
| assert response.status_code == 422 |
|
|
|
|
| class TestSignupWithMissingFields: |
| """Test signup fails with missing required fields""" |
|
|
| def test_signup_with_missing_password(self, client): |
| """Test signup fails with missing password""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "nopass@example.com", |
| "first_name": "No", |
| "last_name": "Password" |
| } |
| ) |
|
|
| assert response.status_code == 422 |
|
|
| def test_signup_with_missing_first_name(self, client): |
| """Test signup fails with missing first name""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "nofirst@example.com", |
| "password": "SecurePass123!", |
| "last_name": "Name" |
| } |
| ) |
|
|
| assert response.status_code == 422 |
|
|
| def test_signup_with_missing_last_name(self, client): |
| """Test signup fails with missing last name""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "nolast@example.com", |
| "password": "SecurePass123!", |
| "first_name": "No" |
| } |
| ) |
|
|
| assert response.status_code == 422 |
|
|
| def test_signup_with_empty_body(self, client): |
| """Test signup fails with empty request body""" |
| response = client.post( |
| "/api/auth/register", |
| json={} |
| ) |
|
|
| assert response.status_code == 422 |
|
|
|
|
| class TestSignupSQLInjectionProtection: |
| """Test SQL injection protection in signup""" |
|
|
| def test_signup_with_sql_injection_in_email(self, client, db_session: Session): |
| """Test signup blocks SQL injection in email field""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "'; DROP TABLE users; --", |
| "password": "SecurePass123!", |
| "first_name": "SQL", |
| "last_name": "Injection" |
| } |
| ) |
|
|
| |
| assert response.status_code in [400, 422, 200] |
|
|
| |
| users_count = db_session.query(User).count() |
| assert users_count >= 0 |
|
|
| def test_signup_with_sql_injection_in_name(self, client, db_session: Session): |
| """Test signup safely handles SQL injection in name fields""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "safe@example.com", |
| "password": "SecurePass123!", |
| "first_name": "'; DROP TABLE users; --", |
| "last_name": "Test" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 422] |
|
|
| |
| users_count = db_session.query(User).count() |
| assert users_count >= 0 |
|
|
|
|
| class TestSignupXSSProtection: |
| """Test XSS protection in signup""" |
|
|
| def test_signup_with_xss_in_name(self, client, db_session: Session): |
| """Test signup safely handles XSS in name fields""" |
| xss_payload = "<script>alert('XSS')</script>" |
|
|
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "xss@example.com", |
| "password": "SecurePass123!", |
| "first_name": xss_payload, |
| "last_name": "Test" |
| } |
| ) |
|
|
| assert response.status_code == 200 |
|
|
| |
| user = db_session.query(User).filter(User.email == "xss@example.com").first() |
| assert user is not None |
| |
| |
| assert user.first_name == xss_payload |
|
|
| def test_signup_xss_not_executed_in_response(self, client): |
| """Test signup response doesn't execute XSS""" |
| xss_payload = "<script>alert('XSS')</script>" |
|
|
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "xss2@example.com", |
| "password": "SecurePass123!", |
| "first_name": xss_payload, |
| "last_name": "Test" |
| } |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.text |
|
|
| |
| |
| assert "<script>" not in data or "\\u003cscript\\u003e" in data |
|
|
|
|
| class TestSignupPasswordHashing: |
| """Test password hashing security""" |
|
|
| def test_password_hashed_with_bcrypt(self, client, db_session: Session): |
| """Test password is hashed with bcrypt""" |
| client.post( |
| "/api/auth/register", |
| json={ |
| "email": "hash@example.com", |
| "password": "SecurePass123!", |
| "first_name": "Hash", |
| "last_name": "Test" |
| } |
| ) |
|
|
| user = db_session.query(User).filter(User.email == "hash@example.com").first() |
| assert user is not None |
|
|
| |
| assert user.password_hash.startswith("$2") |
|
|
| def test_password_hash_uses_salt(self, client, db_session: Session): |
| """Test password hashing uses unique salt""" |
| password = "SecurePass123!" |
|
|
| |
| client.post( |
| "/api/auth/register", |
| json={ |
| "email": "salt1@example.com", |
| "password": password, |
| "first_name": "Salt", |
| "last_name": "One" |
| } |
| ) |
|
|
| client.post( |
| "/api/auth/register", |
| json={ |
| "email": "salt2@example.com", |
| "password": password, |
| "first_name": "Salt", |
| "last_name": "Two" |
| } |
| ) |
|
|
| user1 = db_session.query(User).filter(User.email == "salt1@example.com").first() |
| user2 = db_session.query(User).filter(User.email == "salt2@example.com").first() |
|
|
| |
| assert user1.password_hash != user2.password_hash |
|
|
| |
| assert verify_password(password, user1.password_hash) is True |
| assert verify_password(password, user2.password_hash) is True |
|
|
| def test_password_not_stored_in_plaintext(self, client, db_session: Session): |
| """Test password is never stored in plaintext""" |
| client.post( |
| "/api/auth/register", |
| json={ |
| "email": "plaintext@example.com", |
| "password": "SecurePass123!", |
| "first_name": "No", |
| "last_name": "Plaintext" |
| } |
| ) |
|
|
| user = db_session.query(User).filter(User.email == "plaintext@example.com").first() |
| assert user is not None |
|
|
| |
| assert "SecurePass123!" not in user.password_hash |
| assert user.password_hash != "SecurePass123!" |
| assert len(user.password_hash) == 60 |
|
|
|
|
| class TestSignupTokenGeneration: |
| """Test JWT token generation on signup""" |
|
|
| def test_signup_token_contains_user_id(self, client): |
| """Test signup token contains user ID claim""" |
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "tokenuser@example.com", |
| "password": "SecurePass123!", |
| "first_name": "Token", |
| "last_name": "User" |
| } |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| token = data["access_token"] |
|
|
| from core.auth import SECRET_KEY |
| payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) |
|
|
| assert "sub" in payload |
| assert payload["sub"] is not None |
|
|
| def test_signup_token_has_expiration(self, client): |
| """Test signup token has expiration claim""" |
| import time |
|
|
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "expire@example.com", |
| "password": "SecurePass123!", |
| "first_name": "Expire", |
| "last_name": "Test" |
| } |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| token = data["access_token"] |
|
|
| from core.auth import SECRET_KEY |
| payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) |
|
|
| assert "exp" in payload |
| assert payload["exp"] > time.time() |
|
|
| def test_signup_token_expires_after_24_hours(self, client): |
| """Test signup token expiration time (24 hours default)""" |
| import time |
|
|
| response = client.post( |
| "/api/auth/register", |
| json={ |
| "email": "expire24@example.com", |
| "password": "SecurePass123!", |
| "first_name": "Expire", |
| "last_name": "24h" |
| } |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| token = data["access_token"] |
|
|
| from core.auth import SECRET_KEY |
| payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) |
|
|
| |
| current_time = time.time() |
| exp_time = payload["exp"] |
| time_diff = exp_time - current_time |
|
|
| |
| |
| assert 86400 - 5 <= time_diff <= 86400 + 5 |
|
|