diff --git a/backend/requirements.txt b/backend/requirements.txt index 46ee4d85..32b95ee2 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -15,4 +15,5 @@ slowapi>=0.1.9 apscheduler>=3.10.1 pytest>=8.0.0 pytest-asyncio>=0.23.0 +pytest-cov>=4.1.0 httpx>=0.27.0 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index b8aa2883..6017724a 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,14 +1,17 @@ import pytest -from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool +from fastapi.testclient import TestClient +from unittest.mock import patch, MagicMock +from datetime import datetime, timezone -from backend.database import Base, get_db from backend.main import app +from backend.database import Base, get_db +from backend.models import User +from backend.config_manager import ConfigManager from backend.auth import get_current_admin, TokenData -from datetime import datetime, timezone # Use in-memory SQLite for tests SQLALCHEMY_DATABASE_URL = "sqlite://" @@ -20,8 +23,10 @@ engine = create_engine( ) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + @pytest.fixture(scope="function", autouse=True) def mock_scheduler(): + """Shutdown scheduler before and after each test to avoid conflicts.""" from backend.scheduler import scheduler if scheduler.running: scheduler.shutdown() @@ -29,37 +34,162 @@ def mock_scheduler(): if scheduler.running: scheduler.shutdown() + @pytest.fixture(scope="function") -def db(): +def test_db(): + """Create in-memory SQLite database for tests.""" Base.metadata.create_all(bind=engine) session = TestingSessionLocal() - try: - yield session - finally: - session.close() - Base.metadata.drop_all(bind=engine) + yield session + session.close() + Base.metadata.drop_all(bind=engine) + @pytest.fixture(scope="function") -def client(db): +def test_client(test_db): + """Create FastAPI test client with mocked database.""" + def override_get_db(): try: - yield db + yield test_db finally: pass - - # Mock admin user with valid TokenData - def override_get_current_admin(): - return TokenData( - sub=1, - username="admin", - role="admin", - exp=datetime.now(timezone.utc) - ) app.dependency_overrides[get_db] = override_get_db - app.dependency_overrides[get_current_admin] = override_get_current_admin - - with TestClient(app) as c: - yield c - + + client = TestClient(app) + yield client app.dependency_overrides.clear() + + +@pytest.fixture(scope="function") +def mock_ldap(): + """Mock LDAP authentication.""" + with patch("backend.auth.ldap3.Server") as mock_server, \ + patch("backend.auth.ldap3.Connection") as mock_conn_class: + + mock_conn = MagicMock() + mock_conn.bind.return_value = True + mock_conn.search.return_value = True + mock_conn.entries = [ + MagicMock(entry_dn="uid=testuser,ou=people,dc=example,dc=com", + uid=["testuser"]) + ] + mock_conn_class.return_value = mock_conn + + yield mock_conn + + +@pytest.fixture(scope="function") +def mock_gemini(): + """Mock Google Gemini AI extraction.""" + with patch("backend.ai.gemini_extractor.generate_content") as mock_gen: + mock_response = MagicMock() + mock_response.text = '''{ + "name": "Test Item", + "part_number": "PN-12345", + "category": "Electronics", + "quantity": 5 + }''' + mock_gen.return_value = mock_response + yield mock_gen + + +@pytest.fixture(scope="function") +def mock_claude(): + """Mock Anthropic Claude AI extraction.""" + with patch("backend.ai.claude_extractor.generate_content") as mock_gen: + mock_content = MagicMock() + mock_content.text = '''{ + "name": "Test Item", + "part_number": "PN-12345", + "category": "Electronics", + "quantity": 5 + }''' + mock_response = MagicMock() + mock_response.content = [mock_content] + mock_gen.return_value = mock_response + yield mock_gen + + +@pytest.fixture(scope="function") +def mock_config(): + """Mock ConfigManager.""" + with patch.object(ConfigManager, "get_config") as mock_get: + mock_get.return_value = { + "ai_provider": "gemini", + "api_key": "test-key" + } + yield mock_get + + +@pytest.fixture(scope="function") +def admin_token(test_db): + """Create a test admin user and return auth token.""" + admin = User( + username="admin", + hashed_password="hashed_password", + role="admin", + origin="local" + ) + test_db.add(admin) + test_db.commit() + test_db.refresh(admin) + + # Return mocked admin token data + return TokenData( + sub=admin.id, + username="admin", + role="admin", + exp=datetime.now(timezone.utc) + ) + + +@pytest.fixture(scope="function") +def user_token(test_db): + """Create a test regular user and return auth token.""" + user = User( + username="user", + hashed_password="hashed_password", + role="user", + origin="local" + ) + test_db.add(user) + test_db.commit() + test_db.refresh(user) + + # Return mocked user token data + return TokenData( + sub=user.id, + username="user", + role="user", + exp=datetime.now(timezone.utc) + ) + + +@pytest.fixture(scope="function") +def admin_client(test_client, admin_token): + """Create a test client authenticated as admin.""" + def override_get_current_admin(): + return admin_token + + app.dependency_overrides[get_current_admin] = override_get_current_admin + + yield test_client + + app.dependency_overrides.pop(get_current_admin, None) + + +@pytest.fixture(scope="function") +def user_client(test_client, user_token): + """Create a test client authenticated as regular user.""" + from backend.auth import get_current_user + + def override_get_current_user(): + return user_token + + app.dependency_overrides[get_current_user] = override_get_current_user + + yield test_client + + app.dependency_overrides.pop(get_current_user, None)