"""
Security Tests for User Login Flow
Tests comprehensive login scenarios including:
- Valid login with correct credentials
- Invalid login with wrong email/password
- SQL injection protection in login
- XSS protection in login responses
- Account status validation (active/inactive)
- Login rate limiting (if implemented)
"""
# Set TESTING before any imports
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, SECRET_KEY
from jose import jwt
class TestLoginWithValidCredentials:
"""Test user login with valid credentials"""
def test_login_with_valid_credentials(self, client, db_session: Session):
"""Test login returns JWT token for valid credentials"""
# Create test user
user = User(
email="login@example.com",
password_hash=get_password_hash("SecurePass123!"),
first_name="Login",
last_name="Test",
status="active"
)
db_session.add(user)
db_session.commit()
response = client.post(
"/api/auth/login",
json={
"username": "login@example.com",
"password": "SecurePass123!"
}
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert data["token_type"] == "bearer"
# Verify token is valid JWT
token = data["access_token"]
parts = token.split(".")
assert len(parts) == 3
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
assert "sub" in payload
assert "exp" in payload
def test_login_updates_last_login_timestamp(self, client, db_session: Session):
"""Test login updates user's last_login timestamp"""
from datetime import datetime
user = User(
email="timestamp@example.com",
password_hash=get_password_hash("SecurePass123!"),
first_name="Time",
last_name="Stamp",
status="active"
)
db_session.add(user)
db_session.commit()
# Get initial last_login
initial_last_login = user.last_login
import time
time.sleep(1) # Ensure timestamp difference
response = client.post(
"/api/auth/login",
json={
"username": "timestamp@example.com",
"password": "SecurePass123!"
}
)
assert response.status_code == 200
# Refresh and check last_login was updated
db_session.refresh(user)
assert user.last_login is not None
if initial_last_login:
assert user.last_login > initial_last_login
class TestLoginWithInvalidCredentials:
"""Test login fails with invalid credentials"""
def test_login_with_wrong_email(self, client, db_session: Session):
"""Test login fails with non-existent email"""
response = client.post(
"/api/auth/login",
json={
"username": "nonexistent@example.com",
"password": "SecurePass123!"
}
)
assert response.status_code == 401
assert "incorrect" in response.json()["detail"].lower()
def test_login_with_wrong_password(self, client, db_session: Session):
"""Test login fails with wrong password"""
user = User(
email="wrongpass@example.com",
password_hash=get_password_hash("CorrectPass123!"),
first_name="Wrong",
last_name="Pass",
status="active"
)
db_session.add(user)
db_session.commit()
response = client.post(
"/api/auth/login",
json={
"username": "wrongpass@example.com",
"password": "WrongPass123!"
}
)
assert response.status_code == 401
assert "incorrect" in response.json()["detail"].lower()
def test_login_with_empty_password(self, client, db_session: Session):
"""Test login fails with empty password"""
user = User(
email="empty@example.com",
password_hash=get_password_hash("SecurePass123!"),
first_name="Empty",
last_name="Pass",
status="active"
)
db_session.add(user)
db_session.commit()
response = client.post(
"/api/auth/login",
json={
"username": "empty@example.com",
"password": ""
}
)
# Should fail - either validation or auth error
assert response.status_code in [401, 422]
def test_login_with_missing_fields(self, client):
"""Test login fails with missing required fields"""
response = client.post(
"/api/auth/login",
json={}
)
assert response.status_code == 422 # Validation error
class TestLoginSQLInjectionProtection:
"""Test SQL injection protection in login"""
def test_login_with_sql_injection_in_email(self, client, db_session: Session):
"""Test login blocks SQL injection in email field"""
response = client.post(
"/api/auth/login",
json={
"username": "'; DROP TABLE users; --",
"password": "password"
}
)
# Should return auth error
assert response.status_code == 401
# Verify users table still exists
users_count = db_session.query(User).count()
assert users_count >= 0 # Table not dropped
def test_login_with_sql_injection_in_password(self, client, db_session: Session):
"""Test login safely handles SQL injection in password"""
user = User(
email="safepass@example.com",
password_hash=get_password_hash("RealPassword123!"),
first_name="Safe",
last_name="Pass",
status="active"
)
db_session.add(user)
db_session.commit()
response = client.post(
"/api/auth/login",
json={
"username": "safepass@example.com",
"password": "' OR '1'='1"
}
)
# Should not authenticate with SQL injection
assert response.status_code == 401
def test_login_with_union_based_injection(self, client, db_session: Session):
"""Test login blocks UNION-based SQL injection"""
response = client.post(
"/api/auth/login",
json={
"username": "admin' UNION SELECT * FROM users--",
"password": "password"
}
)
# Should return auth error, not database error
assert response.status_code == 401
class TestLoginXSSProtection:
"""Test XSS protection in login responses"""
def test_login_xss_in_error_messages(self, client, db_session: Session):
"""Test login error messages are XSS-safe"""
xss_email = "@example.com"
response = client.post(
"/api/auth/login",
json={
"username": xss_email,
"password": "password"
}
)
# Should fail authentication
assert response.status_code == 401
# Error message should be JSON-encoded, not executable
data = response.text
# Response should be JSON, not HTML with script tags
assert "