Files
breakpilot-compliance/backend-compliance/tests/test_quality_routes.py
Benjamin Admin 6509e64dd9
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
feat(sdk): API-Referenz Frontend + Backend-Konsolidierung (Shared Utilities, CRUD Factory)
- API-Referenz Seite (/sdk/api-docs) mit ~690 Endpoints, Suche, Filter, Modul-Index
- Shared db_utils.py (row_to_dict) + tenant_utils Integration in 6 Route-Dateien
- CRUD Factory (crud_factory.py) fuer zukuenftige Module
- Version-Route Auto-Registration in versioning_utils.py
- 1338 Tests bestanden, -232 Zeilen Duplikat-Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:07:43 +01:00

906 lines
32 KiB
Python

"""Tests for AI Quality Metrics and Tests routes (quality_routes.py).
Covers:
- Schema validation (MetricCreate, MetricUpdate, TestCreate, TestUpdate)
- Helper functions (_row_to_dict, _get_tenant_id)
- HTTP endpoints via FastAPI TestClient with mocked DB session
"""
import pytest
from unittest.mock import MagicMock
from datetime import datetime
from fastapi import FastAPI
from fastapi.testclient import TestClient
from compliance.api.quality_routes import (
router,
MetricCreate,
MetricUpdate,
TestCreate,
TestUpdate,
)
from compliance.api.db_utils import row_to_dict as _row_to_dict
DEFAULT_TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
from classroom_engine.database import get_db
# =============================================================================
# TestClient Setup
# =============================================================================
app = FastAPI()
app.include_router(router)
client = TestClient(app)
DEFAULT_TENANT = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
OTHER_TENANT = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"
METRIC_ID = "bbbbbbbb-0001-0001-0001-000000000001"
TEST_ID = "cccccccc-0001-0001-0001-000000000001"
def make_metric_row(overrides=None):
data = {
"id": METRIC_ID,
"tenant_id": DEFAULT_TENANT,
"name": "Test Metric",
"category": "accuracy",
"score": 85.0,
"threshold": 80.0,
"trend": "stable",
"ai_system": None,
"last_measured": datetime(2024, 1, 1),
"created_at": datetime(2024, 1, 1),
"updated_at": datetime(2024, 1, 1),
}
if overrides:
data.update(overrides)
row = MagicMock()
row._mapping = data
return row
def make_test_row(overrides=None):
data = {
"id": TEST_ID,
"tenant_id": DEFAULT_TENANT,
"name": "Test Run",
"status": "passed",
"duration": "1.2s",
"ai_system": None,
"details": None,
"last_run": datetime(2024, 1, 1),
"created_at": datetime(2024, 1, 1),
"updated_at": datetime(2024, 1, 1),
}
if overrides:
data.update(overrides)
row = MagicMock()
row._mapping = data
return row
@pytest.fixture
def mock_db():
db = MagicMock()
def override_get_db():
yield db
app.dependency_overrides[get_db] = override_get_db
yield db
app.dependency_overrides.clear()
# =============================================================================
# Schema Tests — MetricCreate
# =============================================================================
class TestMetricCreate:
def test_minimal_valid(self):
req = MetricCreate(name="Accuracy Score")
assert req.name == "Accuracy Score"
assert req.category == "accuracy"
assert req.score == 0.0
assert req.threshold == 80.0
assert req.trend == "stable"
assert req.ai_system is None
assert req.last_measured is None
def test_full_values(self):
ts = datetime(2026, 3, 1)
req = MetricCreate(
name="Fairness Score",
category="fairness",
score=92.5,
threshold=85.0,
trend="improving",
ai_system="RecSys-v2",
last_measured=ts,
)
assert req.name == "Fairness Score"
assert req.category == "fairness"
assert req.score == 92.5
assert req.trend == "improving"
assert req.ai_system == "RecSys-v2"
assert req.last_measured == ts
def test_serialization_excludes_none(self):
req = MetricCreate(name="Drift Score", score=75.0)
data = req.model_dump(exclude_none=True)
assert data["name"] == "Drift Score"
assert data["score"] == 75.0
assert "ai_system" not in data
assert "last_measured" not in data
def test_default_trend_stable(self):
req = MetricCreate(name="Test")
assert req.trend == "stable"
def test_default_score_zero(self):
req = MetricCreate(name="Test")
assert req.score == 0.0
# =============================================================================
# Schema Tests — MetricUpdate
# =============================================================================
class TestMetricUpdate:
def test_empty_update(self):
req = MetricUpdate()
data = req.model_dump(exclude_unset=True)
assert data == {}
def test_partial_score_update(self):
req = MetricUpdate(score=95.0)
data = req.model_dump(exclude_unset=True)
assert data == {"score": 95.0}
def test_trend_and_threshold_update(self):
req = MetricUpdate(trend="declining", threshold=90.0)
data = req.model_dump(exclude_unset=True)
assert data["trend"] == "declining"
assert data["threshold"] == 90.0
assert "name" not in data
def test_full_update(self):
req = MetricUpdate(
name="New Name",
category="robustness",
score=88.0,
threshold=85.0,
trend="improving",
ai_system="ModelB",
)
data = req.model_dump(exclude_unset=True)
assert len(data) == 6
assert data["category"] == "robustness"
# =============================================================================
# Schema Tests — TestCreate
# =============================================================================
class TestTestCreate:
def test_minimal_valid(self):
req = TestCreate(name="Bias Detection Test")
assert req.name == "Bias Detection Test"
assert req.status == "pending"
assert req.duration is None
assert req.ai_system is None
assert req.details is None
assert req.last_run is None
def test_full_values(self):
ts = datetime(2026, 3, 1, 12, 0, 0)
req = TestCreate(
name="Accuracy Test Suite",
status="passed",
duration="3.45s",
ai_system="ClassifierV3",
details="All 500 samples passed",
last_run=ts,
)
assert req.status == "passed"
assert req.duration == "3.45s"
assert req.ai_system == "ClassifierV3"
assert req.details == "All 500 samples passed"
assert req.last_run == ts
def test_failed_status(self):
req = TestCreate(name="Fairness Check", status="failed")
assert req.status == "failed"
def test_serialization_excludes_none(self):
req = TestCreate(name="Quick Test", status="passed")
data = req.model_dump(exclude_none=True)
assert "duration" not in data
assert "ai_system" not in data
# =============================================================================
# Schema Tests — TestUpdate
# =============================================================================
class TestTestUpdate:
def test_empty_update(self):
req = TestUpdate()
data = req.model_dump(exclude_unset=True)
assert data == {}
def test_status_update(self):
req = TestUpdate(status="failed")
data = req.model_dump(exclude_unset=True)
assert data == {"status": "failed"}
def test_duration_and_details_update(self):
req = TestUpdate(duration="10.5s", details="Timeout on 3 samples")
data = req.model_dump(exclude_unset=True)
assert data["duration"] == "10.5s"
assert data["details"] == "Timeout on 3 samples"
assert "name" not in data
# =============================================================================
# Helper Tests — _row_to_dict
# =============================================================================
class TestRowToDict:
def test_basic_metric_conversion(self):
row = make_metric_row()
result = _row_to_dict(row)
assert result["id"] == METRIC_ID
assert result["name"] == "Test Metric"
assert result["score"] == 85.0
assert result["threshold"] == 80.0
def test_datetime_serialized(self):
ts = datetime(2024, 6, 15, 9, 0, 0)
row = make_metric_row({"created_at": ts, "last_measured": ts})
result = _row_to_dict(row)
assert result["created_at"] == ts.isoformat()
assert result["last_measured"] == ts.isoformat()
def test_none_values_preserved(self):
row = make_metric_row()
result = _row_to_dict(row)
assert result["ai_system"] is None
def test_uuid_converted_to_string(self):
import uuid
uid = uuid.UUID(DEFAULT_TENANT)
row = MagicMock()
row._mapping = {"id": uid, "tenant_id": uid}
result = _row_to_dict(row)
assert result["id"] == str(uid)
def test_numeric_fields_unchanged(self):
row = MagicMock()
row._mapping = {"score": 92.5, "threshold": 80.0, "count": 10}
result = _row_to_dict(row)
assert result["score"] == 92.5
assert result["threshold"] == 80.0
assert result["count"] == 10
# =============================================================================
# HTTP Tests — GET /quality/stats
# =============================================================================
class TestGetQualityStats:
def test_stats_all_zeros(self, mock_db):
metrics_row = MagicMock()
metrics_row.total_metrics = 0
metrics_row.avg_score = 0
metrics_row.metrics_above_threshold = 0
tests_row = MagicMock()
tests_row.passed = 0
tests_row.failed = 0
tests_row.warning = 0
tests_row.total = 0
mock_db.execute.return_value.fetchone.side_effect = [metrics_row, tests_row]
response = client.get("/quality/stats")
assert response.status_code == 200
data = response.json()
assert data["total_metrics"] == 0
assert data["avg_score"] == 0.0
assert data["metrics_above_threshold"] == 0
assert data["passed"] == 0
assert data["failed"] == 0
assert data["warning"] == 0
assert data["total_tests"] == 0
def test_stats_with_data(self, mock_db):
metrics_row = MagicMock()
metrics_row.total_metrics = 5
metrics_row.avg_score = 87.4
metrics_row.metrics_above_threshold = 4
tests_row = MagicMock()
tests_row.passed = 10
tests_row.failed = 2
tests_row.warning = 1
tests_row.total = 13
mock_db.execute.return_value.fetchone.side_effect = [metrics_row, tests_row]
response = client.get("/quality/stats")
assert response.status_code == 200
data = response.json()
assert data["total_metrics"] == 5
assert data["avg_score"] == 87.4
assert data["metrics_above_threshold"] == 4
assert data["passed"] == 10
assert data["failed"] == 2
assert data["warning"] == 1
assert data["total_tests"] == 13
def test_stats_none_values_become_zero(self, mock_db):
metrics_row = MagicMock()
metrics_row.total_metrics = None
metrics_row.avg_score = None
metrics_row.metrics_above_threshold = None
tests_row = MagicMock()
tests_row.passed = None
tests_row.failed = None
tests_row.warning = None
tests_row.total = None
mock_db.execute.return_value.fetchone.side_effect = [metrics_row, tests_row]
response = client.get("/quality/stats")
assert response.status_code == 200
data = response.json()
assert data["total_metrics"] == 0
assert data["avg_score"] == 0.0
assert data["total_tests"] == 0
def test_stats_with_tenant_header(self, mock_db):
metrics_row = MagicMock()
metrics_row.total_metrics = 2
metrics_row.avg_score = 90.0
metrics_row.metrics_above_threshold = 2
tests_row = MagicMock()
tests_row.passed = 5
tests_row.failed = 0
tests_row.warning = 0
tests_row.total = 5
mock_db.execute.return_value.fetchone.side_effect = [metrics_row, tests_row]
response = client.get(
"/quality/stats",
headers={"X-Tenant-Id": OTHER_TENANT},
)
assert response.status_code == 200
assert response.json()["total_metrics"] == 2
# =============================================================================
# HTTP Tests — GET /quality/metrics
# =============================================================================
class TestListMetrics:
def test_empty_list(self, mock_db):
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 0
execute_result = MagicMock()
execute_result.fetchone.return_value = count_row
execute_result.fetchall.return_value = []
mock_db.execute.return_value = execute_result
response = client.get("/quality/metrics")
assert response.status_code == 200
data = response.json()
assert data["metrics"] == []
assert data["total"] == 0
def test_list_with_data(self, mock_db):
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 2
m1 = make_metric_row()
m2 = make_metric_row({"id": "bbbbbbbb-0002-0002-0002-000000000002", "name": "Second Metric"})
execute_result = MagicMock()
execute_result.fetchone.return_value = count_row
execute_result.fetchall.return_value = [m1, m2]
mock_db.execute.return_value = execute_result
response = client.get("/quality/metrics")
assert response.status_code == 200
data = response.json()
assert len(data["metrics"]) == 2
assert data["total"] == 2
def test_filter_by_category(self, mock_db):
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 1
execute_result = MagicMock()
execute_result.fetchone.return_value = count_row
execute_result.fetchall.return_value = [make_metric_row({"category": "fairness"})]
mock_db.execute.return_value = execute_result
response = client.get("/quality/metrics?category=fairness")
assert response.status_code == 200
data = response.json()
assert data["metrics"][0]["category"] == "fairness"
def test_filter_by_ai_system(self, mock_db):
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 1
execute_result = MagicMock()
execute_result.fetchone.return_value = count_row
execute_result.fetchall.return_value = [make_metric_row({"ai_system": "ModelAlpha"})]
mock_db.execute.return_value = execute_result
response = client.get("/quality/metrics?ai_system=ModelAlpha")
assert response.status_code == 200
data = response.json()
assert data["metrics"][0]["ai_system"] == "ModelAlpha"
def test_pagination(self, mock_db):
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 20
execute_result = MagicMock()
execute_result.fetchone.return_value = count_row
execute_result.fetchall.return_value = [make_metric_row()]
mock_db.execute.return_value = execute_result
response = client.get("/quality/metrics?limit=1&offset=10")
assert response.status_code == 200
data = response.json()
assert data["total"] == 20
assert len(data["metrics"]) == 1
# =============================================================================
# HTTP Tests — POST /quality/metrics
# =============================================================================
class TestCreateMetric:
def test_create_success(self, mock_db):
created_row = make_metric_row({"name": "New Metric"})
mock_db.execute.return_value.fetchone.return_value = created_row
response = client.post(
"/quality/metrics",
json={"name": "New Metric"},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "New Metric"
mock_db.commit.assert_called_once()
def test_create_full_metric(self, mock_db):
created_row = make_metric_row({
"name": "Robustness Score",
"category": "robustness",
"score": 78.5,
"threshold": 75.0,
"trend": "improving",
})
mock_db.execute.return_value.fetchone.return_value = created_row
response = client.post("/quality/metrics", json={
"name": "Robustness Score",
"category": "robustness",
"score": 78.5,
"threshold": 75.0,
"trend": "improving",
})
assert response.status_code == 201
data = response.json()
assert data["category"] == "robustness"
assert data["score"] == 78.5
def test_create_missing_name_fails(self, mock_db):
response = client.post("/quality/metrics", json={"score": 90.0})
assert response.status_code == 422
def test_create_with_tenant_header(self, mock_db):
created_row = make_metric_row({"tenant_id": OTHER_TENANT})
mock_db.execute.return_value.fetchone.return_value = created_row
response = client.post(
"/quality/metrics",
json={"name": "Tenant B metric"},
headers={"X-Tenant-Id": OTHER_TENANT},
)
assert response.status_code == 201
assert mock_db.commit.called
# =============================================================================
# HTTP Tests — PUT /quality/metrics/{id}
# =============================================================================
class TestUpdateMetric:
def test_update_success(self, mock_db):
updated_row = make_metric_row({"score": 95.0, "trend": "improving"})
mock_db.execute.return_value.fetchone.return_value = updated_row
response = client.put(
f"/quality/metrics/{METRIC_ID}",
json={"score": 95.0, "trend": "improving"},
)
assert response.status_code == 200
data = response.json()
assert data["score"] == 95.0
assert data["trend"] == "improving"
mock_db.commit.assert_called_once()
def test_update_not_found_returns_404(self, mock_db):
mock_db.execute.return_value.fetchone.return_value = None
response = client.put(
"/quality/metrics/nonexistent-id",
json={"score": 50.0},
)
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
def test_update_no_fields_returns_400(self, mock_db):
response = client.put(f"/quality/metrics/{METRIC_ID}", json={})
assert response.status_code == 400
def test_partial_update_category(self, mock_db):
updated_row = make_metric_row({"category": "explainability"})
mock_db.execute.return_value.fetchone.return_value = updated_row
response = client.put(
f"/quality/metrics/{METRIC_ID}",
json={"category": "explainability"},
)
assert response.status_code == 200
assert response.json()["category"] == "explainability"
# =============================================================================
# HTTP Tests — DELETE /quality/metrics/{id}
# =============================================================================
class TestDeleteMetric:
def test_delete_success_204(self, mock_db):
delete_result = MagicMock()
delete_result.rowcount = 1
mock_db.execute.return_value = delete_result
response = client.delete(f"/quality/metrics/{METRIC_ID}")
assert response.status_code == 204
mock_db.commit.assert_called_once()
def test_delete_not_found_returns_404(self, mock_db):
delete_result = MagicMock()
delete_result.rowcount = 0
mock_db.execute.return_value = delete_result
response = client.delete("/quality/metrics/nonexistent-id")
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
def test_delete_with_tenant_header(self, mock_db):
delete_result = MagicMock()
delete_result.rowcount = 1
mock_db.execute.return_value = delete_result
response = client.delete(
f"/quality/metrics/{METRIC_ID}",
headers={"X-Tenant-Id": OTHER_TENANT},
)
assert response.status_code == 204
# =============================================================================
# HTTP Tests — GET /quality/tests
# =============================================================================
class TestListQualityTests:
def test_empty_list(self, mock_db):
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 0
execute_result = MagicMock()
execute_result.fetchone.return_value = count_row
execute_result.fetchall.return_value = []
mock_db.execute.return_value = execute_result
response = client.get("/quality/tests")
assert response.status_code == 200
data = response.json()
assert data["tests"] == []
assert data["total"] == 0
def test_list_with_data(self, mock_db):
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 2
t1 = make_test_row()
t2 = make_test_row({"id": "cccccccc-0002-0002-0002-000000000002", "name": "Second Test"})
execute_result = MagicMock()
execute_result.fetchone.return_value = count_row
execute_result.fetchall.return_value = [t1, t2]
mock_db.execute.return_value = execute_result
response = client.get("/quality/tests")
assert response.status_code == 200
data = response.json()
assert len(data["tests"]) == 2
assert data["total"] == 2
def test_filter_by_status(self, mock_db):
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 1
execute_result = MagicMock()
execute_result.fetchone.return_value = count_row
execute_result.fetchall.return_value = [make_test_row({"status": "failed"})]
mock_db.execute.return_value = execute_result
response = client.get("/quality/tests?status=failed")
assert response.status_code == 200
data = response.json()
assert data["tests"][0]["status"] == "failed"
def test_filter_by_ai_system(self, mock_db):
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 1
execute_result = MagicMock()
execute_result.fetchone.return_value = count_row
execute_result.fetchall.return_value = [make_test_row({"ai_system": "ModelBeta"})]
mock_db.execute.return_value = execute_result
response = client.get("/quality/tests?ai_system=ModelBeta")
assert response.status_code == 200
data = response.json()
assert data["tests"][0]["ai_system"] == "ModelBeta"
def test_pagination(self, mock_db):
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 50
execute_result = MagicMock()
execute_result.fetchone.return_value = count_row
execute_result.fetchall.return_value = [make_test_row()]
mock_db.execute.return_value = execute_result
response = client.get("/quality/tests?limit=1&offset=5")
assert response.status_code == 200
assert response.json()["total"] == 50
# =============================================================================
# HTTP Tests — POST /quality/tests
# =============================================================================
class TestCreateQualityTest:
def test_create_success(self, mock_db):
created_row = make_test_row({"name": "New Test Run"})
mock_db.execute.return_value.fetchone.return_value = created_row
response = client.post(
"/quality/tests",
json={"name": "New Test Run"},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "New Test Run"
mock_db.commit.assert_called_once()
def test_create_full_test(self, mock_db):
created_row = make_test_row({
"name": "Full Test Suite",
"status": "passed",
"duration": "5.0s",
"ai_system": "Classifier",
"details": "All assertions passed",
})
mock_db.execute.return_value.fetchone.return_value = created_row
response = client.post("/quality/tests", json={
"name": "Full Test Suite",
"status": "passed",
"duration": "5.0s",
"ai_system": "Classifier",
"details": "All assertions passed",
})
assert response.status_code == 201
data = response.json()
assert data["status"] == "passed"
assert data["duration"] == "5.0s"
def test_create_missing_name_fails(self, mock_db):
response = client.post("/quality/tests", json={"status": "passed"})
assert response.status_code == 422
def test_create_failed_status(self, mock_db):
created_row = make_test_row({"status": "failed"})
mock_db.execute.return_value.fetchone.return_value = created_row
response = client.post(
"/quality/tests",
json={"name": "Failing test", "status": "failed"},
)
assert response.status_code == 201
assert response.json()["status"] == "failed"
# =============================================================================
# HTTP Tests — PUT /quality/tests/{id}
# =============================================================================
class TestUpdateQualityTest:
def test_update_success(self, mock_db):
updated_row = make_test_row({"status": "failed", "details": "Assertion error on line 42"})
mock_db.execute.return_value.fetchone.return_value = updated_row
response = client.put(
f"/quality/tests/{TEST_ID}",
json={"status": "failed", "details": "Assertion error on line 42"},
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "failed"
assert data["details"] == "Assertion error on line 42"
mock_db.commit.assert_called_once()
def test_update_not_found_returns_404(self, mock_db):
mock_db.execute.return_value.fetchone.return_value = None
response = client.put(
"/quality/tests/nonexistent-id",
json={"status": "passed"},
)
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
def test_update_no_fields_returns_400(self, mock_db):
response = client.put(f"/quality/tests/{TEST_ID}", json={})
assert response.status_code == 400
def test_update_duration_only(self, mock_db):
updated_row = make_test_row({"duration": "2.8s"})
mock_db.execute.return_value.fetchone.return_value = updated_row
response = client.put(
f"/quality/tests/{TEST_ID}",
json={"duration": "2.8s"},
)
assert response.status_code == 200
assert response.json()["duration"] == "2.8s"
def test_update_with_tenant_header(self, mock_db):
updated_row = make_test_row({"tenant_id": OTHER_TENANT, "status": "warning"})
mock_db.execute.return_value.fetchone.return_value = updated_row
response = client.put(
f"/quality/tests/{TEST_ID}",
json={"status": "warning"},
headers={"X-Tenant-Id": OTHER_TENANT},
)
assert response.status_code == 200
assert response.json()["status"] == "warning"
# =============================================================================
# HTTP Tests — DELETE /quality/tests/{id}
# =============================================================================
class TestDeleteQualityTest:
def test_delete_success_204(self, mock_db):
delete_result = MagicMock()
delete_result.rowcount = 1
mock_db.execute.return_value = delete_result
response = client.delete(f"/quality/tests/{TEST_ID}")
assert response.status_code == 204
mock_db.commit.assert_called_once()
def test_delete_not_found_returns_404(self, mock_db):
delete_result = MagicMock()
delete_result.rowcount = 0
mock_db.execute.return_value = delete_result
response = client.delete("/quality/tests/nonexistent-id")
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
def test_delete_with_tenant_header(self, mock_db):
delete_result = MagicMock()
delete_result.rowcount = 1
mock_db.execute.return_value = delete_result
response = client.delete(
f"/quality/tests/{TEST_ID}",
headers={"X-Tenant-Id": OTHER_TENANT},
)
assert response.status_code == 204
assert mock_db.commit.called
# =============================================================================
# Tenant Isolation Tests
# =============================================================================
class TestTenantIsolation:
def test_metrics_tenant_isolation(self, mock_db):
"""Tenant A sees 3 metrics, Tenant B sees 0."""
def side_effect(query, params):
result = MagicMock()
tid = params.get("tenant_id", "")
if tid == DEFAULT_TENANT:
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 3
result.fetchone.return_value = count_row
result.fetchall.return_value = [
make_metric_row(),
make_metric_row({"id": "bbbbbbbb-0002-0002-0002-000000000002"}),
make_metric_row({"id": "bbbbbbbb-0003-0003-0003-000000000003"}),
]
else:
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 0
result.fetchone.return_value = count_row
result.fetchall.return_value = []
return result
mock_db.execute.side_effect = side_effect
resp_a = client.get(
"/quality/metrics",
headers={"X-Tenant-Id": DEFAULT_TENANT},
)
assert resp_a.json()["total"] == 3
resp_b = client.get(
"/quality/metrics",
headers={"X-Tenant-Id": OTHER_TENANT},
)
assert resp_b.json()["total"] == 0
def test_tests_tenant_isolation(self, mock_db):
"""Tenant A sees 2 tests, Tenant B sees 0."""
def side_effect(query, params):
result = MagicMock()
tid = params.get("tenant_id", "")
if tid == DEFAULT_TENANT:
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 2
result.fetchone.return_value = count_row
result.fetchall.return_value = [make_test_row(), make_test_row({"id": "cccccccc-0002-0002-0002-000000000002"})]
else:
count_row = MagicMock()
count_row.__getitem__ = lambda self, i: 0
result.fetchone.return_value = count_row
result.fetchall.return_value = []
return result
mock_db.execute.side_effect = side_effect
resp_a = client.get("/quality/tests", headers={"X-Tenant-Id": DEFAULT_TENANT})
assert resp_a.json()["total"] == 2
resp_b = client.get("/quality/tests", headers={"X-Tenant-Id": OTHER_TENANT})
assert resp_b.json()["total"] == 0
def test_invalid_tenant_header_returns_400(self, mock_db):
response = client.get(
"/quality/metrics",
headers={"X-Tenant-Id": "bad-uuid"},
)
assert response.status_code == 400
def test_delete_wrong_tenant_returns_404(self, mock_db):
"""Deleting a metric that belongs to a different tenant returns 404."""
delete_result = MagicMock()
delete_result.rowcount = 0
mock_db.execute.return_value = delete_result
response = client.delete(
f"/quality/metrics/{METRIC_ID}",
headers={"X-Tenant-Id": OTHER_TENANT},
)
assert response.status_code == 404