From f62310f1ad570e5df8320e96cddbe3d9bac387b0 Mon Sep 17 00:00:00 2001 From: krishnachinta Date: Tue, 23 Sep 2025 13:35:51 -0500 Subject: [PATCH 01/15] initial commit --- .idea/.gitignore | 3 + .idea/vcs.xml | 4 + backend/tests/test_api_integration.py | 70 ++ backend/tests/test_api_middleware.py | 35 + backend/tests/test_authentication.py | 28 + backend/tests/test_authorization.py | 29 + backend/tests/test_config.py | 26 + backend/tests/test_crud_operations.py | 38 + backend/tests/test_database.py | 36 + backend/tests/test_email_service.py | 45 ++ backend/tests/test_error_handling.py | 42 ++ backend/tests/test_external_services.py | 51 ++ backend/tests/test_file_handling.py | 49 ++ backend/tests/test_input_validation.py | 38 + backend/tests/test_items_api.py | 40 ++ backend/tests/test_logging.py | 42 ++ backend/tests/test_main.py | 28 + backend/tests/test_models_validation.py | 38 + backend/tests/test_pagination.py | 40 ++ backend/tests/test_password_recovery.py | 41 ++ backend/tests/test_performance.py | 58 ++ backend/tests/test_security.py | 28 + .../tests/test_security_vulnerabilities.py | 58 ++ backend/tests/test_users_api.py | 38 + backend/tests/test_utils.py | 31 + script1_model_a_init.py | 122 ++++ script2_model_b_init.py | 260 +++++++ script3_model_b_capture.py | 250 +++++++ set_api_key.sh | 22 + utils.py | 668 ++++++++++++++++++ 30 files changed, 2258 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/vcs.xml create mode 100644 backend/tests/test_api_integration.py create mode 100644 backend/tests/test_api_middleware.py create mode 100644 backend/tests/test_authentication.py create mode 100644 backend/tests/test_authorization.py create mode 100644 backend/tests/test_config.py create mode 100644 backend/tests/test_crud_operations.py create mode 100644 backend/tests/test_database.py create mode 100644 backend/tests/test_email_service.py create mode 100644 backend/tests/test_error_handling.py create mode 100644 backend/tests/test_external_services.py create mode 100644 backend/tests/test_file_handling.py create mode 100644 backend/tests/test_input_validation.py create mode 100644 backend/tests/test_items_api.py create mode 100644 backend/tests/test_logging.py create mode 100644 backend/tests/test_main.py create mode 100644 backend/tests/test_models_validation.py create mode 100644 backend/tests/test_pagination.py create mode 100644 backend/tests/test_password_recovery.py create mode 100644 backend/tests/test_performance.py create mode 100644 backend/tests/test_security.py create mode 100644 backend/tests/test_security_vulnerabilities.py create mode 100644 backend/tests/test_users_api.py create mode 100644 backend/tests/test_utils.py create mode 100644 script1_model_a_init.py create mode 100644 script2_model_b_init.py create mode 100644 script3_model_b_capture.py create mode 100644 set_api_key.sh create mode 100644 utils.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..26d33521af --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..d843f340d2 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/backend/tests/test_api_integration.py b/backend/tests/test_api_integration.py new file mode 100644 index 0000000000..0069e9540b --- /dev/null +++ b/backend/tests/test_api_integration.py @@ -0,0 +1,70 @@ +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch, MagicMock +from app.main import app +import json + +client = TestClient(app) + +def test_api_versioning(): + """Test API versioning and backward compatibility.""" + # Test current API version + response = client.get("/api/v1/utils/health-check/") + assert response.status_code == 200 + + # Test that v1 endpoints are available + assert "/api/v1/" in str(response.url) or response.status_code == 200 + +def test_openapi_schema_validation(): + """Test OpenAPI schema is valid and complete.""" + response = client.get("/openapi.json") + assert response.status_code == 200 + + schema = response.json() + assert "openapi" in schema + assert "info" in schema + assert "paths" in schema + +def test_api_documentation_accessibility(): + """Test that API documentation is accessible.""" + docs_response = client.get("/docs") + redoc_response = client.get("/redoc") + + # At least one documentation endpoint should be available + assert docs_response.status_code == 200 or redoc_response.status_code == 200 + +def test_health_monitoring_endpoints(): + """Test health monitoring and status endpoints.""" + health_response = client.get("/api/v1/utils/health-check/") + assert health_response.status_code == 200 + + # Verify health check response format + data = health_response.json() + assert isinstance(data, dict) + +def test_api_error_response_format(): + """Test that API errors follow consistent format.""" + # Make request to non-existent endpoint + response = client.get("/api/v1/nonexistent") + assert response.status_code == 404 + + # Verify error response is JSON + try: + error_data = response.json() + assert isinstance(error_data, dict) + except json.JSONDecodeError: + # Some APIs might return non-JSON 404s, which is also acceptable + pass + +def test_request_id_tracking(): + """Test request ID tracking for debugging.""" + response = client.get("/api/v1/utils/health-check/") + + # Check if request ID is present in headers (implementation dependent) + request_id_headers = [ + "x-request-id", "request-id", "x-trace-id" + ] + + has_request_id = any(header in response.headers for header in request_id_headers) + # This test is optional - not all APIs implement request ID tracking + assert response.status_code == 200 # Main assertion is that the endpoint works diff --git a/backend/tests/test_api_middleware.py b/backend/tests/test_api_middleware.py new file mode 100644 index 0000000000..273cf686ab --- /dev/null +++ b/backend/tests/test_api_middleware.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +def test_cors_headers(): + """Test CORS headers are properly set.""" + response = client.options("/api/v1/users/me") + assert response.status_code in [200, 405] + # Check if CORS headers might be present + if "access-control-allow-origin" in response.headers: + assert response.headers["access-control-allow-origin"] + +def test_api_version_consistency(): + """Test that all API endpoints use consistent versioning.""" + # Test health check endpoint version + response = client.get("/api/v1/utils/health-check/") + assert response.status_code == 200 + +def test_content_type_headers(): + """Test that API returns proper content-type headers.""" + response = client.get("/api/v1/utils/health-check/") + assert "application/json" in response.headers.get("content-type", "") + +def test_response_time_performance(): + """Test basic response time performance.""" + import time + start_time = time.time() + response = client.get("/api/v1/utils/health-check/") + end_time = time.time() + + assert response.status_code == 200 + # Response should be under 1 second for health check + assert (end_time - start_time) < 1.0 diff --git a/backend/tests/test_authentication.py b/backend/tests/test_authentication.py new file mode 100644 index 0000000000..f1fe588b6f --- /dev/null +++ b/backend/tests/test_authentication.py @@ -0,0 +1,28 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +def test_login_valid_credentials(): + """Test login with valid credentials.""" + response = client.post( + "/api/v1/login/access-token", + data={"username": "test@example.com", "password": "testpassword"} + ) + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + +def test_login_invalid_credentials(): + """Test login with invalid credentials.""" + response = client.post( + "/api/v1/login/access-token", + data={"username": "invalid@example.com", "password": "wrongpassword"} + ) + assert response.status_code == 400 + +def test_protected_route_without_token(): + """Test accessing protected route without token.""" + response = client.get("/api/v1/users/me") + assert response.status_code == 401 diff --git a/backend/tests/test_authorization.py b/backend/tests/test_authorization.py new file mode 100644 index 0000000000..8a3f12c018 --- /dev/null +++ b/backend/tests/test_authorization.py @@ -0,0 +1,29 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +def test_unauthorized_access(): + """Test accessing protected endpoints without authentication.""" + response = client.get("/api/v1/users/me") + assert response.status_code == 401 + +def test_invalid_token_format(): + """Test using malformed authorization token.""" + headers = {"Authorization": "InvalidTokenFormat"} + response = client.get("/api/v1/users/me", headers=headers) + assert response.status_code == 401 + +def test_expired_token(): + """Test using expired token.""" + # This would require creating an expired token + headers = {"Authorization": "Bearer expired_token_here"} + response = client.get("/api/v1/users/me", headers=headers) + assert response.status_code == 401 + +def test_insufficient_permissions(): + """Test accessing admin endpoints with regular user token.""" + headers = {"Authorization": "Bearer regular_user_token"} + response = client.get("/api/v1/admin/users/", headers=headers) + assert response.status_code in [401, 403, 404] diff --git a/backend/tests/test_config.py b/backend/tests/test_config.py new file mode 100644 index 0000000000..f427028800 --- /dev/null +++ b/backend/tests/test_config.py @@ -0,0 +1,26 @@ +import pytest +from unittest.mock import patch +from app.core.config import Settings + +def test_settings_initialization(): + """Test settings class initialization.""" + settings = Settings() + assert hasattr(settings, 'SECRET_KEY') + assert hasattr(settings, 'PROJECT_NAME') + +def test_database_url_construction(): + """Test database URL construction.""" + settings = Settings() + # Should have postgres in the URL + assert 'postgresql' in str(settings.SQLALCHEMY_DATABASE_URI) + +def test_cors_origins_parsing(): + """Test CORS origins parsing.""" + with patch.dict('os.environ', {'BACKEND_CORS_ORIGINS': '["http://localhost:3000", "http://localhost:8000"]'}): + settings = Settings() + assert isinstance(settings.BACKEND_CORS_ORIGINS, list) + +def test_environment_validation(): + """Test environment variable validation.""" + settings = Settings() + assert settings.ENVIRONMENT in ['local', 'staging', 'production'] diff --git a/backend/tests/test_crud_operations.py b/backend/tests/test_crud_operations.py new file mode 100644 index 0000000000..5b645e1ca2 --- /dev/null +++ b/backend/tests/test_crud_operations.py @@ -0,0 +1,38 @@ +import pytest +from sqlalchemy.orm import Session +from app.models import User, Item +from app.crud import create_user, get_user, update_user, delete_user + +def test_create_user_crud(db: Session): + """Test user creation through CRUD operations.""" + user_data = { + "email": "crud@example.com", + "password": "testpassword", + "full_name": "CRUD User" + } + user = create_user(db, user_data) + assert user.email == "crud@example.com" + assert user.full_name == "CRUD User" + +def test_get_user_by_email(db: Session): + """Test retrieving user by email.""" + # First create a user + user_data = { + "email": "getuser@example.com", + "password": "testpassword" + } + created_user = create_user(db, user_data) + + # Then retrieve it + retrieved_user = get_user(db, email="getuser@example.com") + assert retrieved_user is not None + assert retrieved_user.email == "getuser@example.com" + +def test_update_user_crud(db: Session): + """Test user update through CRUD operations.""" + user_data = {"email": "update@example.com", "password": "test"} + user = create_user(db, user_data) + + update_data = {"full_name": "Updated Name"} + updated_user = update_user(db, user.id, update_data) + assert updated_user.full_name == "Updated Name" diff --git a/backend/tests/test_database.py b/backend/tests/test_database.py new file mode 100644 index 0000000000..348655cc7d --- /dev/null +++ b/backend/tests/test_database.py @@ -0,0 +1,36 @@ +import pytest +from sqlalchemy.orm import Session +from app.core.db import get_db +from app.models import User +from app.utils import generate_password_reset_token, verify_password_reset_token + +def test_database_connection(db: Session): + """Test database connection is working.""" + result = db.execute("SELECT 1") + assert result.fetchone()[0] == 1 + +def test_user_model_creation(db: Session): + """Test creating a user in the database.""" + user_data = { + "email": "test@example.com", + "hashed_password": "hashedpassword123", + "full_name": "Test User" + } + user = User(**user_data) + db.add(user) + db.commit() + db.refresh(user) + + assert user.id is not None + assert user.email == "test@example.com" + assert user.full_name == "Test User" + +def test_database_rollback(db: Session): + """Test database rollback functionality.""" + user = User(email="rollback@example.com", hashed_password="test123") + db.add(user) + db.rollback() + + # User should not exist after rollback + user_check = db.query(User).filter(User.email == "rollback@example.com").first() + assert user_check is None diff --git a/backend/tests/test_email_service.py b/backend/tests/test_email_service.py new file mode 100644 index 0000000000..f31cccf4c4 --- /dev/null +++ b/backend/tests/test_email_service.py @@ -0,0 +1,45 @@ +import pytest +from unittest.mock import patch, MagicMock +from app.utils import send_email + +def test_email_service_integration(): + """Test email service integration.""" + with patch('smtplib.SMTP') as mock_smtp: + mock_server = MagicMock() + mock_smtp.return_value = mock_server + + result = send_email( + email_to="test@example.com", + subject="Test Subject", + html_content="

