"""Tests for measurements router (/api/measurements).""" import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from models.user import User from models.recipe import RecipeVersion from models.task import RecipeTask, RecipeSubtask from tests.conftest import auth_headers, create_test_recipe async def _get_subtask_and_version( client: AsyncClient, user: User, recipe_id: int ) -> tuple[int, int]: """Helper: return (subtask_id, version_id) from the recipe's current version.""" tasks_resp = await client.get( f"/api/recipes/{recipe_id}/tasks", headers=auth_headers(user) ) task = tasks_resp.json()[0] subtask_id = task["subtasks"][0]["id"] version_id = task["version_id"] return subtask_id, version_id class TestCreateMeasurement: """POST /api/measurements/ tests.""" async def test_create_measurement( self, client: AsyncClient, measurement_tec_user: User, db_session: AsyncSession, ): """MeasurementTec can create a measurement.""" recipe = await create_test_recipe( db_session, measurement_tec_user.id ) subtask_id, version_id = await _get_subtask_and_version( client, measurement_tec_user, recipe.id ) resp = await client.post( "/api/measurements/", headers=auth_headers(measurement_tec_user), json={ "subtask_id": subtask_id, "version_id": version_id, "value": 10.1, "input_method": "manual", }, ) assert resp.status_code == 200 data = resp.json() assert data["value"] == pytest.approx(10.1, rel=1e-4) assert data["pass_fail"] in ("pass", "warning", "fail") assert data["measured_by"] == measurement_tec_user.id async def test_measurement_requires_auth(self, client: AsyncClient): """Creating measurement without auth returns 401.""" resp = await client.post( "/api/measurements/", json={ "subtask_id": 1, "version_id": 1, "value": 10.0, }, ) assert resp.status_code == 401 async def test_measurement_validation( self, client: AsyncClient, measurement_tec_user: User, ): """Measurement with non-existent subtask returns 400.""" resp = await client.post( "/api/measurements/", headers=auth_headers(measurement_tec_user), json={ "subtask_id": 99999, "version_id": 99999, "value": 10.0, }, ) assert resp.status_code == 400 class TestMeasurementPassFail: """Pass/fail calculation tests.""" async def test_measurement_pass( self, client: AsyncClient, measurement_tec_user: User, db_session: AsyncSession, ): """Value within tolerance -> pass.""" recipe = await create_test_recipe( db_session, measurement_tec_user.id ) subtask_id, version_id = await _get_subtask_and_version( client, measurement_tec_user, recipe.id ) # nominal=10.0, utl=10.5, uwl=10.3, lwl=9.7, ltl=9.5 resp = await client.post( "/api/measurements/", headers=auth_headers(measurement_tec_user), json={ "subtask_id": subtask_id, "version_id": version_id, "value": 10.0, }, ) assert resp.status_code == 200 assert resp.json()["pass_fail"] == "pass" async def test_measurement_warning( self, client: AsyncClient, measurement_tec_user: User, db_session: AsyncSession, ): """Value outside warning but inside tolerance -> warning.""" recipe = await create_test_recipe( db_session, measurement_tec_user.id ) subtask_id, version_id = await _get_subtask_and_version( client, measurement_tec_user, recipe.id ) # uwl=10.3 < 10.4 < utl=10.5 -> warning resp = await client.post( "/api/measurements/", headers=auth_headers(measurement_tec_user), json={ "subtask_id": subtask_id, "version_id": version_id, "value": 10.4, }, ) assert resp.status_code == 200 assert resp.json()["pass_fail"] == "warning" async def test_measurement_fail( self, client: AsyncClient, measurement_tec_user: User, db_session: AsyncSession, ): """Value outside tolerance limits -> fail.""" recipe = await create_test_recipe( db_session, measurement_tec_user.id ) subtask_id, version_id = await _get_subtask_and_version( client, measurement_tec_user, recipe.id ) # value > utl=10.5 -> fail resp = await client.post( "/api/measurements/", headers=auth_headers(measurement_tec_user), json={ "subtask_id": subtask_id, "version_id": version_id, "value": 10.6, }, ) assert resp.status_code == 200 assert resp.json()["pass_fail"] == "fail" class TestListMeasurements: """GET /api/measurements/ tests.""" async def test_list_measurements( self, client: AsyncClient, measurement_tec_user: User, metrologist_user: User, db_session: AsyncSession, ): """Metrologist can list measurements with pagination.""" recipe = await create_test_recipe( db_session, measurement_tec_user.id ) subtask_id, version_id = await _get_subtask_and_version( client, measurement_tec_user, recipe.id ) # Create some measurements for val in [9.8, 10.0, 10.2]: await client.post( "/api/measurements/", headers=auth_headers(measurement_tec_user), json={ "subtask_id": subtask_id, "version_id": version_id, "value": val, }, ) # List as Metrologist resp = await client.get( "/api/measurements/", headers=auth_headers(metrologist_user), ) assert resp.status_code == 200 data = resp.json() assert "items" in data assert "total" in data assert data["total"] >= 3 async def test_measurement_by_recipe_filter( self, client: AsyncClient, measurement_tec_user: User, metrologist_user: User, db_session: AsyncSession, ): """Filtering measurements by recipe_id returns only matching records.""" recipe = await create_test_recipe( db_session, measurement_tec_user.id ) subtask_id, version_id = await _get_subtask_and_version( client, measurement_tec_user, recipe.id ) await client.post( "/api/measurements/", headers=auth_headers(measurement_tec_user), json={ "subtask_id": subtask_id, "version_id": version_id, "value": 10.0, }, ) resp = await client.get( "/api/measurements/", headers=auth_headers(metrologist_user), params={"recipe_id": recipe.id}, ) assert resp.status_code == 200 assert resp.json()["total"] >= 1 class TestMeasurementStatistics: """Measurement deviation / statistics edge case tests.""" async def test_measurement_has_deviation( self, client: AsyncClient, measurement_tec_user: User, db_session: AsyncSession, ): """Measurement response includes deviation from nominal.""" recipe = await create_test_recipe( db_session, measurement_tec_user.id ) subtask_id, version_id = await _get_subtask_and_version( client, measurement_tec_user, recipe.id ) resp = await client.post( "/api/measurements/", headers=auth_headers(measurement_tec_user), json={ "subtask_id": subtask_id, "version_id": version_id, "value": 10.2, }, ) assert resp.status_code == 200 data = resp.json() # deviation = 10.2 - nominal(10.0) = 0.2 assert data["deviation"] is not None assert abs(data["deviation"] - 0.2) < 0.001