Spaces:
Running
Running
| """ | |
| Shared fixtures for Bio Nexus backend smoke tests. | |
| Mocks heavy/unavailable dependencies (supabase, litellm, Bio, sentry, etc.) | |
| at sys.modules level so routers can import cleanly in a local test env. | |
| """ | |
| import base64 | |
| import json | |
| import os | |
| import sys | |
| from unittest.mock import MagicMock | |
| import pytest | |
| from fastapi import FastAPI | |
| from fastapi.testclient import TestClient | |
| # Ensure the backend app package is importable | |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) | |
| # --------------------------------------------------------------------------- | |
| # Mock out heavy dependencies that aren't installed in the test env | |
| # --------------------------------------------------------------------------- | |
| _MOCK_MODULES = [ | |
| "supabase", | |
| "supabase._sync.client", | |
| "litellm", | |
| "reportlab", | |
| "reportlab.lib", | |
| "reportlab.lib.pagesizes", | |
| "reportlab.lib.styles", | |
| "reportlab.lib.units", | |
| "reportlab.lib.colors", | |
| "reportlab.pdfgen", | |
| "reportlab.pdfgen.canvas", | |
| "reportlab.platypus", | |
| "sentry_sdk", | |
| "redis", | |
| ] | |
| for mod_name in _MOCK_MODULES: | |
| if mod_name not in sys.modules: | |
| sys.modules[mod_name] = MagicMock() | |
| # --------------------------------------------------------------------------- | |
| # Minimal JWT helper | |
| # --------------------------------------------------------------------------- | |
| def _make_test_jwt(user_id: str = "test-user-000") -> str: | |
| header = base64.urlsafe_b64encode(json.dumps({"alg": "none", "typ": "JWT"}).encode()).rstrip(b"=").decode() | |
| payload = base64.urlsafe_b64encode(json.dumps({"sub": user_id}).encode()).rstrip(b"=").decode() | |
| return f"{header}.{payload}.sig" | |
| TEST_JWT = _make_test_jwt() | |
| # --------------------------------------------------------------------------- | |
| # Lightweight test app — only imports the routers we actually test | |
| # --------------------------------------------------------------------------- | |
| def client(): | |
| """Build a minimal FastAPI app with only the 4 routers under test.""" | |
| app = FastAPI(title="Bio Nexus Smoke Tests") | |
| from app.routers import admet, md, function_predict, docking | |
| app.include_router(admet.router) | |
| app.include_router(md.router) | |
| app.include_router(function_predict.router) | |
| app.include_router(docking.router) | |
| with TestClient(app, raise_server_exceptions=False) as c: | |
| yield c | |
| def auth_headers(): | |
| """Headers dict with a valid Bearer token.""" | |
| return {"Authorization": f"Bearer {TEST_JWT}"} | |
| # --------------------------------------------------------------------------- | |
| # Pre-load OpenMM native libs BEFORE rdkit. OpenMM and RDKit wheels bundle | |
| # conflicting copies of MSVC runtime DLLs (msvcp140/concrt140); importing | |
| # rdkit first makes OpenMM's Context creation crash with an access violation. | |
| # Loading openmm.app first resolves the conflict. | |
| # --------------------------------------------------------------------------- | |
| try: | |
| import openmm.app # noqa: F401 | |
| except Exception: | |
| pass | |
| # --------------------------------------------------------------------------- | |
| # Detect rdkit availability (only on HF Spaces Docker, not local dev) | |
| # find_spec avoids actually importing rdkit, which would trigger the | |
| # OpenMM/RDKit runtime conflict above for real-OpenMM tests. | |
| # --------------------------------------------------------------------------- | |
| try: | |
| import importlib.util | |
| HAS_RDKIT = importlib.util.find_spec("rdkit.Chem") is not None | |
| except (ImportError, ValueError): | |
| HAS_RDKIT = False | |
| requires_rdkit = pytest.mark.skipif(not HAS_RDKIT, reason="rdkit not installed locally — run on HF Spaces") | |
| # --------------------------------------------------------------------------- | |
| # Sample molecules | |
| # --------------------------------------------------------------------------- | |
| SAMPLE_MOLECULES = { | |
| "aspirin": "CC(=O)OC1=CC=CC=C1C(=O)O", | |
| "caffeine": "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", | |
| "ibuprofen": "CC(C)CC1=CC=C(C=C1)C(C)C(=O)O", | |
| "paracetamol": "CC(=O)NC1=CC=C(C=C1)O", | |
| "metformin": "CN(C)C(=N)NC(=O)N", | |
| "paclitaxel": "CC(=O)OC1C(O)CC2OC3C(O)C(=CC(=O)O3)CC(O)C12C4=CC=CC=C4C(=O)OC5C(O)C(COC(=O)C)OC(O)C5NC(=O)C6=CC=CC=C6", | |
| "short_pseudo": "C", | |
| } | |
| def sample_smiles(request): | |
| name = request.param | |
| return name, SAMPLE_MOLECULES[name] | |
| def valid_smiles(): | |
| return "CC(=O)OC1=CC=CC=C1C(=O)O" | |
| def invalid_smiles(): | |
| return "NOT_A_SMILES_12345" | |