diff --git a/.claude/anthropic_key.sh b/.claude/anthropic_key.sh new file mode 100755 index 0000000000..13f47935d9 --- /dev/null +++ b/.claude/anthropic_key.sh @@ -0,0 +1,2 @@ +#!/bin/sh + diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..438510822f --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,3 @@ +{ + "apiKeyHelper": ".claude/anthropic_key.sh" +} 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/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000000..4ea72a911a --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml new file mode 100644 index 0000000000..7ef04e2ea0 --- /dev/null +++ b/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000000..1f2ea11e7f --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000000..8648f9401a --- /dev/null +++ b/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/full-stack-fastapi-template.iml b/.idea/full-stack-fastapi-template.iml new file mode 100644 index 0000000000..8ff4b22d3d --- /dev/null +++ b/.idea/full-stack-fastapi-template.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000000..105ce2da2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..7972cc0be8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000..4c9e421c25 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..35eb1ddfbb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/TASK_PROMPT.txt b/TASK_PROMPT.txt new file mode 100644 index 0000000000..f37ab4c008 --- /dev/null +++ b/TASK_PROMPT.txt @@ -0,0 +1,4 @@ +1) Add a toggle button on the main page to switch between dark mode and light mode. The selected mode should persist across page reloads (e.g., using local storage). +the defaulted mode should be system preference and gives us ability to switch it in the ui it should be available on all pages. +2) On the user items page, allow users to pick and upload images when adding new items. Implement the necessary backend API endpoint to handle image uploads and associate them with the items. +3) Implement a database seed script that inserts a test user with the username nandith@gmail.com, password 12345678, and admin rights. Ensure this user is created only if it does not already exist. \ No newline at end of file diff --git a/__pycache__/utils.cpython-313.pyc b/__pycache__/utils.cpython-313.pyc new file mode 100644 index 0000000000..db150cd001 Binary files /dev/null and b/__pycache__/utils.cpython-313.pyc differ 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 "