Files
TieMeasureFlow/server/tests/test_users.py
Adriano dd2ebf863a feat: FASE 7 - Polish & Testing (security, i18n, test suite, docs)
Security hardening: CORS lockdown, rate limiting middleware con sliding
window e eviction IP stale, security headers (CSP, HSTS, X-Frame-Options),
session cookie hardening, filename sanitization upload.

i18n completion: internazionalizzati barcode.js e csv-export.js con bridge
window.BARCODE_I18N/CSV_I18N, aggiornati .po IT/EN con 27 nuove stringhe.

Tablet UX: touch target 44px per dispositivi coarse pointer.

Test suite: 101 test totali (76 server + 25 client), copertura completa
di tutti i router API, autenticazione, ruoli, CRUD, SPC, file upload,
security integration. Infrastruttura SQLite async in-memory con fixtures.

Fix critici: MissingGreenlet in recipe_service (selectinload eager),
route ordering tasks.py, auth_service bcrypt diretto, Measurement.id
Integer per SQLite.

Documentazione: API.md (riferimento completo 40+ endpoint),
DEPLOYMENT.md (guida produzione con Docker/Nginx/SSL),
USER_GUIDE.md (manuale utente per ruolo).

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

166 lines
5.2 KiB
Python

"""Tests for users router (/api/users)."""
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from models.user import User
from tests.conftest import _create_user, auth_headers
class TestListUsers:
"""GET /api/users tests."""
async def test_list_users_admin(
self, client: AsyncClient, admin_user: User
):
"""Admin can list all users."""
resp = await client.get("/api/users", headers=auth_headers(admin_user))
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
# At least the admin user
assert len(data) >= 1
usernames = [u["username"] for u in data]
assert "admin" in usernames
async def test_list_users_forbidden(
self, client: AsyncClient, maker_user: User
):
"""Non-admin user cannot list users."""
resp = await client.get("/api/users", headers=auth_headers(maker_user))
assert resp.status_code == 403
class TestGetUser:
"""GET /api/users/{user_id} is not directly exposed; use get_me or list."""
async def test_get_me(self, client: AsyncClient, maker_user: User):
"""GET /api/auth/me returns current user profile."""
resp = await client.get(
"/api/auth/me", headers=auth_headers(maker_user)
)
assert resp.status_code == 200
data = resp.json()
assert data["username"] == "maker"
assert data["display_name"] == "Maker User"
assert "Maker" in data["roles"]
class TestCreateUser:
"""POST /api/users tests."""
async def test_create_user(
self, client: AsyncClient, admin_user: User
):
"""Admin can create a new user."""
resp = await client.post(
"/api/users",
headers=auth_headers(admin_user),
json={
"username": "newuser",
"password": "NewPass123",
"display_name": "New User",
"roles": ["MeasurementTec"],
"is_admin": False,
},
)
assert resp.status_code == 201
data = resp.json()
assert data["username"] == "newuser"
assert data["display_name"] == "New User"
assert "MeasurementTec" in data["roles"]
assert data["active"] is True
async def test_create_user_duplicate_username(
self, client: AsyncClient, admin_user: User
):
"""Creating a user with existing username returns 409."""
resp = await client.post(
"/api/users",
headers=auth_headers(admin_user),
json={
"username": "admin",
"password": "DupPass123",
"display_name": "Dup",
"roles": [],
},
)
assert resp.status_code == 409
class TestUpdateUser:
"""PUT /api/users/{user_id} tests."""
async def test_update_user(
self,
client: AsyncClient,
admin_user: User,
maker_user: User,
):
"""Admin can update another user."""
resp = await client.put(
f"/api/users/{maker_user.id}",
headers=auth_headers(admin_user),
json={"display_name": "Updated Maker"},
)
assert resp.status_code == 200
assert resp.json()["display_name"] == "Updated Maker"
class TestDeleteUser:
"""DELETE /api/users/{user_id} tests."""
async def test_delete_user(
self,
client: AsyncClient,
admin_user: User,
db_session: AsyncSession,
):
"""Admin can soft-delete another user."""
target = await _create_user(
db_session, username="to_delete", display_name="Delete Me"
)
resp = await client.delete(
f"/api/users/{target.id}", headers=auth_headers(admin_user)
)
assert resp.status_code == 204
async def test_delete_self_forbidden(
self, client: AsyncClient, admin_user: User
):
"""Admin cannot deactivate themselves."""
resp = await client.delete(
f"/api/users/{admin_user.id}", headers=auth_headers(admin_user)
)
assert resp.status_code == 400
assert "yourself" in resp.json()["detail"]
class TestProfile:
"""PUT /api/auth/me tests."""
async def test_update_me(self, client: AsyncClient, maker_user: User):
"""User can update own profile."""
resp = await client.put(
"/api/auth/me",
headers=auth_headers(maker_user),
json={"display_name": "My New Name", "theme_pref": "dark"},
)
assert resp.status_code == 200
data = resp.json()
assert data["display_name"] == "My New Name"
assert data["theme_pref"] == "dark"
async def test_change_password_not_in_profile(
self, client: AsyncClient, maker_user: User
):
"""Profile update schema does not accept password field (422)."""
resp = await client.put(
"/api/auth/me",
headers=auth_headers(maker_user),
json={"password": "HackedPass1"},
)
# The field is simply ignored (exclude_unset), so no change should occur
# but the request itself is valid (empty update)
assert resp.status_code == 200