Test email

" + ) + + mock_smtp.assert_called_once() + +@patch('app.core.config.settings.SMTP_HOST', 'localhost') +def test_email_configuration(): + """Test email configuration settings.""" + from app.core.config import settings + assert settings.SMTP_HOST == 'localhost' + +def test_email_template_rendering(): + """Test email template rendering with variables.""" + template_data = { + "username": "Test User", + "reset_link": "https://example.com/reset" + } + + # Mock template rendering + with patch('app.email_templates.render_template') as mock_render: + mock_render.return_value = "

Hello Test User

" + result = mock_render(template_data) + assert "Test User" in result + +def test_email_sending_failure(): + """Test handling email sending failures.""" + with patch('smtplib.SMTP') as mock_smtp: + mock_smtp.side_effect = Exception("SMTP connection failed") + + result = send_email("test@example.com", "Test", "

Test

") + # Should handle the error gracefully + assert result is False or result is None diff --git a/backend/tests/test_error_handling.py b/backend/tests/test_error_handling.py new file mode 100644 index 0000000000..c9dbbdc281 --- /dev/null +++ b/backend/tests/test_error_handling.py @@ -0,0 +1,42 @@ +import pytest +from unittest.mock import patch, MagicMock +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session +from app.models import User + +def test_database_integrity_error(db: Session): + """Test database integrity constraints.""" + # Create first user + user1 = User(email="duplicate@example.com", hashed_password="test123") + db.add(user1) + db.commit() + + # Try to create duplicate email (should fail) + user2 = User(email="duplicate@example.com", hashed_password="test456") + db.add(user2) + + with pytest.raises(IntegrityError): + db.commit() + +def test_database_connection_error(): + """Test handling database connection errors.""" + with patch('app.core.db.SessionLocal') as mock_session: + mock_session.side_effect = Exception("Database connection failed") + + # Test that the error is properly handled + with pytest.raises(Exception): + mock_session() + +def test_transaction_rollback_on_error(db: Session): + """Test that transactions are properly rolled back on errors.""" + try: + user = User(email="error@example.com", hashed_password="test123") + db.add(user) + # Simulate an error + raise Exception("Simulated error") + except Exception: + db.rollback() + + # Verify the user was not saved + user_check = db.query(User).filter(User.email == "error@example.com").first() + assert user_check is None diff --git a/backend/tests/test_external_services.py b/backend/tests/test_external_services.py new file mode 100644 index 0000000000..e8ae540b04 --- /dev/null +++ b/backend/tests/test_external_services.py @@ -0,0 +1,51 @@ +import pytest +from unittest.mock import patch, MagicMock +from app.core.config import settings + +def test_external_api_integration(): + """Test integration with external APIs.""" + with patch('requests.get') as mock_get: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "ok"} + mock_get.return_value = mock_response + + # Simulate external API call + import requests + response = requests.get("https://api.example.com/status") + assert response.status_code == 200 + +def test_third_party_service_timeout(): + """Test handling of third-party service timeouts.""" + with patch('requests.get') as mock_get: + mock_get.side_effect = requests.exceptions.Timeout("Request timed out") + + try: + import requests + requests.get("https://api.example.com/status", timeout=5) + except requests.exceptions.Timeout: + # Should handle timeout gracefully + assert True + +def test_api_rate_limiting(): + """Test API rate limiting behavior.""" + with patch('requests.get') as mock_get: + mock_response = MagicMock() + mock_response.status_code = 429 # Too Many Requests + mock_get.return_value = mock_response + + import requests + response = requests.get("https://api.example.com/data") + assert response.status_code == 429 + +def test_service_health_check(): + """Test external service health checks.""" + with patch('requests.get') as mock_get: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"healthy": True} + mock_get.return_value = mock_response + + import requests + response = requests.get("https://service.example.com/health") + assert response.json()["healthy"] is True diff --git a/backend/tests/test_file_handling.py b/backend/tests/test_file_handling.py new file mode 100644 index 0000000000..d381575af8 --- /dev/null +++ b/backend/tests/test_file_handling.py @@ -0,0 +1,49 @@ +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch, MagicMock +from app.main import app + +client = TestClient(app) + +def test_file_upload_validation(): + """Test file upload validation and security.""" + # Test file size limits + large_file_data = {"file": ("test.txt", b"x" * 1000000)} # 1MB file + response = client.post("/api/v1/upload/", files=large_file_data) + # Should handle large files appropriately + assert response.status_code in [200, 413, 422] + +def test_file_type_validation(): + """Test file type validation.""" + # Test with potentially dangerous file type + malicious_file = {"file": ("script.exe", b"MZ\x90\x00")} + response = client.post("/api/v1/upload/", files=malicious_file) + # Should reject executable files + assert response.status_code in [400, 422] + +def test_image_processing(): + """Test image file processing capabilities.""" + with patch('PIL.Image.open') as mock_image: + mock_img = MagicMock() + mock_img.size = (100, 100) + mock_image.return_value = mock_img + + # Simulate image upload + image_file = {"file": ("test.jpg", b"\xff\xd8\xff\xe0")} # JPEG header + response = client.post("/api/v1/upload/", files=image_file) + + # Should process image successfully + assert response.status_code in [200, 404] # 404 if endpoint doesn't exist + +def test_file_storage_integration(): + """Test file storage integration.""" + with patch('os.path.exists') as mock_exists: + with patch('builtins.open', create=True) as mock_open: + mock_exists.return_value = True + mock_file = MagicMock() + mock_open.return_value.__enter__.return_value = mock_file + + # Test file retrieval + response = client.get("/api/v1/files/test-file.txt") + # Should handle file retrieval appropriately + assert response.status_code in [200, 404] diff --git a/backend/tests/test_input_validation.py b/backend/tests/test_input_validation.py new file mode 100644 index 0000000000..662bc14787 --- /dev/null +++ b/backend/tests/test_input_validation.py @@ -0,0 +1,38 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +def test_invalid_json_payload(): + """Test API response to invalid JSON payload.""" + headers = {"Content-Type": "application/json"} + response = client.post("/api/v1/login/access-token", + data="invalid json", headers=headers) + assert response.status_code == 422 + +def test_missing_required_fields(): + """Test API response when required fields are missing.""" + incomplete_data = {"email": "test@example.com"} # Missing password + response = client.post("/api/v1/login/access-token", json=incomplete_data) + assert response.status_code == 422 + +def test_field_validation_email(): + """Test email field validation.""" + invalid_data = {"email": "not-an-email", "password": "test123"} + response = client.post("/api/v1/users/", json=invalid_data) + assert response.status_code == 422 + +def test_field_validation_password_length(): + """Test password length validation.""" + short_password_data = {"email": "test@example.com", "password": "123"} + response = client.post("/api/v1/users/", json=short_password_data) + assert response.status_code == 422 + +def test_response_schema_validation(): + """Test that API responses match expected schema.""" + response = client.get("/api/v1/utils/health-check/") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, dict) + assert "message" in data diff --git a/backend/tests/test_items_api.py b/backend/tests/test_items_api.py new file mode 100644 index 0000000000..2759c7dfb7 --- /dev/null +++ b/backend/tests/test_items_api.py @@ -0,0 +1,40 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app +from app.core.security import create_access_token + +client = TestClient(app) + +def test_create_item(): + """Test creating a new item.""" + token = create_access_token(subject="test@example.com") + headers = {"Authorization": f"Bearer {token}"} + + item_data = { + "title": "Test Item", + "description": "Test Description" + } + + response = client.post("/api/v1/items/", json=item_data, headers=headers) + assert response.status_code == 200 + data = response.json() + assert data["title"] == "Test Item" + +def test_get_items(): + """Test retrieving items list.""" + token = create_access_token(subject="test@example.com") + headers = {"Authorization": f"Bearer {token}"} + + response = client.get("/api/v1/items/", headers=headers) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + +def test_get_item_by_id(): + """Test retrieving a specific item by ID.""" + token = create_access_token(subject="test@example.com") + headers = {"Authorization": f"Bearer {token}"} + + response = client.get("/api/v1/items/1", headers=headers) + # Should return 200 if item exists, 404 if not + assert response.status_code in [200, 404] diff --git a/backend/tests/test_logging.py b/backend/tests/test_logging.py new file mode 100644 index 0000000000..7ed4d81fcc --- /dev/null +++ b/backend/tests/test_logging.py @@ -0,0 +1,42 @@ +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch +from app.main import app + +client = TestClient(app) + +def test_logging_configuration(): + """Test that logging is properly configured.""" + import logging + logger = logging.getLogger("app") + assert logger.level <= logging.INFO + +def test_request_logging(): + """Test that API requests are logged.""" + with patch('app.main.logger') as mock_logger: + response = client.get("/api/v1/utils/health-check/") + assert response.status_code == 200 + # Verify logging was called (implementation dependent) + +def test_error_logging(): + """Test that errors are properly logged.""" + with patch('app.main.logger') as mock_logger: + # Make a request that might cause an error + response = client.get("/api/v1/nonexistent-endpoint") + assert response.status_code == 404 + +def test_log_level_filtering(): + """Test log level filtering works correctly.""" + import logging + + # Test different log levels + logger = logging.getLogger("test") + logger.setLevel(logging.WARNING) + + with patch.object(logger, 'info') as mock_info: + with patch.object(logger, 'warning') as mock_warning: + logger.info("This should not be logged") + logger.warning("This should be logged") + + mock_info.assert_called_once() + mock_warning.assert_called_once() diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py new file mode 100644 index 0000000000..262c12cdde --- /dev/null +++ b/backend/tests/test_main.py @@ -0,0 +1,28 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +def test_health_check(): + """Test the health check endpoint.""" + response = client.get("/api/v1/utils/health-check/") + assert response.status_code == 200 + assert response.json() == {"message": "OK"} + +def test_root_endpoint(): + """Test the root endpoint redirect.""" + response = client.get("/") + # Should redirect to docs or return some response + assert response.status_code in [200, 307, 308] + +def test_docs_endpoint(): + """Test the OpenAPI docs endpoint.""" + response = client.get("/docs") + assert response.status_code == 200 + +def test_openapi_json(): + """Test the OpenAPI JSON schema endpoint.""" + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" diff --git a/backend/tests/test_models_validation.py b/backend/tests/test_models_validation.py new file mode 100644 index 0000000000..c94070013f --- /dev/null +++ b/backend/tests/test_models_validation.py @@ -0,0 +1,38 @@ +import pytest +from pydantic import ValidationError +from app.models import User, Item + +def test_user_model_validation(): + """Test user model field validation.""" + # Valid user data + valid_data = { + "email": "test@example.com", + "hashed_password": "hashedpass123", + "full_name": "Test User" + } + user = User(**valid_data) + assert user.email == "test@example.com" + +def test_user_model_invalid_email(): + """Test user model with invalid email.""" + with pytest.raises(ValidationError): + User(email="invalid-email", hashed_password="test123") + +def test_item_model_creation(): + """Test item model creation and validation.""" + item_data = { + "title": "Test Item", + "description": "Test Description", + "owner_id": 1 + } + item = Item(**item_data) + assert item.title == "Test Item" + assert item.owner_id == 1 + +def test_model_relationships(): + """Test model relationships and foreign keys.""" + user = User(email="owner@example.com", hashed_password="test123") + item = Item(title="User's Item", owner_id=1) + + assert item.owner_id == 1 + assert item.title == "User's Item" diff --git a/backend/tests/test_pagination.py b/backend/tests/test_pagination.py new file mode 100644 index 0000000000..630846091f --- /dev/null +++ b/backend/tests/test_pagination.py @@ -0,0 +1,40 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app +from app.core.security import create_access_token + +client = TestClient(app) + +def test_pagination_default_params(): + """Test pagination with default parameters.""" + token = create_access_token(subject="test@example.com") + headers = {"Authorization": f"Bearer {token}"} + + response = client.get("/api/v1/items/", headers=headers) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) or "items" in data + +def test_pagination_custom_limit(): + """Test pagination with custom limit parameter.""" + token = create_access_token(subject="test@example.com") + headers = {"Authorization": f"Bearer {token}"} + + response = client.get("/api/v1/items/?limit=5", headers=headers) + assert response.status_code == 200 + +def test_pagination_skip_parameter(): + """Test pagination with skip parameter.""" + token = create_access_token(subject="test@example.com") + headers = {"Authorization": f"Bearer {token}"} + + response = client.get("/api/v1/items/?skip=10&limit=5", headers=headers) + assert response.status_code == 200 + +def test_pagination_invalid_parameters(): + """Test pagination with invalid parameters.""" + token = create_access_token(subject="test@example.com") + headers = {"Authorization": f"Bearer {token}"} + + response = client.get("/api/v1/items/?limit=-1", headers=headers) + assert response.status_code in [200, 422] # Should handle invalid params gracefully diff --git a/backend/tests/test_password_recovery.py b/backend/tests/test_password_recovery.py new file mode 100644 index 0000000000..d36ea8e328 --- /dev/null +++ b/backend/tests/test_password_recovery.py @@ -0,0 +1,41 @@ +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch, MagicMock +from app.main import app + +client = TestClient(app) + +def test_password_recovery_request(): + """Test requesting password recovery.""" + recovery_data = {"email": "test@example.com"} + response = client.post("/api/v1/password-recovery/", json=recovery_data) + assert response.status_code in [200, 404] + +@patch('app.utils.send_email') +def test_password_recovery_email_sent(mock_send_email): + """Test that password recovery email is sent.""" + mock_send_email.return_value = True + + recovery_data = {"email": "test@example.com"} + response = client.post("/api/v1/password-recovery/", json=recovery_data) + + if response.status_code == 200: + mock_send_email.assert_called_once() + +def test_reset_password_with_token(): + """Test resetting password with valid token.""" + reset_data = { + "token": "valid_reset_token", + "new_password": "newpassword123" + } + response = client.post("/api/v1/reset-password/", json=reset_data) + assert response.status_code in [200, 400] + +def test_reset_password_invalid_token(): + """Test resetting password with invalid token.""" + reset_data = { + "token": "invalid_token", + "new_password": "newpassword123" + } + response = client.post("/api/v1/reset-password/", json=reset_data) + assert response.status_code == 400 diff --git a/backend/tests/test_performance.py b/backend/tests/test_performance.py new file mode 100644 index 0000000000..09bb4b1938 --- /dev/null +++ b/backend/tests/test_performance.py @@ -0,0 +1,58 @@ +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch +from app.main import app +import time + +client = TestClient(app) + +def test_api_response_time(): + """Test API response time is within acceptable limits.""" + start_time = time.time() + response = client.get("/api/v1/utils/health-check/") + end_time = time.time() + + assert response.status_code == 200 + assert (end_time - start_time) < 2.0 # Should respond within 2 seconds + +def test_concurrent_requests(): + """Test handling of concurrent requests.""" + import concurrent.futures + import threading + + def make_request(): + return client.get("/api/v1/utils/health-check/") + + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + futures = [executor.submit(make_request) for _ in range(5)] + results = [future.result() for future in futures] + + # All requests should succeed + for result in results: + assert result.status_code == 200 + +def test_memory_usage_stability(): + """Test that repeated requests don't cause memory leaks.""" + import gc + + # Make multiple requests + for _ in range(10): + response = client.get("/api/v1/utils/health-check/") + assert response.status_code == 200 + + # Force garbage collection + gc.collect() + # This is a basic test - more sophisticated memory monitoring would be needed in practice +e +def test_database_connection_pool(): + """Test database connection pooling under load.""" + responses = [] + + # Make multiple database-dependent requests + for _ in range(5): + response = client.get("/api/v1/utils/health-check/") + responses.append(response) + + # All should succeed without connection pool exhaustion + for response in responses: + assert response.status_code == 200 diff --git a/backend/tests/test_security.py b/backend/tests/test_security.py new file mode 100644 index 0000000000..224a92b846 --- /dev/null +++ b/backend/tests/test_security.py @@ -0,0 +1,28 @@ +import pytest +from unittest.mock import patch, MagicMock +from app.core.security import create_access_token, verify_password, get_password_hash + +def test_create_access_token(): + """Test JWT token creation.""" + token = create_access_token(subject="test@example.com") + assert isinstance(token, str) + assert len(token) > 0 + +def test_verify_password_correct(): + """Test password verification with correct password.""" + plain_password = "testpassword" + hashed_password = get_password_hash(plain_password) + assert verify_password(plain_password, hashed_password) is True + +def test_verify_password_incorrect(): + """Test password verification with incorrect password.""" + plain_password = "testpassword" + hashed_password = get_password_hash(plain_password) + assert verify_password("wrongpassword", hashed_password) is False + +def test_get_password_hash(): + """Test password hashing.""" + password = "testpassword" + hashed = get_password_hash(password) + assert hashed != password + assert len(hashed) > 0 diff --git a/backend/tests/test_security_vulnerabilities.py b/backend/tests/test_security_vulnerabilities.py new file mode 100644 index 0000000000..3efbb02216 --- /dev/null +++ b/backend/tests/test_security_vulnerabilities.py @@ -0,0 +1,58 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app +from app.core.security import create_access_token + +client = TestClient(app) + +def test_sql_injection_prevention(): + """Test that SQL injection attempts are prevented.""" + token = create_access_token(subject="test@example.com") + headers = {"Authorization": f"Bearer {token}"} + + # Attempt SQL injection in query parameters + malicious_query = "1' OR '1'='1" + response = client.get(f"/api/v1/items/{malicious_query}", headers=headers) + + # Should return 404 or 422, not expose database errors + assert response.status_code in [404, 422] + +def test_xss_prevention(): + """Test XSS prevention in API responses.""" + token = create_access_token(subject="test@example.com") + headers = {"Authorization": f"Bearer {token}"} + + xss_payload = { + "title": "", + "description": "Test description" + } + + response = client.post("/api/v1/items/", json=xss_payload, headers=headers) + + if response.status_code == 200: + # Check that script tags are not returned as-is + data = response.json() + assert "