"""Tests for statistics blueprint (client/blueprints/statistics.py). Covers dashboard rendering, role enforcement, and SPC/summary API proxies. """ import pytest class TestDashboard: """GET /statistics/dashboard tests.""" def test_dashboard_renders(self, logged_in_client, mock_api_client): """Dashboard renders for Metrologist role.""" mock_api_client.get.return_value = { "items": [ {"id": 1, "code": "REC-001", "name": "Recipe A"}, ], "total": 1, "pages": 1, } resp = logged_in_client.get("/statistics/dashboard") assert resp.status_code == 200 def test_dashboard_requires_login(self, client): """Unauthenticated user is redirected to login.""" resp = client.get("/statistics/dashboard", follow_redirects=False) assert resp.status_code == 302 assert "/auth/login" in resp.headers["Location"] def test_dashboard_pages_past_the_first_hundred_recipes( self, logged_in_client, mock_api_client, ): """#37.6: a single per_page=100 call used to drop every recipe past #100.""" page1 = { "items": [{"id": i, "code": f"REC-{i:03d}"} for i in range(100)], "total": 101, "pages": 2, } page2 = { "items": [{"id": 100, "code": "REC-100"}], "total": 101, "pages": 2, } mock_api_client.get.side_effect = [page1, page2] resp = logged_in_client.get("/statistics/dashboard") assert resp.status_code == 200 assert mock_api_client.get.call_count == 2 assert "REC-100" in resp.get_data(as_text=True) first_call = mock_api_client.get.call_args_list[0] second_call = mock_api_client.get.call_args_list[1] assert first_call.kwargs["params"]["page"] == 1 assert second_call.kwargs["params"]["page"] == 2 class TestRoleRequired: """Metrologist role enforcement tests.""" def test_requires_metrologist_role(self, flask_app): """User without Metrologist role gets 403 on statistics endpoints.""" with flask_app.test_client() as c: with c.session_transaction() as sess: sess["api_key"] = "test-key" sess["user"] = { "id": 3, "username": "maker_only", "display_name": "Maker Only", "roles": ["Maker"], "is_admin": False, "language_pref": "en", "theme_pref": "light", "active": True, } sess["user_id"] = 3 sess["language"] = "en" sess["theme"] = "light" resp = c.get("/statistics/dashboard") assert resp.status_code == 403 class TestSPCProxy: """SPC data proxy endpoint tests.""" def test_summary_proxy(self, logged_in_client, mock_api_client): """Summary API proxy returns JSON from backend.""" mock_api_client.get.return_value = { "total": 100, "pass_count": 90, "fail_count": 5, "warning_count": 5, } resp = logged_in_client.get( "/statistics/api/summary?recipe_id=1" ) assert resp.status_code == 200 data = resp.get_json() assert data["total"] == 100 def test_capability_proxy(self, logged_in_client, mock_api_client): """Capability API proxy returns JSON from backend.""" mock_api_client.get.return_value = { "cp": 1.33, "cpk": 1.20, } resp = logged_in_client.get( "/statistics/api/capability?recipe_id=1&subtask_id=1" ) assert resp.status_code == 200 data = resp.get_json() assert "cp" in data class TestRunsProxy: """#58: filtro per sessione di produzione.""" def test_runs_proxy(self, logged_in_client, mock_api_client): mock_api_client.get.return_value = [{"id": 5, "measurement_count": 3}] resp = logged_in_client.get("/statistics/api/runs?recipe_id=7") assert resp.status_code == 200 assert resp.get_json()[0]["id"] == 5 args, kwargs = mock_api_client.get.call_args assert args[0] == "/api/statistics/runs" assert kwargs["params"] == {"recipe_id": "7"} def test_summary_forwards_production_run_id(self, logged_in_client, mock_api_client): mock_api_client.get.return_value = {"total": 1} logged_in_client.get("/statistics/api/summary?recipe_id=1&production_run_id=5") assert mock_api_client.get.call_args.kwargs["params"]["production_run_id"] == "5" class TestRunTaskResultsProxy: """#58: esiti dei task e allegati della sessione.""" def test_task_results_proxy(self, logged_in_client, mock_api_client): payload = {"run_id": 5, "counts": {"ok": 1, "nok": 0, "done": 0}, "results": []} mock_api_client.get.return_value = payload resp = logged_in_client.get("/statistics/api/runs/5/task-results") assert resp.status_code == 200 assert resp.get_json() == payload assert mock_api_client.get.call_args.args[0] == "/api/statistics/runs/5/task-results" def test_task_results_proxy_relays_error(self, logged_in_client, mock_api_client): mock_api_client.get.return_value = {"error": True, "detail": "x", "status_code": 404} resp = logged_in_client.get("/statistics/api/runs/9/task-results") assert resp.status_code == 404 assert resp.get_json()["error"] is True def test_file_proxy_serves_task_results_only(self, logged_in_client): from unittest.mock import patch from flask import Response with patch("blueprints.statistics.proxy_file", return_value=Response("img", status=200)) as pf: ok = logged_in_client.get("/statistics/api/files/task_results/a.jpg") assert ok.status_code == 200 pf.assert_called_once_with("task_results/a.jpg") for bad in ("statistics/x", "task_results/../statistics/x", "../etc/passwd"): assert logged_in_client.get(f"/statistics/api/files/{bad}").status_code == 404 assert pf.call_count == 1 # i percorsi rifiutati non arrivano al proxy def test_dashboard_has_task_results_panel(self, logged_in_client, mock_api_client): mock_api_client.get.return_value = {"items": [], "total": 0, "pages": 1} html = logged_in_client.get("/statistics/dashboard").get_data(as_text=True) assert 'id="run-task-results"' in html assert "loadTaskResults" in html assert "/statistics/api/runs/" in html