LC_8 / tests /test_api.py
github-actions[bot]
Deploy API from GitHub Actions
4e13ce7
Raw History Blame Contribute Delete
7.22 kB
from fastapi.testclient import TestClient
from app import main as api
from app.schemas import CreditApplication, MODEL_FEATURES, TOP_SHAP_FEATURES
import numpy as np
import pandas as pd
import logging
from app.drift_monitor import DriftMonitor
AUTH_HEADER = {"Authorization": "Bearer test-token"}
class FakeModel:
def __init__(self):
self.last_frame = None
def predict_proba(self, model_frame):
self.last_frame = model_frame
return [[0.2, 0.8]]
def build_test_monitor(batch_size=2):
test_monitor = DriftMonitor.__new__(DriftMonitor)
test_monitor.batch_size = batch_size
test_monitor.drift_threshold = 0.05
test_monitor.prediction_buffer = []
test_monitor.logger = logging.getLogger("drift_monitor")
test_monitor.reference_data = pd.DataFrame([
{
"EXT_SOURCE_MEAN": 0.5,
"AMT_CREDIT": 200000,
"AMT_INCOME_TOTAL": 150000,
"DAYS_BIRTH": -15000,
"DAYS_EMPLOYED": -3000,
}
])
return test_monitor
def test_health_reports_loaded_model(monkeypatch):
fake_model = FakeModel()
monkeypatch.setattr(api, "load_model", lambda: fake_model)
monkeypatch.setattr(api, "get_model", lambda: fake_model)
with TestClient(api.app) as client:
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy", "model_loaded": True}
def test_features_endpoint_lists_exposed_top_shap_features(monkeypatch):
fake_model = FakeModel()
monkeypatch.setattr(api, "load_model", lambda: fake_model)
with TestClient(api.app) as client:
response = client.get("/features")
assert response.status_code == 200
assert response.json() == list(TOP_SHAP_FEATURES)
def test_predict_uses_top_features_and_imputes_hidden_features(monkeypatch):
fake_model = FakeModel()
monkeypatch.setenv("API_TOKEN", "test-token")
monkeypatch.setattr(api, "load_model", lambda: fake_model)
monkeypatch.setattr(api, "get_model", lambda: fake_model)
with TestClient(api.app) as client:
response = client.post("/predict", json={}, headers=AUTH_HEADER)
assert response.status_code == 200
assert response.json() == {
"prediction": 1,
"probability_of_default": 0.8,
"risk_category": "High",
}
expected_input = CreditApplication().to_model_input()
expected_values = [expected_input[feature] for feature in MODEL_FEATURES]
assert fake_model.last_frame.shape == (1, len(MODEL_FEATURES))
np.testing.assert_allclose(fake_model.last_frame[0], expected_values)
def test_predict_rejects_hidden_model_features(monkeypatch):
fake_model = FakeModel()
monkeypatch.setenv("API_TOKEN", "test-token")
monkeypatch.setattr(api, "load_model", lambda: fake_model)
monkeypatch.setattr(api, "get_model", lambda: fake_model)
with TestClient(api.app) as client:
response = client.post(
"/predict",
json={"CNT_CHILDREN": 2},
headers=AUTH_HEADER,
)
assert response.status_code == 422
def test_predict_returns_503_when_model_is_not_loaded(monkeypatch):
fake_model = FakeModel()
monkeypatch.setenv("API_TOKEN", "test-token")
monkeypatch.setattr(api, "load_model", lambda: fake_model)
monkeypatch.setattr(
api,
"get_model",
lambda: (_ for _ in ()).throw(RuntimeError("Model not loaded")),
)
with TestClient(api.app) as client:
response = client.post("/predict", json={}, headers=AUTH_HEADER)
assert response.status_code == 503
assert response.json()["detail"] == "Model not loaded"
def test_predict_requires_bearer_token(monkeypatch):
fake_model = FakeModel()
monkeypatch.setenv("API_TOKEN", "test-token")
monkeypatch.setattr(api, "load_model", lambda: fake_model)
monkeypatch.setattr(api, "get_model", lambda: fake_model)
with TestClient(api.app) as client:
response = client.post("/predict", json={})
assert response.status_code == 401
assert response.json()["detail"] == "Missing bearer token"
def test_predict_rejects_invalid_bearer_token(monkeypatch):
fake_model = FakeModel()
monkeypatch.setenv("API_TOKEN", "test-token")
monkeypatch.setattr(api, "load_model", lambda: fake_model)
monkeypatch.setattr(api, "get_model", lambda: fake_model)
with TestClient(api.app) as client:
response = client.post(
"/predict",
json={},
headers={"Authorization": "Bearer wrong-token"},
)
assert response.status_code == 401
assert response.json()["detail"] == "Invalid bearer token"
def test_predict_returns_503_when_api_token_is_not_configured(monkeypatch):
fake_model = FakeModel()
monkeypatch.delenv("API_TOKEN", raising=False)
monkeypatch.setattr(api, "load_model", lambda: fake_model)
monkeypatch.setattr(api, "get_model", lambda: fake_model)
with TestClient(api.app) as client:
response = client.post("/predict", json={}, headers=AUTH_HEADER)
assert response.status_code == 503
assert response.json()["detail"] == "API_TOKEN is not configured"
def test_drift_monitor_triggers(monkeypatch):
fake_model = FakeModel()
monkeypatch.setenv("API_TOKEN", "test-token")
monkeypatch.setattr(api, "load_model", lambda: fake_model)
monkeypatch.setattr(api, "get_model", lambda: fake_model)
# 🔥 Replace drift monitor with small batch size
# from app.drift_monitor import DriftMonitor
test_monitor = build_test_monitor(batch_size=3)
monkeypatch.setattr(api, "drift_monitor", test_monitor)
with TestClient(api.app) as client:
for _ in range(3): # trigger batch
response = client.post("/predict", json={}, headers=AUTH_HEADER)
assert response.status_code == 200
def test_drift_monitor_logs(monkeypatch, caplog):
fake_model = FakeModel()
monkeypatch.setenv("API_TOKEN", "test-token")
monkeypatch.setattr(api, "load_model", lambda: fake_model)
monkeypatch.setattr(api, "get_model", lambda: fake_model)
# from app.drift_monitor import DriftMonitor
test_monitor = build_test_monitor(batch_size=2)
# Fake reference data
test_monitor.reference_data = pd.DataFrame([
{
"EXT_SOURCE_MEAN": 0.5,
"AMT_CREDIT": 200000,
"AMT_INCOME_TOTAL": 150000,
"DAYS_BIRTH": -15000,
"DAYS_EMPLOYED": -3000,
}
])
monkeypatch.setattr(api, "drift_monitor", test_monitor)
caplog.set_level(logging.INFO)
with TestClient(api.app) as client:
for _ in range(2):
client.post("/predict", json={}, headers=AUTH_HEADER)
# ✅ Check drift analysis triggered
drift_logs = [
record for record in caplog.records
if "drift_analysis_completed" in record.getMessage()
]
assert len(drift_logs) > 0
# ✅ Check feature-level drift (OPTIONAL)
feature_logs = [
record for record in caplog.records
if "feature_drift_detected" in record.getMessage()
]
# You can assert or just inspect
assert isinstance(feature_logs, list)