"""gitea:Adriano/TieMeasureFlow#72 - frame grab from the station camera. The backend (station.camera_url / camera_source, GET /api/stations/by-code/{code}) is built elsewhere: api_client and requests are mocked here. """ from unittest.mock import MagicMock, patch import requests from tests.conftest import api_get_router STATION = { "id": 1, "code": "ST-TEST", "name": "Linea", "camera_url": "http://host.docker.internal:8765", "camera_source": "camera:folder", } PNG = b"\x89PNG\r\n\x1a\n fake" def _fake(status=200, content=PNG, ctype="image/png", json_body=None): resp = MagicMock(status_code=status, content=content, text=content.decode("latin-1")) resp.headers = {"content-type": ctype} if json_body is None: resp.json.side_effect = ValueError("no json") else: resp.json.return_value = json_body return resp def _station(mock_api_client, monkeypatch, station): monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST") mock_api_client.get_station_by_code.return_value = station def test_grab_without_camera_is_409(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, {**STATION, "camera_url": None}) with patch("blueprints.measure.http_requests.post") as post: resp = logged_in_client.post("/measure/api/vision/grab") assert resp.status_code == 409 assert resp.get_json()["error"] post.assert_not_called() def test_grab_without_source_is_409(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, {**STATION, "camera_source": None}) with patch("blueprints.measure.http_requests.post") as post: resp = logged_in_client.post("/measure/api/vision/grab") assert resp.status_code == 409 post.assert_not_called() def test_grab_station_lookup_error_is_409(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, {"error": True, "status_code": 404, "detail": "x"}) resp = logged_in_client.post("/measure/api/vision/grab") assert resp.status_code == 409 def test_grab_passes_png_through(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, STATION) with patch("blueprints.measure.http_requests.post", return_value=_fake()) as post: resp = logged_in_client.post("/measure/api/vision/grab") assert resp.status_code == 200 assert resp.mimetype == "image/png" assert resp.data == PNG mock_api_client.get_station_by_code.assert_called_once_with("ST-TEST") args, kwargs = post.call_args assert args[0] == "http://host.docker.internal:8765/camera/grab" assert kwargs["json"] == { "source": "camera:folder", "trigger": "continuo", "timeout_ms": 5000, } assert kwargs["timeout"] == 15 def test_grab_service_503_is_502(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, STATION) fake = _fake(503, b"{}", "application/json", {"detail": "camera occupata"}) with patch("blueprints.measure.http_requests.post", return_value=fake): resp = logged_in_client.post("/measure/api/vision/grab") assert resp.status_code == 502 body = resp.get_json() assert body["error"] is True assert "camera occupata" in body["detail"] def test_grab_service_504_and_404_are_502(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, STATION) for status in (404, 504): with patch("blueprints.measure.http_requests.post", return_value=_fake(status, b"nope", "text/plain")): resp = logged_in_client.post("/measure/api/vision/grab") assert resp.status_code == 502 assert resp.get_json()["error"] is True def test_grab_connection_error_is_502(logged_in_client, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, STATION) with patch("blueprints.measure.http_requests.post", side_effect=requests.ConnectionError("refused")): resp = logged_in_client.post("/measure/api/vision/grab") assert resp.status_code == 502 assert resp.get_json()["error"] is True def test_grab_requires_measurement_tec(flask_app, mock_api_client, monkeypatch): _station(mock_api_client, monkeypatch, STATION) with flask_app.test_client() as c: with c.session_transaction() as sess: sess["api_key"] = "k" sess["user"] = {"id": 2, "username": "m", "roles": ["Maker"], "is_admin": False, "active": True} sess["user_id"] = 2 with patch("blueprints.measure.http_requests.post") as post: resp = c.post("/measure/api/vision/grab") assert resp.status_code == 403 post.assert_not_called() def test_grab_requires_login(client): resp = client.post("/measure/api/vision/grab") assert resp.status_code in (302, 401) # --- execute page: button only when the station has a camera ----------------- TASK = { "id": 12, "recipe_id": 3, "version_id": 5, "task_type": "camera_measure", "title": "Foto", "directive": None, "description": None, "file_path": None, "file_type": None, "annotations_json": None, "subtasks": [], } RECIPE = { "id": 3, "code": "REC-1", "name": "Ricetta", "measurement_interval_minutes": 30, "allow_manual_input": True, "current_version": {"id": 5, "version_number": 1}, } def _page(logged_in_client, mock_api_client, monkeypatch, station): _station(mock_api_client, monkeypatch, station) mock_api_client.get_station_recipes.return_value = {"items": [{"id": 3}]} mock_api_client.get.side_effect = api_get_router({ "/api/tasks/12": TASK, "/api/recipes/3/tasks": [{"id": 12, "order_index": 0}], "/api/recipes/3": RECIPE, }) return logged_in_client.get("/measure/execute/12").get_data(as_text=True) def test_execute_page_shows_grab_button_with_camera(logged_in_client, mock_api_client, monkeypatch): html = _page(logged_in_client, mock_api_client, monkeypatch, STATION) assert "/measure/api/vision/grab" in html assert "grab()" in html def test_execute_page_hides_grab_button_without_camera(logged_in_client, mock_api_client, monkeypatch): html = _page(logged_in_client, mock_api_client, monkeypatch, {**STATION, "camera_url": None}) assert "grab()" not in html def test_execute_page_hides_grab_button_when_lookup_fails(logged_in_client, mock_api_client, monkeypatch): html = _page(logged_in_client, mock_api_client, monkeypatch, {"error": True, "status_code": 502, "detail": "down"}) assert "grab()" not in html