Files
service_manager/backend/tests/integration/test_auth_integration.py
Ernesto Herrera 16d795e8bd feat: Implementar suite completa de tests de integración v1.9.0
- Agregar 46 tests de integración (auth, multi-tenancy, tickets)
- Crear estructura organizada tests/integration/ y tests/unit/
- Implementar fixtures completas para testing con BD separada
- Agregar conftest_integration.py con setup async
- Mover scripts PowerShell de testing a tests/scripts/
- Actualizar pytest.ini con markers y configuración
- Crear run_tests.sh script ejecutable para testing
- Documentación completa en README_TESTS.md
- Fix: Remover opciones obsoletas de TypeScript (importsNotUsedAsValues)

Tests implementados:
- Authentication: 15 tests (login, refresh, permisos, seguridad)
- Multi-tenancy: 13 tests (aislamiento, validaciones, seguridad B2B)
- Tickets: 18 tests (CRUD, filtros, permisos por rol)
- Unit: 10 tests básicos
- Verificación: 8 tests de setup

Base de datos de testing: servicemanager_test (separada de producción)
Cobertura estimada: ~40% (desde 5%)

Próximos pasos: Agregar tests de SLA, attachments, auditoría
2026-02-18 13:08:32 -07:00

365 lines
10 KiB
Python

