File size: 5,912 Bytes
5370232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
"""
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