Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import os | |
| from typing import Any | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| import main | |
| from src.utils import load_env | |
| TEST_API_KEY = "test-api-key" | |
| AUTH_HEADERS = {"X-API-Key": TEST_API_KEY} | |
| def _build_valid_record() -> dict[str, Any]: | |
| record: dict[str, Any] = {} | |
| for feature, spec in main.FEATURE_SPECS.items(): | |
| expected_type = str(spec.get("type", "")).strip().lower() | |
| if expected_type == "number": | |
| record[feature] = 1.0 | |
| elif expected_type == "category": | |
| choices = spec.get("choices", []) or [] | |
| record[feature] = choices[0] | |
| else: | |
| raise AssertionError(f"Type de feature non supporté dans le schéma: {expected_type!r}") | |
| return record | |
| def client(monkeypatch: pytest.MonkeyPatch) -> TestClient: | |
| monkeypatch.setattr(main, "API_KEY", TEST_API_KEY) | |
| monkeypatch.setattr(main, "get_engine_from_env", lambda: None) | |
| monkeypatch.setattr(main, "init_api_logging_tables", lambda _engine: None) | |
| monkeypatch.setattr(main, "init_feature_tables_if_missing", lambda _engine: None) | |
| monkeypatch.setattr( | |
| main, | |
| "predict_with_artifact_model", | |
| lambda X, threshold=0.5: ([0.8 for _ in range(len(X))], [1 for _ in range(len(X))]), | |
| ) | |
| with TestClient(main.app) as test_client: | |
| yield test_client | |
| # --------------------------------------------------------------------- | |
| # Tests API | |
| # --------------------------------------------------------------------- | |
| def test_health_returns_ok(client: TestClient) -> None: | |
| response = client.get("/health") | |
| assert response.status_code == 200 | |
| assert response.json() == {"status": "ok"} | |
| def test_predict_returns_proba_and_label(client: TestClient) -> None: | |
| payload = {"records": [_build_valid_record()]} | |
| response = client.post("/predict", json=payload, headers=AUTH_HEADERS) | |
| assert response.status_code == 200 | |
| body = response.json() | |
| assert set(body.keys()) == {"proba_leave", "label"} | |
| assert len(body["proba_leave"]) == 1 | |
| assert len(body["label"]) == 1 | |
| assert isinstance(body["proba_leave"][0], float) | |
| assert body["label"][0] in (0, 1) | |
| def test_predict_returns_401_without_api_key(client: TestClient) -> None: | |
| payload = {"records": [_build_valid_record()]} | |
| response = client.post("/predict", json=payload) | |
| assert response.status_code == 401 | |
| def test_predict_returns_401_with_wrong_api_key(client: TestClient) -> None: | |
| payload = {"records": [_build_valid_record()]} | |
| response = client.post("/predict", json=payload, headers={"X-API-Key": "wrong-api-key"}) | |
| assert response.status_code == 401 | |
| def test_predict_returns_422_when_record_has_missing_key(client: TestClient) -> None: | |
| record = _build_valid_record() | |
| missing_feature = next(iter(main.REQUIRED_FEATURES_ORDER)) | |
| record.pop(missing_feature) | |
| payload = {"records": [record]} | |
| response = client.post("/predict", json=payload, headers=AUTH_HEADERS) | |
| assert response.status_code == 422 | |
| assert "features manquantes" in response.text | |
| def test_predict_returns_422_when_feature_has_invalid_type(client: TestClient) -> None: | |
| record = _build_valid_record() | |
| numeric_feature = next( | |
| feature | |
| for feature, spec in main.FEATURE_SPECS.items() | |
| if str(spec.get("type", "")).strip().lower() == "number" | |
| ) | |
| record[numeric_feature] = "not-a-number" | |
| payload = {"records": [record]} | |
| response = client.post("/predict", json=payload, headers=AUTH_HEADERS) | |
| assert response.status_code == 422 | |
| assert "doit être un nombre" in response.text | |
| # --------------------------------------------------------------------- | |
| # Tests load_env (migré depuis main._resolve_env_file_path) | |
| # --------------------------------------------------------------------- | |
| def test_load_env_loads_root_env_if_present(tmp_path) -> None: | |
| root_env = tmp_path / ".env" | |
| root_env.write_text("TEST_VAR_ROOT=root\n", encoding="utf-8") | |
| load_env(root_dir=tmp_path) | |
| assert os.getenv("TEST_VAR_ROOT") == "root" | |
| def test_load_env_does_nothing_if_no_env_file(tmp_path) -> None: | |
| # Ne doit pas lever d'exception | |
| load_env(root_dir=tmp_path) | |
| def test_load_env_falls_back_to_confs_env(tmp_path) -> None: | |
| confs_env = tmp_path / "confs" / "dev" / ".env" | |
| confs_env.parent.mkdir(parents=True, exist_ok=True) | |
| confs_env.write_text("TEST_VAR_CONFS=confs\n", encoding="utf-8") | |
| load_env(root_dir=tmp_path) | |
| assert os.getenv("TEST_VAR_CONFS") == "confs" | |
| def test_load_env_falls_back_to_confs_env_pattern(tmp_path) -> None: | |
| confs_env = tmp_path / "confs" / "dev" / ".env.dev" | |
| confs_env.parent.mkdir(parents=True, exist_ok=True) | |
| confs_env.write_text("TEST_VAR_PATTERN=pattern\n", encoding="utf-8") | |
| load_env(root_dir=tmp_path) | |
| assert os.getenv("TEST_VAR_PATTERN") == "pattern" |