"""
Authentication Integration Tests - ServiceManagerWeb
Tests completos del flujo de autenticación incluyendo:
- Login
- Refresh tokens
- Logout
- Permisos y roles
"""
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user import User, UserRole
from app.models.tenant import Tenant
# Importar fixtures desde conftest_integration
pytest_plugins = ['tests.conftest_integration']
@pytest.mark.integration
@pytest.mark.auth
class TestAuthentication:
"""Tests de autenticación básica."""
async def test_login_success(
self,
client: AsyncClient,
test_admin_user: User,
test_tenant: Tenant
):
"""Test login exitoso con credenciales válidas."""
response = await client.post(
"/v1/auth/login",
json={
"email": "admin@test.com",
"password": "AdminPass123!",
"tenant_slug": test_tenant.slug
}
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "refresh_token" in data
assert data["token_type"] == "bearer"
assert data["expires_in"] > 0
assert data["user"]["email"] == "admin@test.com"
assert data["user"]["role"] == "ADMIN"
async def test_login_invalid_password(
self,
client: AsyncClient,
test_admin_user: User,
test_tenant: Tenant
):
"""Test login con contraseña incorrecta."""
response = await client.post(
"/v1/auth/login",
json={
"email": "admin@test.com",
"password": "WrongPassword123!",
"tenant_slug": test_tenant.slug
}
)
assert response.status_code == 401
assert "Invalid credentials" in response.json()["detail"]
async def test_login_invalid_tenant_slug(
self,
client: AsyncClient,
test_admin_user: User
):
"""Test login con tenant slug inexistente."""
response = await client.post(
"/v1/auth/login",
json={
"email": "admin@test.com",
"password": "AdminPass123!",
"tenant_slug": "nonexistent-tenant"
}
)
assert response.status_code == 404
async def test_login_user_not_found(
self,
client: AsyncClient,
test_tenant: Tenant
):
"""Test login con email inexistente."""
response = await client.post(
"/v1/auth/login",
json={
"email": "notfound@test.com",
"password": "SomePassword123!",
"tenant_slug": test_tenant.slug
}
)
assert response.status_code == 401
async def test_login_inactive_user(
self,
client: AsyncClient,
db_session: AsyncSession,
test_admin_user: User,
test_tenant: Tenant
):
"""Test login con usuario desactivado."""
# Desactivar usuario
test_admin_user.is_active = False
await db_session.commit()
response = await client.post(
"/v1/auth/login",
json={
"email": "admin@test.com",
"password": "AdminPass123!",
"tenant_slug": test_tenant.slug
}
)
assert response.status_code == 403
@pytest.mark.integration
@pytest.mark.auth
class TestRefreshToken:
"""Tests de refresh tokens."""
async def test_refresh_token_success(
self,
client: AsyncClient,
test_admin_user: User,
test_tenant: Tenant
):
"""Test refresh token exitoso."""
# Login para obtener tokens
login_response = await client.post(
"/v1/auth/login",
json={
"email": "admin@test.com",
"password": "AdminPass123!",
"tenant_slug": test_tenant.slug
}
)
assert login_response.status_code == 200
refresh_token = login_response.json()["refresh_token"]
# Usar refresh token
refresh_response = await client.post(
"/v1/auth/refresh",
json={"refresh_token": refresh_token}
)
assert refresh_response.status_code == 200
data = refresh_response.json()
assert "access_token" in data
assert data["token_type"] == "bearer"
assert data["expires_in"] > 0
async def test_refresh_token_invalid(self, client: AsyncClient):
"""Test refresh con token inválido."""
response = await client.post(
"/v1/auth/refresh",
json={"refresh_token": "invalid-token"}
)
assert response.status_code == 401
async def test_refresh_token_after_logout(
self,
client: AsyncClient,
test_admin_user: User,
test_tenant: Tenant,
admin_token: str
):
"""Test que refresh token no funciona después de logout."""
# Login
login_response = await client.post(
"/v1/auth/login",
json={
"email": "admin@test.com",
"password": "AdminPass123!",
"tenant_slug": test_tenant.slug
}
)
refresh_token = login_response.json()["refresh_token"]
# Logout
logout_response = await client.post(
"/v1/auth/logout",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert logout_response.status_code == 200
# Intentar usar refresh token después de logout
refresh_response = await client.post(
"/v1/auth/refresh",
json={"refresh_token": refresh_token}
)
assert refresh_response.status_code == 401
@pytest.mark.integration
@pytest.mark.auth
class TestAuthorization:
"""Tests de autorización y permisos."""
async def test_admin_can_access_admin_endpoint(
self,
client: AsyncClient,
test_tenant: Tenant,
auth_headers_admin: dict
):
"""Test que admin puede acceder a endpoints de admin."""
response = await client.get(
"/v1/tenants/",
headers={
**auth_headers_admin,
"X-Tenant-ID": str(test_tenant.id)
}
)
assert response.status_code == 200
async def test_agent_cannot_access_admin_endpoint(
self,
client: AsyncClient,
test_tenant: Tenant,
auth_headers_agent: dict
):
"""Test que agent no puede acceder a endpoints de admin."""
response = await client.get(
"/v1/tenants/",
headers={
**auth_headers_agent,
"X-Tenant-ID": str(test_tenant.id)
}
)
assert response.status_code == 403
async def test_client_cannot_access_admin_endpoint(
self,
client: AsyncClient,
test_tenant: Tenant,
auth_headers_client: dict
):
"""Test que client no puede acceder a endpoints de admin."""
response = await client.get(
"/v1/tenants/",
headers={
**auth_headers_client,
"X-Tenant-ID": str(test_tenant.id)
}
)
assert response.status_code == 403
async def test_protected_endpoint_without_token(
self,
client: AsyncClient,
test_tenant: Tenant
):
"""Test que endpoints protegidos requieren token."""
response = await client.get(
"/v1/tickets/",
headers={"X-Tenant-ID": str(test_tenant.id)}
)
assert response.status_code == 401
async def test_protected_endpoint_with_invalid_token(
self,
client: AsyncClient,
test_tenant: Tenant
):
"""Test con token inválido."""
response = await client.get(
"/v1/tickets/",
headers={
"Authorization": "Bearer invalid-token",
"X-Tenant-ID": str(test_tenant.id)
}
)
assert response.status_code == 401
@pytest.mark.integration
@pytest.mark.auth
class TestUserProfile:
"""Tests del perfil de usuario."""
async def test_get_current_user_profile(
self,
client: AsyncClient,
test_admin_user: User,
auth_headers_admin: dict
):
"""Test obtener perfil del usuario actual."""
response = await client.get(
"/v1/users/me",
headers=auth_headers_admin
)
assert response.status_code == 200
data = response.json()
assert data["email"] == "admin@test.com"
assert data["role"] == "ADMIN"
assert data["first_name"] == "Admin"
assert data["last_name"] == "User"
assert "password_hash" not in data # No debe exponer password
@pytest.mark.integration
@pytest.mark.auth
class TestPasswordSecurity:
"""Tests de seguridad de contraseñas."""
async def test_password_hashing(self):
"""Test que las contraseñas se hashean correctamente."""
from app.core.security import SecurityUtils
password = "TestPassword123!"
hashed = SecurityUtils.hash_password(password)
# Debe ser diferente del original
assert hashed != password
# Debe poder verificarse
assert SecurityUtils.verify_password(password, hashed)
# Contraseña incorrecta no debe verificar
assert not SecurityUtils.verify_password("WrongPassword", hashed)
async def test_password_not_exposed_in_response(
self,
client: AsyncClient,
test_admin_user: User,
auth_headers_admin: dict
):
"""Test que el password hash nunca se expone en las respuestas."""
response = await client.get(
"/v1/users/me",
headers=auth_headers_admin
)
assert response.status_code == 200
data = response.json()
assert "password" not in data
assert "password_hash" not in data