""" Smoke tests for the ADMET descriptor endpoint. Covers: - Valid SMILES return 200 with correct shape - Invalid SMILES return 422 or meaningful error - Response contains 3a (core descriptors) and 3b (toxicity) sections - _methodology metadata present - Toxicity section contains _disclaimer - All core numeric fields are finite numbers """ import math import pytest from tests.conftest import requires_rdkit BASE = "/api/admet/descriptors" @requires_rdkit class TestADMETBasic: """Core endpoint health checks.""" def test_aspirin_returns_200(self, client): resp = client.post(BASE, json={"smiles": "CC(=O)OC1=CC=CC=C1C(=O)O"}) assert resp.status_code == 200 body = resp.json() assert body["status"] == "complete" assert body["result"] is not None def test_result_has_core_fields(self, client, valid_smiles): result = client.post(BASE, json={"smiles": valid_smiles}).json()["result"] for field in [ "smiles", "formula", "molecular_weight", "logp", "tpsa", "hbd", "hba", "rotatable_bonds", "qed_score", "heavy_atoms", "molar_refractivity", "molecular_volume", "fsp3", "ring_count", "aromatic_ring_count", ]: assert field in result, f"Missing core field: {field}" def test_numeric_fields_are_finite(self, client, valid_smiles): result = client.post(BASE, json={"smiles": valid_smiles}).json()["result"] numeric_fields = [ "molecular_weight", "logp", "tpsa", "qed_score", "molar_refractivity", "molecular_volume", "fsp3", ] for field in numeric_fields: val = result[field] assert isinstance(val, (int, float)), f"{field} is not numeric: {val}" assert math.isfinite(val), f"{field} is not finite: {val}" @requires_rdkit class TestADMETMethodology: """Verify 3a/3b split metadata.""" def test_methodology_present(self, client, valid_smiles): result = client.post(BASE, json={"smiles": valid_smiles}).json()["result"] assert "_methodology" in result def test_core_descriptors_are_3a(self, client, valid_smiles): meth = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["_methodology"] assert meth["core_descriptors"]["tier"] == "3a" assert meth["core_descriptors"]["confidence"] == "high" def test_toxicity_is_3b(self, client, valid_smiles): meth = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["_methodology"] assert meth["toxicity"]["tier"] == "3b" assert meth["toxicity"]["confidence"] == "approximate" def test_drug_likeness_is_3a(self, client, valid_smiles): meth = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["_methodology"] assert meth["drug_likeness"]["tier"] == "3a" @requires_rdkit class TestADMETToxicity: """Verify toxicity section shape and disclaimer.""" def test_toxicity_has_disclaimer(self, client, valid_smiles): tox = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["toxicity"] assert "_disclaimer" in tox assert "heuristic" in tox["_disclaimer"].lower() or "no ML" in tox["_disclaimer"] def test_toxicity_has_all_fields(self, client, valid_smiles): tox = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["toxicity"] for field in [ "ames_mutagenicity", "ames_alerts", "herg_liability", "hepatotoxicity_dili", "skin_sensitization", "acute_toxicity_ld50", "ld50_estimate_log", "risk_score", ]: assert field in tox, f"Missing toxicity field: {field}" def test_risk_score_in_range(self, client, valid_smiles): score = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["toxicity"]["risk_score"] assert 0 <= score <= 10 @requires_rdkit class TestADMETSafety: """Verify drug-likeness and structural alerts sections.""" def test_drug_likeness_has_lipinski(self, client, valid_smiles): dl = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["drug_likeness"] assert "lipinski" in dl assert "pass" in dl["lipinski"] assert "violation_count" in dl["lipinski"] def test_structural_alerts_present(self, client, valid_smiles): sa = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["structural_alerts"] assert "pains" in sa assert "brenk" in sa assert "total_alert_count" in sa def test_absorption_section_present(self, client, valid_smiles): abs_ = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["absorption"] assert "oral_bioavailability" in abs_ assert "caco2_permeability" in abs_ assert "hia" in abs_ @requires_rdkit class TestADMETEdgeCases: """Edge cases and error handling.""" def test_invalid_smiles_returns_error(self, client, invalid_smiles): resp = client.post(BASE, json={"smiles": invalid_smiles}) assert resp.status_code in (400, 422, 500) body = resp.json() assert "error" in body or "detail" in body def test_empty_smiles_returns_422(self, client): resp = client.post(BASE, json={"smiles": ""}) assert resp.status_code == 422 def test_missing_smiles_returns_422(self, client): resp = client.post(BASE, json={}) assert resp.status_code == 422 def test_parametrized_molecules(self, client, sample_smiles): """Run every molecule in the parametrized fixture.""" name, smiles = sample_smiles resp = client.post(BASE, json={"smiles": smiles}) assert resp.status_code == 200, f"{name} failed: {resp.text}" result = resp.json()["result"] assert result is not None assert result["molecular_weight"] > 0