feat(roadmap): implementar Sprints 1, 2 y 3 (MVC, Stateless JWT API y Spring Security)

This commit is contained in:
Carlos Tello
2026-09-05 01:46:57 -03:00
parent fa878cb0d5
commit 69b8b7ccef
34 changed files with 1750 additions and 21 deletions
+84
View File
@@ -0,0 +1,84 @@
import unittest
import json
from app import create_app, db
from app.models.user import User
from app.services.jwt_service import JWTService
class TestJWTAndAPI(unittest.TestCase):
def setUp(self):
self.app = create_app()
self.app.config['TESTING'] = True
self.client = self.app.test_client()
self.app_context = self.app.app_context()
self.app_context.push()
# Buscar usuario activo o crear de prueba
self.test_user = User.query.filter_by(is_active=True).first()
if not self.test_user:
self.test_user = User(
email="test_api_admin@unicaba.edu.ar",
name="Test API Admin",
role="ADMIN",
is_active=True
)
self.test_user.set_password("Test1234!")
db.session.add(self.test_user)
db.session.commit()
def tearDown(self):
db.session.rollback()
self.app_context.pop()
def test_jwt_generation_and_decoding(self):
tokens = JWTService.generate_tokens(self.test_user)
self.assertIn('access_token', tokens)
self.assertIn('refresh_token', tokens)
payload = JWTService.decode_token(tokens['access_token'], expected_type='access')
self.assertEqual(payload['sub'], str(self.test_user.id))
self.assertEqual(payload['email'], self.test_user.email)
def test_jwt_revocation(self):
tokens = JWTService.generate_tokens(self.test_user)
token = tokens['access_token']
# Antes de revocar: válido
payload = JWTService.decode_token(token, expected_type='access')
self.assertIsNotNone(payload)
# Revocar
revoked = JWTService.revoke_token(token)
self.assertTrue(revoked)
# Después de revocar: debe lanzar InvalidTokenError
with self.assertRaises(Exception):
JWTService.decode_token(token, expected_type='access')
def test_protected_endpoints_require_token(self):
# Sin token debe dar 401
res = self.client.get('/api/v1/auth/me')
self.assertEqual(res.status_code, 401)
res_classrooms = self.client.get('/api/v1/classrooms')
self.assertEqual(res_classrooms.status_code, 401)
def test_authorized_endpoint_access(self):
tokens = JWTService.generate_tokens(self.test_user)
headers = {'Authorization': f"Bearer {tokens['access_token']}"}
res = self.client.get('/api/v1/auth/me', headers=headers)
self.assertEqual(res.status_code, 200)
data = res.get_json()
self.assertEqual(data['email'], self.test_user.email)
res_classrooms = self.client.get('/api/v1/classrooms', headers=headers)
self.assertEqual(res_classrooms.status_code, 200)
def test_token_refresh_flow(self):
tokens = JWTService.generate_tokens(self.test_user)
res = self.client.post('/api/v1/auth/refresh', json={'refresh_token': tokens['refresh_token']})
self.assertEqual(res.status_code, 200)
data = res.get_json()
self.assertIn('access_token', data)
if __name__ == '__main__':
unittest.main()
+42
View File
@@ -0,0 +1,42 @@
import unittest
from app import create_app, db
from app.models.audit_log import AuditLog
from app.services.audit_service import AuditService
class TestSecurityChainAndAudit(unittest.TestCase):
def setUp(self):
self.app = create_app()
self.app.config['TESTING'] = True
self.client = self.app.test_client()
self.app_context = self.app.app_context()
self.app_context.push()
def tearDown(self):
db.session.rollback()
self.app_context.pop()
def test_security_headers_injected(self):
res = self.client.get('/login')
self.assertIn('X-Content-Type-Options', res.headers)
self.assertEqual(res.headers['X-Content-Type-Options'], 'nosniff')
self.assertIn('X-Frame-Options', res.headers)
self.assertEqual(res.headers['X-Frame-Options'], 'SAMEORIGIN')
self.assertIn('X-XSS-Protection', res.headers)
self.assertEqual(res.headers['X-XSS-Protection'], '1; mode=block')
def test_audit_log_creation(self):
entry = AuditService.log(
action='TEST_ACTION',
module='classrooms',
entity_id=999,
details={'test': True, 'desc': 'Prueba unitaria de auditoría'}
)
self.assertIsNotNone(entry.id)
self.assertEqual(entry.action, 'TEST_ACTION')
self.assertEqual(entry.module, 'classrooms')
recent = AuditService.get_recent_logs(limit=5, module='classrooms')
self.assertTrue(any(e.id == entry.id for e in recent))
if __name__ == '__main__':
unittest.main()
+60
View File
@@ -0,0 +1,60 @@
import unittest
from datetime import datetime, timedelta
from app import create_app, db
from app.models.user import User
from app.models.classroom import Classroom
from app.models.building import Building
from app.models.subject import Commission
from app.schemas.classroom_dto import ClassroomCreateDTO
from app.schemas.reservation_dto import ReservationCreateDTO
from app.services.classroom_service import ClassroomService
from app.services.reservation_service import ReservationService
from app.services.user_service import UserService
class TestServicesAndDTO(unittest.TestCase):
def setUp(self):
self.app = create_app()
self.app.config['TESTING'] = True
self.app.config['WTF_CSRF_ENABLED'] = False
self.app_context = self.app.app_context()
self.app_context.push()
self.classroom_service = ClassroomService()
self.reservation_service = ReservationService()
self.user_service = UserService()
def tearDown(self):
db.session.rollback()
self.app_context.pop()
def test_dto_validation(self):
# Valid ClassroomCreateDTO
dto = ClassroomCreateDTO(code="Aula Test 101", floor="1", capacity=40, building_name="Edificio Central")
self.assertEqual(dto.code, "Aula Test 101")
self.assertEqual(dto.capacity, 40)
# Invalid Reservation range (end before start)
with self.assertRaises(Exception):
ReservationCreateDTO(
classroom_id=1,
commission_id=1,
user_id=1,
start_time=datetime(2026, 9, 10, 10, 0),
end_time=datetime(2026, 9, 10, 9, 0),
purpose="Clase Test"
)
def test_classroom_service_listing(self):
classrooms = self.classroom_service.list_classrooms()
self.assertIsInstance(classrooms, list)
def test_user_service_profile(self):
user = User.query.first()
if user:
profile = self.user_service.get_profile_data(user)
self.assertIn('email', profile)
self.assertIn('role', profile)
self.assertIn('permissions', profile)
if __name__ == '__main__':
unittest.main()