chromeleon1 / test_api.py
Srinu007's picture
Upload test_api.py
a1b9fa3 verified
Raw History Blame Contribute Delete
8.67 kB
"""tests/test_api.py — pytest suite (no GROQ_API_KEY required to run)"""
import os, math, pytest
from unittest.mock import MagicMock
os.environ.pop("GROQ_API_KEY", None) # ensure clean state
from fastapi.testclient import TestClient
@pytest.fixture(scope="session")
def client():
from app import app, DB, STAGE_MAP
stub = [
{"i":"1","p":"ACARBOSE","s":"ACA_SEQ_01","d":"2025-01-03","sc":"RM",
"it":"Unknown","st":"Finished","sw":501.0,"t":"ASSAY","b":"BATCH001",
"ar":"AR001","in":"INSTR-01","co":"COL-01","d1":25.0,"wl":"210 nm",
"ft":"Isocratic","th":25.0,"ct":35.0,"rp":106.0,"pp":102.0,
"pa":"311000.0","pr":"17.6","pas":"1.19","pp2":"3900","cn":"Acarbose","sw1":"502.41","pn":"Acarbose"},
{"i":"2","p":"METFORMIN","s":"MET_SEQ_01","d":"2025-03-15","sc":"FP",
"it":"CalibrationStandard","st":"Finished","sw":200.0,"t":"DISSOLUTION",
"b":"BATCH002","ar":"AR002","in":"INSTR-02","co":"COL-02","d1":100.0,
"wl":"254 nm","ft":"Gradient","th":37.0,"ct":40.0,"rp":200.0,"pp":198.0,
"pa":"150000.0","pr":"5.3","pas":"1.05","pp2":"5200","cn":"Metformin HCl","sw1":"100.0","pn":"Metformin"},
{"i":"3","p":"ACARBOSE","s":"ACA_SEQ_01","d":"2025-06-20","sc":"RM",
"it":"Blank","st":"Finished","sw":None,"t":"ASSAY","b":"BATCH001",
"ar":"AR001","in":"INSTR-01","co":"COL-01","d1":1.0,"wl":"210 nm",
"ft":"Isocratic","th":25.0,"ct":35.0,"rp":104.0,"pp":101.0,
"pa":None,"pr":None,"pas":None,"pp2":None,"cn":None,"sw1":None,"pn":None},
]
areas=[float(r["pa"]) for r in stub if r.get("pa")]
pv=[float(r["rp"]) for r in stub if r.get("rp")]
cv=[float(r["ct"]) for r in stub if r.get("ct") is not None]
def avg(a): return sum(a)/len(a) if a else 0.0
def std(a):
if len(a)<2: return 0.0
m=avg(a); return math.sqrt(sum((x-m)**2 for x in a)/len(a))
DB.records=stub
DB.agg={"total":3,"finished":3,
"area_mean":avg(areas),"area_std":std(areas),"area_n":len(areas),
"pres_mean":avg(pv),"pres_std":std(pv),"pres_n":len(pv),
"ct_mean":avg(cv),"ct_std":std(cv),"ct_n":len(cv),
"stage_cnt":{"Raw Material":2,"Finished Product":1},
"it_cnt":{"Unknown":1,"CalibrationStandard":1,"Blank":1},
"instr_cnt":{"INSTR-01":2,"INSTR-02":1},"wl_cnt":{"210 nm":2,"254 nm":1},
"prod_cnt":{"ACARBOSE":2,"METFORMIN":1},"ft_cnt":{"Isocratic":2,"Gradient":1},
"thermo_cnt":{"25.0":2,"37.0":1},"ct_cnt":{"35.0":2,"40.0":1},
"daily":[
{"d":"2025-01-03","n":1,"a":311000.0,"p":106.0,"c":35.0,"r":17.6,"as":1.19},
{"d":"2025-03-15","n":1,"a":150000.0,"p":200.0,"c":40.0,"r":5.3,"as":1.05},
{"d":"2025-06-20","n":1,"a":None,"p":104.0,"c":35.0,"r":None,"as":None},
],
"area_sample":areas,"sa_stats":{"Raw Material":{"mean":311000.0,"std":0.0,"n":1}},"it_stats":{}}
DB.meta={"total":3,"sequences":["ACA_SEQ_01","MET_SEQ_01"],"products":["ACARBOSE","METFORMIN"],
"instruments":["INSTR-01","INSTR-02"],"columns":["COL-01","COL-02"],
"inj_types":["Blank","CalibrationStandard","Unknown"],"stage_cats":list(STAGE_MAP.values()),
"tests":["ASSAY","DISSOLUTION"],"wavelengths":["210 nm","254 nm"],"flow_types":["Gradient","Isocratic"]}
DB.loaded=True
return TestClient(app)
class TestHealth:
def test_ok(self, client):
r=client.get("/api/health"); assert r.status_code==200
b=r.json(); assert b["status"]=="ok"; assert b["records"]==3
assert b["port"]==7860; assert "ai_enabled" in b
def test_meta(self, client):
r=client.get("/api/meta"); assert r.status_code==200
assert "ACARBOSE" in r.json()["products"]
def test_agg(self, client):
r=client.get("/api/aggregate"); assert r.status_code==200; assert r.json()["total"]==3
class TestInjections:
def test_list(self, client): assert client.get("/api/injections").json()["total"]==3
def test_page(self, client):
r=client.get("/api/injections?per_page=2"); assert len(r.json()["data"])==2
def test_product(self, client): assert client.get("/api/injections?product=ACARBOSE").json()["total"]==2
def test_inj_type(self, client): assert client.get("/api/injections?inj_type=Blank").json()["total"]==1
def test_stage(self, client): assert client.get("/api/injections?stage=Finished+Product").json()["total"]==1
def test_dates(self, client): assert client.get("/api/injections?date_from=2025-02-01").json()["total"]==2
def test_pressure(self, client): assert client.get("/api/injections?pres_min=150&pres_max=300").json()["total"]==1
def test_instrument(self, client): assert client.get("/api/injections?instrument=INSTR-02").json()["total"]==1
def test_wavelength(self, client): assert client.get("/api/injections?wavelength=254+nm").json()["total"]==1
def test_fulltext(self, client): assert client.get("/api/injections?q=BATCH002").json()["total"]==1
def test_stage_label(self, client):
for rec in client.get("/api/injections").json()["data"]: assert "stage_label" in rec
class TestSingle:
def test_found(self, client):
r=client.get("/api/injections/1"); assert r.status_code==200
assert r.json()["stage_label"]=="Raw Material"
def test_missing(self, client): assert client.get("/api/injections/99999").status_code==404
class TestTrends:
def test_count(self, client): assert client.get("/api/trends/daily?metric=n").json()["values"]==[1,1,1]
def test_area(self, client): assert client.get("/api/trends/daily?metric=a").json()["values"][0]==311000.0
def test_date_filter(self, client):
assert len(client.get("/api/trends/daily?date_from=2025-03-01").json()["labels"])==2
class TestStats:
def test_summary(self, client):
b=client.get("/api/stats/summary").json()
assert b["total"]==3; assert b["finish_rate_pct"]==100.0
assert b["area"]["p10"]<=b["area"]["median"]<=b["area"]["p90"]
class TestExport:
def test_csv(self, client):
r=client.get("/api/injections/export"); assert "text/csv" in r.headers["content-type"]
lines=r.text.strip().split("\n"); assert lines[0].startswith("Product,Batch"); assert len(lines)==4
def test_filtered(self, client):
lines=client.get("/api/injections/export?product=ACARBOSE").text.strip().split("\n"); assert len(lines)==3
class TestCompare:
def test_ok(self, client):
r=client.post("/api/compare",json={"by":"product","groups":["ACARBOSE","METFORMIN"]})
assert r.status_code==200; assert r.json()["groups"]["ACARBOSE"]["count"]==2
def test_too_few(self, client): assert client.post("/api/compare",json={"by":"product","groups":["A"]}).status_code==400
def test_too_many(self, client): assert client.post("/api/compare",json={"by":"product","groups":["A","B","C","D"]}).status_code==400
def test_bad_by(self, client): assert client.post("/api/compare",json={"by":"bad","groups":["A","B"]}).status_code==400
class TestAI:
def test_models_no_key(self, client):
# Works without any key — just lists models
r=client.get("/api/ai/models"); assert r.status_code==200
b=r.json(); assert b["provider"]=="GROQ"
assert any(m["id"]=="llama-3.3-70b-versatile" for m in b["models"])
assert b["enabled"]==False # no key in test env
assert "note" in b
def test_chat_no_key_503(self, client):
# Without key → 503 with clear message
r=client.post("/api/ai/chat",json={"messages":[{"role":"user","content":"hi"}]})
assert r.status_code==503
assert "GROQ_API_KEY" in r.json()["detail"]
assert "console.groq.com" in r.json()["detail"]
def test_chat_with_mock(self, client):
import app as app_mod
mock_resp=MagicMock()
mock_resp.choices=[MagicMock()]
mock_resp.choices[0].message.content="RSD 12% is within acceptable limits."
mock_resp.choices[0].finish_reason="stop"
mock_resp.model="llama-3.3-70b-versatile"
mock_resp.usage.prompt_tokens=50; mock_resp.usage.completion_tokens=30; mock_resp.usage.total_tokens=80
mock_client=MagicMock()
mock_client.chat.completions.create.return_value=mock_resp
original=app_mod._get_groq_client
app_mod._get_groq_client=lambda: mock_client
r=client.post("/api/ai/chat",json={"messages":[{"role":"user","content":"Is RSD ok?"}]})
app_mod._get_groq_client=original
assert r.status_code==200
assert "RSD" in r.json()["reply"]
assert r.json()["model"]=="llama-3.3-70b-versatile"