Files
TieMeasureFlow/src/backend/tests/test_reports.py
T
Adriano 1a0431366f chore(v2): restructure monorepo to src/ layout with uv
Aligns the repo with the python-project-spec-design.md template chosen
for V2.0.0. Big move, no logic changes. The 3 pre-existing test
failures (test_recipes::test_update_recipe, test_recipes::
test_recipe_versioning, test_tasks::test_reorder_tasks, plus the
client test_save_measurement_proxy) survive unchanged.

Layout changes
- server/        -> src/backend/
- server/middleware/ -> src/backend/api/middleware/
- server/routers/    -> src/backend/api/routers/
- server/models/     -> src/backend/models/orm/
- server/schemas/    -> src/backend/models/api/
- server/uploads/    -> uploads/ (project root, mounted volume)
- server/tests/      -> src/backend/tests/
- client/            -> src/frontend/flask_app/ (Flask kept; React
  deroga is documented in CLAUDE.md, justified by tablet UX, USB
  caliper/barcode workflow and Fabric.js integration)

Tooling
- pyproject.toml: monorepo with [project] core deps and
  optional-dependencies server / client / dev. Replaces both
  server/requirements.txt and client/requirements.txt.
- uv.lock + .python-version (3.11) committed for reproducible builds.
- Dockerfile (root, backend) and Dockerfile.frontend rewritten to use
  uv sync --frozen --no-dev --extra server|client; legacy Dockerfiles
  preserved as Dockerfile.legacy for reference but excluded from build
  context via .dockerignore.
- docker-compose.dev.yml + docker-compose.yml: build context now ".",
  dockerfile pointing to the root files.

Code adjustments forced by the move
- Every "from config|database|models|schemas|services|routers|middleware
  import ..." rewritten to its src.backend.* equivalent (50+ files
  including indented inline imports inside test bodies).
- src/backend/migrations/env.py: insert project root into sys.path so
  alembic can resolve src.backend.* imports regardless of cwd.
- src/backend/config.py: env_file ../../.env (was ../.env), upload_path
  resolves project root via parents[2].
- src/backend/tests/conftest.py + tests: import ... from src.backend.*
  instead of bare names; old per-directory pytest.ini files removed in
  favor of root pyproject.toml [tool.pytest.ini_options].
- .gitignore: uploads/ at root, src/frontend/flask_app/static/css/
  tailwind.css path; .dockerignore tightened.
- CLAUDE.md: rewrote sections "Layout del repository", "Comandi di
  Sviluppo", "Database & Migrations", "Test", "i18n", and all path
  references throughout the architecture sections.

Verified
- uv lock resolves 77 packages; uv sync --extra server --extra client
  --extra dev installs cleanly.
- uv run pytest: 171 passed, 4 pre-existing failures.
- uv run alembic -c src/backend/migrations/alembic.ini check loads
  config and metadata (errors only on the absent local MySQL).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:26:47 +02:00

108 lines
3.4 KiB
Python

"""Tests for reports router (/api/reports).
Note: Report generation requires WeasyPrint/Kaleido and Jinja2 templates.
These tests mock the PDF generation to test endpoint plumbing and auth.
"""
from unittest.mock import AsyncMock, patch
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from src.backend.models.orm.user import User
from src.backend.tests.conftest import auth_headers, create_test_recipe
class TestMeasurementReport:
"""GET /api/reports/measurements tests."""
async def test_generate_measurement_report(
self,
client: AsyncClient,
metrologist_user: User,
db_session: AsyncSession,
):
"""Metrologist can generate a measurement report (mocked PDF)."""
recipe = await create_test_recipe(
db_session, metrologist_user.id
)
fake_pdf = b"%PDF-1.4 fake content"
with patch(
"src.backend.api.routers.reports.generate_measurement_report",
new_callable=AsyncMock,
return_value=fake_pdf,
):
resp = await client.get(
"/api/reports/measurements",
headers=auth_headers(metrologist_user),
params={"recipe_id": recipe.id},
)
assert resp.status_code == 200
assert resp.headers["content-type"] == "application/pdf"
assert b"%PDF" in resp.content
async def test_report_requires_auth(self, client: AsyncClient):
"""Report endpoints require authentication."""
resp = await client.get(
"/api/reports/measurements",
params={"recipe_id": 1},
)
assert resp.status_code == 401
class TestSPCReport:
"""GET /api/reports/spc tests."""
async def test_generate_spc_report(
self,
client: AsyncClient,
metrologist_user: User,
db_session: AsyncSession,
):
"""Metrologist can generate an SPC report (mocked PDF)."""
recipe = await create_test_recipe(
db_session, metrologist_user.id
)
# Get subtask_id
tasks_resp = await client.get(
f"/api/recipes/{recipe.id}/tasks",
headers=auth_headers(metrologist_user),
)
subtask_id = tasks_resp.json()[0]["subtasks"][0]["id"]
fake_pdf = b"%PDF-1.4 spc report content"
with patch(
"src.backend.api.routers.reports.generate_spc_report",
new_callable=AsyncMock,
return_value=fake_pdf,
):
resp = await client.get(
"/api/reports/spc",
headers=auth_headers(metrologist_user),
params={
"recipe_id": recipe.id,
"subtask_id": subtask_id,
},
)
assert resp.status_code == 200
assert resp.headers["content-type"] == "application/pdf"
async def test_spc_report_requires_metrologist(
self,
client: AsyncClient,
maker_user: User,
db_session: AsyncSession,
):
"""Non-Metrologist cannot generate SPC reports."""
recipe = await create_test_recipe(db_session, maker_user.id)
resp = await client.get(
"/api/reports/spc",
headers=auth_headers(maker_user),
params={"recipe_id": recipe.id, "subtask_id": 1},
)
assert resp.status_code == 403