From 902e30a2613ad620539e876bbea9f581bcae59fb Mon Sep 17 00:00:00 2001 From: Carlos Tello Date: Wed, 23 Sep 2026 14:05:48 -0300 Subject: [PATCH] chore: depurar scripts obsoletos de migracion y pruebas legacy no requeridas --- .gitignore | 2 +- backend/app/models/genetic_algorithm.py | 2 +- backend/migrate_phase2_fields.py | 44 -- backend/migrate_phase3_fields.py | 69 --- backend/migrate_phase6_settings.py | 24 - backend/migrate_phase6_sync_tasks.py | 29 - backend/migrate_user_profile_fields.py | 79 --- backend/test_batch_coupling.py | 48 -- backend/test_integration.py | 77 --- .../tests/test_enrollment_and_commissions.py | 539 ------------------ backend/tests/test_role_dashboard_and_menu.py | 103 ---- backend/tests/test_security_chain.py | 42 -- backend/tests/test_services_and_dto.py | 60 -- .../test_sprint4_optimizer_and_concurrency.py | 21 +- backend/tests/test_view_switcher.py | 45 -- 15 files changed, 16 insertions(+), 1168 deletions(-) delete mode 100644 backend/migrate_phase2_fields.py delete mode 100644 backend/migrate_phase3_fields.py delete mode 100644 backend/migrate_phase6_settings.py delete mode 100644 backend/migrate_phase6_sync_tasks.py delete mode 100644 backend/migrate_user_profile_fields.py delete mode 100644 backend/test_batch_coupling.py delete mode 100644 backend/test_integration.py delete mode 100644 backend/tests/test_enrollment_and_commissions.py delete mode 100644 backend/tests/test_role_dashboard_and_menu.py delete mode 100644 backend/tests/test_security_chain.py delete mode 100644 backend/tests/test_services_and_dto.py delete mode 100644 backend/tests/test_view_switcher.py diff --git a/.gitignore b/.gitignore index 6dc4531..01e43c8 100644 --- a/.gitignore +++ b/.gitignore @@ -223,4 +223,4 @@ legacy_admin-edu-space/ .vscode/ .schemathesis/ -AlumnosLS \ No newline at end of file +AlumnosLS diff --git a/backend/app/models/genetic_algorithm.py b/backend/app/models/genetic_algorithm.py index 6926690..7adb554 100644 --- a/backend/app/models/genetic_algorithm.py +++ b/backend/app/models/genetic_algorithm.py @@ -99,7 +99,7 @@ class Individual: score += conflict_score * 0.2 # Factor 4: Resource matching (10% weight) - resource_score = self._calculate_resource_score(classroom, request.subject_requirements) + resource_score = self._calculate_resource_score(classroom, res_req.subject_requirements) score += resource_score * 0.1 self.fitness = score diff --git a/backend/migrate_phase2_fields.py b/backend/migrate_phase2_fields.py deleted file mode 100644 index 7bb177e..0000000 --- a/backend/migrate_phase2_fields.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Migration script for Phase 2 MVP fields: -- subjects.is_short_course (BOOLEAN DEFAULT 0) -- student_enrollments.allow_same_day_exception (BOOLEAN DEFAULT 0) -- student_enrollments.exception_reason (VARCHAR(255)) -""" -import sqlite3 -import os - -def migrate(): - db_paths = [ - os.path.join(os.path.dirname(__file__), 'instance', 'app.db'), - os.path.join(os.path.dirname(__file__), 'app.db') - ] - for db_path in db_paths: - if not os.path.exists(db_path): - continue - print(f"Migrating SQLite DB: {db_path}") - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - # Check subjects.is_short_course - cursor.execute("PRAGMA table_info(subjects)") - columns = [row[1] for row in cursor.fetchall()] - if 'is_short_course' not in columns: - print("Adding column 'is_short_course' to subjects table...") - cursor.execute("ALTER TABLE subjects ADD COLUMN is_short_course BOOLEAN DEFAULT 0") - - # Check student_enrollments.allow_same_day_exception - cursor.execute("PRAGMA table_info(student_enrollments)") - enr_columns = [row[1] for row in cursor.fetchall()] - if 'allow_same_day_exception' not in enr_columns: - print("Adding column 'allow_same_day_exception' to student_enrollments table...") - cursor.execute("ALTER TABLE student_enrollments ADD COLUMN allow_same_day_exception BOOLEAN DEFAULT 0") - if 'exception_reason' not in enr_columns: - print("Adding column 'exception_reason' to student_enrollments table...") - cursor.execute("ALTER TABLE student_enrollments ADD COLUMN exception_reason VARCHAR(255)") - - conn.commit() - conn.close() - print(f"Migration completed for {db_path}") - -if __name__ == '__main__': - migrate() diff --git a/backend/migrate_phase3_fields.py b/backend/migrate_phase3_fields.py deleted file mode 100644 index 9c503c3..0000000 --- a/backend/migrate_phase3_fields.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Migration script for Phase 3 MVP fields & tables: -- commissions.grades_closed (BOOLEAN DEFAULT 0) -- commissions.grades_closed_at (DATETIME) -- commissions.grades_closed_by (INTEGER) -- commissions.acta_number (VARCHAR(50)) -- milestone_grades table -""" -import sqlite3 -import os - -def migrate(): - db_paths = [ - os.path.join(os.path.dirname(__file__), 'instance', 'app.db'), - os.path.join(os.path.dirname(__file__), 'app.db') - ] - for db_path in db_paths: - if not os.path.exists(db_path): - continue - print(f"Migrating SQLite DB: {db_path}") - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - # Check commissions columns - cursor.execute("PRAGMA table_info(commissions)") - columns = [row[1] for row in cursor.fetchall()] - if 'grades_closed' not in columns: - print("Adding column 'grades_closed' to commissions table...") - cursor.execute("ALTER TABLE commissions ADD COLUMN grades_closed BOOLEAN DEFAULT 0") - if 'grades_closed_at' not in columns: - print("Adding column 'grades_closed_at' to commissions table...") - cursor.execute("ALTER TABLE commissions ADD COLUMN grades_closed_at DATETIME") - if 'grades_closed_by' not in columns: - print("Adding column 'grades_closed_by' to commissions table...") - cursor.execute("ALTER TABLE commissions ADD COLUMN grades_closed_by INTEGER") - if 'acta_number' not in columns: - print("Adding column 'acta_number' to commissions table...") - cursor.execute("ALTER TABLE commissions ADD COLUMN acta_number VARCHAR(50)") - - # Create milestone_grades table if not exists - cursor.execute(""" - CREATE TABLE IF NOT EXISTS milestone_grades ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - milestone_id INTEGER NOT NULL, - enrollment_id INTEGER NOT NULL, - student_id INTEGER NOT NULL, - score FLOAT, - is_absent BOOLEAN DEFAULT 0, - feedback TEXT, - graded_by_user_id INTEGER, - graded_at DATETIME, - updated_at DATETIME, - FOREIGN KEY (milestone_id) REFERENCES academic_milestones(id) ON DELETE CASCADE, - FOREIGN KEY (enrollment_id) REFERENCES student_enrollments(id) ON DELETE CASCADE, - FOREIGN KEY (student_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (graded_by_user_id) REFERENCES users(id) ON DELETE SET NULL, - CONSTRAINT uq_milestone_student UNIQUE (milestone_id, student_id) - ) - """) - cursor.execute("CREATE INDEX IF NOT EXISTS ix_milestone_grades_milestone_id ON milestone_grades (milestone_id)") - cursor.execute("CREATE INDEX IF NOT EXISTS ix_milestone_grades_enrollment_id ON milestone_grades (enrollment_id)") - cursor.execute("CREATE INDEX IF NOT EXISTS ix_milestone_grades_student_id ON milestone_grades (student_id)") - - conn.commit() - conn.close() - print(f"Migration completed for {db_path}") - -if __name__ == '__main__': - migrate() diff --git a/backend/migrate_phase6_settings.py b/backend/migrate_phase6_settings.py deleted file mode 100644 index 2ab8fd3..0000000 --- a/backend/migrate_phase6_settings.py +++ /dev/null @@ -1,24 +0,0 @@ -from app import create_app, db -from sqlalchemy import text - -app = create_app() - -with app.app_context(): - print("Verificando columnas en system_settings...") - try: - db.session.execute(text("ALTER TABLE system_settings ADD COLUMN category VARCHAR(50) DEFAULT 'system'")) - db.session.commit() - print("Columna 'category' agregada exitosamente.") - except Exception as e: - db.session.rollback() - print("Aviso al agregar 'category':", str(e).split('\n')[0]) - - try: - db.session.execute(text("ALTER TABLE system_settings ADD COLUMN is_encrypted BOOLEAN DEFAULT 0")) - db.session.commit() - print("Columna 'is_encrypted' agregada exitosamente.") - except Exception as e: - db.session.rollback() - print("Aviso al agregar 'is_encrypted':", str(e).split('\n')[0]) - - print("Migración de system_settings completa.") diff --git a/backend/migrate_phase6_sync_tasks.py b/backend/migrate_phase6_sync_tasks.py deleted file mode 100644 index 591b75f..0000000 --- a/backend/migrate_phase6_sync_tasks.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Migration script to create moodle_sync_tasks table if it doesn't exist. -Supports both SQLite and PostgreSQL. -""" -from app import create_app, db -from app.models.sync_task import MoodleSyncTask -from sqlalchemy import inspect - -def run_migration(): - app = create_app() - with app.app_context(): - engine = db.engine - print(f"Connecting to database using engine: {engine.name}") - - # db.create_all() creates any missing tables including moodle_sync_tasks - db.create_all() - print("db.create_all() executed successfully. Checking table existence...") - - inspector = inspect(engine) - tables = inspector.get_table_names() - if 'moodle_sync_tasks' in tables: - print("SUCCESS: 'moodle_sync_tasks' table exists and is ready.") - cols = [c['name'] for c in inspector.get_columns('moodle_sync_tasks')] - print(f"Columns in moodle_sync_tasks: {cols}") - else: - print("ERROR: 'moodle_sync_tasks' table was not created.") - -if __name__ == '__main__': - run_migration() diff --git a/backend/migrate_user_profile_fields.py b/backend/migrate_user_profile_fields.py deleted file mode 100644 index 4cb6120..0000000 --- a/backend/migrate_user_profile_fields.py +++ /dev/null @@ -1,79 +0,0 @@ -import sqlite3 -import os -import sys - -# Locate database -possible_paths = [ - os.path.join(os.path.dirname(__file__), 'instance', 'edu_space.db'), - os.path.join(os.path.dirname(__file__), 'instance', 'app.db'), - os.path.join(os.path.dirname(__file__), 'edu_space.db'), - os.path.join(os.path.dirname(__file__), 'app.db'), -] - -db_path = None -for p in possible_paths: - if os.path.exists(p): - db_path = p - break - -if not db_path: - # Check current directory - for root, dirs, files in os.walk(os.path.dirname(__file__)): - for f in files: - if f.endswith('.db'): - db_path = os.path.join(root, f) - break - if db_path: - break - -print(f"Connecting to database: {db_path}") -conn = sqlite3.connect(db_path) -cursor = conn.cursor() - -# Check existing columns in users table -cursor.execute("PRAGMA table_info(users)") -columns = [row[1] for row in cursor.fetchall()] -print(f"Existing columns in 'users': {columns}") - -new_columns = [ - ('first_name', 'VARCHAR(100)'), - ('last_name', 'VARCHAR(100)'), - ('phone', 'VARCHAR(50)'), - ('address', 'VARCHAR(255)'), - ('document_type', 'VARCHAR(20) DEFAULT "DNI"'), - ('document_number', 'VARCHAR(30)') -] - -for col_name, col_type in new_columns: - if col_name not in columns: - print(f"Adding column '{col_name}'...") - cursor.execute(f"ALTER TABLE users ADD COLUMN {col_name} {col_type}") - else: - print(f"Column '{col_name}' already exists.") - -conn.commit() - -# Create index on document_number if not exists -try: - cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_document_number ON users(document_number)") - conn.commit() - print("Index on document_number verified.") -except Exception as e: - print(f"Notice on index: {e}") - -# Migrate existing users: split name into first_name and last_name if empty -cursor.execute("SELECT id, name, first_name, last_name FROM users") -users = cursor.fetchall() -migrated = 0 -for u_id, name, fn, ln in users: - if not fn and name: - parts = name.strip().split() - first_n = parts[0] if parts else '' - last_n = ' '.join(parts[1:]) if len(parts) > 1 else '' - cursor.execute("UPDATE users SET first_name = ?, last_name = ? WHERE id = ?", (first_n, last_n, u_id)) - migrated += 1 - -conn.commit() -print(f"Successfully migrated {migrated} users with first_name and last_name from name.") -conn.close() -print("Migration completed successfully!") diff --git a/backend/test_batch_coupling.py b/backend/test_batch_coupling.py deleted file mode 100644 index 4ec9bc0..0000000 --- a/backend/test_batch_coupling.py +++ /dev/null @@ -1,48 +0,0 @@ -import requests -from app import create_app -from app.models.user import User -from app.services.jwt_service import JWTService - -BASE_URL = "http://localhost:3000" - -app = create_app() -with app.app_context(): - admin = User.query.filter_by(email='admin@edu-space.com').first() - token = JWTService.create_access_token(admin) - -cookies = {'auth_token': token} -session = requests.Session() -session.cookies.update(cookies) - -routes_to_test = [ - ("admin.google_sheets_import", "/admin/google_sheets_import", ["Google Sheets", "Official Schedule Board"]), - ("classrooms.add_classroom", "/classrooms/add_classroom", ["Add Classroom", "Classroom Information"]), - ("admin.occupancy_metrics", "/admin/occupancy_metrics", ["Ocupación", "Métricas"]), - ("buildings.list_buildings", "/buildings/list_buildings", ["Edificios", "Edificio Central"]), - ("schedule.add_reservation", "/schedule/add_reservation", ["New Reservation", "Detalles de la Reserva"]) -] - -all_passed = True -print("\n========================================================") -print("Testing coupling of 5 newly added BFF functionalities:") -print("========================================================") - -for name, path, expected_texts in routes_to_test: - r = session.get(f"{BASE_URL}{path}", allow_redirects=True) - is_200 = r.status_code == 200 - texts_found = [t for t in expected_texts if t.lower() in r.text.lower()] - all_texts_ok = len(texts_found) == len(expected_texts) - has_fallback = "En Construcción" in r.text - - status = "PASSED" if is_200 and all_texts_ok and not has_fallback else "FAILED" - print(f"[{status}] {name} ({path}) -> Status: {r.status_code}, Texts match: {texts_found}/{expected_texts}, Has Fallback: {has_fallback}") - if status == "FAILED": - all_passed = False - print(f"Response snippet:\n{r.text[:500]}\n---") - -print("========================================================") -if all_passed: - print("SUCCESS: ALL 5 FUNCTIONALITIES ARE COUPLED TO THE NODE.JS BFF!") -else: - print("FAILURE: SOME FUNCTIONALITIES FAILED!") -print("========================================================\n") diff --git a/backend/test_integration.py b/backend/test_integration.py deleted file mode 100644 index c3f36b7..0000000 --- a/backend/test_integration.py +++ /dev/null @@ -1,77 +0,0 @@ -import requests -from app import create_app -from app.models.user import User -from app.services.jwt_service import JWTService - -app = create_app() -with app.app_context(): - admin = User.query.filter_by(email='admin@edu-space.com').first() - token = JWTService.create_access_token(admin) - -cookies = {'auth_token': token} -session = requests.Session() -session.cookies.update(cookies) - -tests = [ - # Dashboard y redirecciones - ('GET /main/dashboard (redirect to /dashboard)', 'http://localhost:3000/main/dashboard', 200, ['Edu-Space Admin', 'Aulas Totales']), - ('GET /dashboard', 'http://localhost:3000/dashboard', 200, ['Edu-Space Admin', 'Aulas Totales', 'Reservas de Hoy']), - - # Classrooms - ('GET /classrooms/list_classrooms', 'http://localhost:3000/classrooms/list_classrooms', 200, ['Aulas y Espacios', 'PB-01', 'Edificio Central']), - ('GET /classrooms/list', 'http://localhost:3000/classrooms/list', 200, ['Aulas y Espacios']), - ('GET /classrooms/add_classroom (Coupled)', 'http://localhost:3000/classrooms/add_classroom', 200, ['Add Classroom', 'Classroom Information']), - ('GET /classrooms/add', 'http://localhost:3000/classrooms/add', 200, ['Add Classroom']), - - # Schedule - ('GET /schedule/calendar_view', 'http://localhost:3000/schedule/calendar_view', 200, ['Reservations Calendar', 'calendar']), - ('GET /schedule/calendar_data (JSON Feed)', 'http://localhost:3000/schedule/calendar_data', 200, None), - ('GET /schedule/today', 'http://localhost:3000/schedule/today', 200, ['Reservas']), - ('GET /schedule/list_reservations', 'http://localhost:3000/schedule/list_reservations', 200, ['Listado de Reservas']), - ('GET /schedule/add_reservation (Coupled)', 'http://localhost:3000/schedule/add_reservation', 200, ['New Reservation', 'Detalles de la Reserva']), - ('GET /schedule/add', 'http://localhost:3000/schedule/add', 200, ['New Reservation']), - - # Buildings - ('GET /buildings/list_buildings (Coupled)', 'http://localhost:3000/buildings/list_buildings', 200, ['Gestión de Edificios', 'Edificio Central']), - ('GET /buildings', 'http://localhost:3000/buildings', 200, ['Gestión de Edificios']), - ('GET /buildings/add_building', 'http://localhost:3000/buildings/add_building', 200, ['Nuevo Edificio']), - - # Admin Features - ('GET /admin/google_sheets_import (Coupled)', 'http://localhost:3000/admin/google_sheets_import', 200, ['Google Sheets', 'Official Schedule Board']), - ('GET /admin/import', 'http://localhost:3000/admin/import', 200, ['Google Sheets']), - ('GET /admin/occupancy_metrics (Coupled)', 'http://localhost:3000/admin/occupancy_metrics', 200, ['Ocupación', 'Métricas']), - ('GET /admin/occupancy-metrics', 'http://localhost:3000/admin/occupancy-metrics', 200, ['Ocupación']) -] - -all_passed = True -print('=== EJECUTANDO SUITE DE PRUEBAS DE INTEGRACION BFF NODE.JS ===') -for name, url, expected_status, expected_texts in tests: - r = session.get(url, allow_redirects=True) - status_ok = r.status_code == expected_status - text_ok = True - if 'En Construcción' in r.text: - text_ok = False - print(f' [FAIL] Fallback "En Construcción" encountered in {url}') - if expected_texts: - for t in expected_texts: - if t.lower() not in r.text.lower(): - text_ok = False - print(f' [FAIL] Expected text "{t}" not found in {url}') - if expected_texts is None: # JSON check - data = r.json() - text_ok = isinstance(data, list) and len(data) > 0 - if not text_ok: - print(f' [FAIL] Expected non-empty JSON list in {url}') - - if status_ok and text_ok: - print(f'[PASS] {name} -> Status: {r.status_code} OK') - else: - all_passed = False - print(f'[FAIL] {name} -> Status: {r.status_code} FAIL') - -print('===============================================================') -if all_passed: - print('TODAS LAS PRUEBAS DE INTEGRACION PASARON SATISFACTORIAMENTE (100% OK)') -else: - print('SE ENCONTRARON FALLOS EN LA SUITE DE PRUEBAS') -print('===============================================================') diff --git a/backend/tests/test_enrollment_and_commissions.py b/backend/tests/test_enrollment_and_commissions.py deleted file mode 100644 index 297894f..0000000 --- a/backend/tests/test_enrollment_and_commissions.py +++ /dev/null @@ -1,539 +0,0 @@ -""" -test_enrollment_and_commissions.py -Tests for the enrollment management system: -- StudentEnrollment model CRUD -- Commission management routes (Bedelia/Admin) -- /mis-materias routes per role (Docente, Alumno, Admin, Bedelia) -- RBAC enrollment permissions -- Bedelia role deduplication (no accented duplicate) -""" -import unittest -from app import create_app, db -from app.models.user import User -from app.models.role import Role, SYSTEM_MODULES -from app.models.subject import Subject, Commission -from app.models.enrollment import StudentEnrollment - - -def _login_as(client, user_id): - """Helper: inject Flask-Login session for the given user id.""" - with client.session_transaction() as sess: - sess['_user_id'] = str(user_id) - sess['_fresh'] = True - - -class TestStudentEnrollmentModel(unittest.TestCase): - """Unit tests for the StudentEnrollment SQLAlchemy model.""" - - def setUp(self): - self.app = create_app() - self.app.config['TESTING'] = True - self.app.config['WTF_CSRF_ENABLED'] = False - self.ctx = self.app.app_context() - self.ctx.push() - - def tearDown(self): - self.ctx.pop() - - def test_table_exists(self): - """student_enrollments table must exist in the database.""" - result = db.session.execute( - db.text("SELECT to_regclass('public.student_enrollments')") - ).scalar() - self.assertIsNotNone(result, "Tabla student_enrollments no existe en la BD") - - def test_model_columns(self): - """StudentEnrollment must expose all required columns.""" - cols = {c.name for c in StudentEnrollment.__table__.columns} - for expected in ('id', 'student_id', 'commission_id', 'status', 'enrolled_at', 'notes'): - self.assertIn(expected, cols, f"Columna faltante: {expected}") - - def test_unique_constraint_name(self): - """Unique constraint uq_student_commission must exist.""" - constraint_names = {c.name for c in StudentEnrollment.__table__.constraints} - self.assertIn('uq_student_commission', constraint_names) - - def test_valid_statuses(self): - """to_dict must return a status in the accepted set.""" - alumno = User.query.filter( - User.role.ilike('%ALUMNO%'), User.is_active == True - ).first() - comm = Commission.query.filter_by(active=True).first() - - if not alumno or not comm: - self.skipTest("Se requiere al menos un alumno y una comision activa en la BD") - - # Check if already enrolled to avoid integrity error - existing = StudentEnrollment.query.filter_by( - student_id=alumno.id, commission_id=comm.id - ).first() - if not existing: - enr = StudentEnrollment( - student_id=alumno.id, - commission_id=comm.id, - status='activo', - notes='Test enrollment' - ) - db.session.add(enr) - db.session.commit() - enrolled = enr - else: - enrolled = existing - - d = enrolled.to_dict() - self.assertIn(d['status'], ['activo', 'retirado', 'condicional']) - self.assertEqual(d['student_id'], alumno.id) - self.assertEqual(d['commission_id'], comm.id) - - # Cleanup (only if we created it) - if not existing: - db.session.delete(enrolled) - # Restore current_students counter - comm.current_students = max(0, (comm.current_students or 1) - 1) - db.session.commit() - - def test_relationships_accessible(self): - """student and commission relationships must resolve without error.""" - enr = StudentEnrollment.query.first() - if enr: - _ = enr.student - _ = enr.commission - - -class TestRBACEnrollmentModule(unittest.TestCase): - """Tests for the enrollment RBAC module and role permissions.""" - - def setUp(self): - self.app = create_app() - self.app.config['TESTING'] = True - self.ctx = self.app.app_context() - self.ctx.push() - - def tearDown(self): - self.ctx.pop() - - def test_enrollment_module_in_system_modules(self): - """SYSTEM_MODULES must contain an 'enrollment' module.""" - module_ids = [m['id'] for m in SYSTEM_MODULES] - self.assertIn('enrollment', module_ids, - "Modulo 'enrollment' no encontrado en SYSTEM_MODULES") - - def test_bedelia_has_enrollment_read_write(self): - """Bedelia role must have read_write on enrollment.""" - bedelia = Role.query.filter_by(name='Bedelia').first() - self.assertIsNotNone(bedelia, "Rol 'Bedelia' no existe en la BD") - self.assertEqual(bedelia.get_permission('enrollment'), 'read_write') - self.assertTrue(bedelia.has_permission('enrollment', 'read_write')) - - def test_admin_has_enrollment_read_write(self): - """Admin role must have read_write on enrollment.""" - admin = Role.query.filter_by(name='Admin').first() - self.assertIsNotNone(admin) - self.assertEqual(admin.get_permission('enrollment'), 'read_write') - - def test_docente_has_enrollment_read(self): - """Docente role must have read on enrollment (not write).""" - docente = Role.query.filter_by(name='Docente').first() - self.assertIsNotNone(docente) - self.assertTrue(docente.has_permission('enrollment', 'read')) - self.assertFalse(docente.has_permission('enrollment', 'read_write')) - - def test_alumno_has_enrollment_read(self): - """Alumno role must have read on enrollment.""" - alumno_role = Role.query.filter_by(name='Alumno').first() - self.assertIsNotNone(alumno_role) - self.assertTrue(alumno_role.has_permission('enrollment', 'read')) - - def test_no_duplicate_bedelia_roles(self): - """There must be exactly one Bedelia role (no accented duplicate).""" - bedelia_variants = Role.query.filter( - Role.name.in_(['Bedelia', 'Bedelía', 'BEDELÍA']) - ).all() - names = [r.name for r in bedelia_variants] - self.assertNotIn('Bedelía', names, - "Rol duplicado 'Bedelía' (con acento) todavía existe en la BD") - self.assertEqual(len(bedelia_variants), 1, - f"Se encontraron {len(bedelia_variants)} variantes de Bedelia: {names}") - - -class TestCommissionsManagementRoutes(unittest.TestCase): - """Integration tests for /admin/commissions/ routes (Bedelia/Admin).""" - - def setUp(self): - self.app = create_app() - self.app.config['TESTING'] = True - self.app.config['WTF_CSRF_ENABLED'] = False - self.client = self.app.test_client() - self.ctx = self.app.app_context() - self.ctx.push() - - # Login as admin (first active user) - self.admin = User.query.filter_by(email='admin@edu-space.com').first() - self.bedelia = User.query.filter_by(email='bedelia@edu-space.com').first() - self.docente = User.query.filter_by(email='docente@edu-space.com').first() - self.alumno_user = User.query.filter_by(email='alumno@edu-space.com').first() - - def tearDown(self): - self.ctx.pop() - - def _login(self, user): - _login_as(self.client, user.id) - - def test_commissions_list_accessible_by_admin(self): - """Admin can access /admin/commissions.""" - self._login(self.admin) - res = self.client.get('/admin/commissions') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn('Gestión de Comisiones', html) - - def test_commissions_list_accessible_by_bedelia(self): - """Bedelia can access /admin/commissions.""" - if not self.bedelia: - self.skipTest("No bedelia user in DB") - self._login(self.bedelia) - res = self.client.get('/admin/commissions') - self.assertEqual(res.status_code, 200) - - def test_commissions_list_forbidden_for_docente(self): - """Docente without manage permissions should get 403 on /admin/commissions.""" - if not self.docente: - self.skipTest("No docente user in DB") - self._login(self.docente) - res = self.client.get('/admin/commissions') - # Docente has 'academic: read' → can_manage() is False → 403 - self.assertIn(res.status_code, [403, 302]) - - def test_commission_detail_accessible_by_admin(self): - """Commission detail page returns 200 for an existing commission.""" - self._login(self.admin) - comm = Commission.query.filter_by(active=True).first() - if not comm: - self.skipTest("No active commission in DB") - res = self.client.get(f'/admin/commissions/{comm.id}') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn('Alumnos Inscriptos', html) - self.assertIn('Docente Asignado', html) - - def test_commission_detail_accessible_by_bedelia(self): - """Bedelia can view commission detail.""" - if not self.bedelia: - self.skipTest("No bedelia user in DB") - self._login(self.bedelia) - comm = Commission.query.filter_by(active=True).first() - if not comm: - self.skipTest("No active commission in DB") - res = self.client.get(f'/admin/commissions/{comm.id}') - self.assertEqual(res.status_code, 200) - - def test_commission_detail_nonexistent_returns_404(self): - """Non-existent commission ID must return 404.""" - self._login(self.admin) - res = self.client.get('/admin/commissions/999999') - self.assertEqual(res.status_code, 404) - - def test_assign_teacher_post(self): - """Bedelia can POST to assign a teacher to a commission.""" - if not self.bedelia or not self.docente: - self.skipTest("Need bedelia and docente users") - self._login(self.bedelia) - comm = Commission.query.filter_by(active=True).first() - if not comm: - self.skipTest("No active commission in DB") - - res = self.client.post( - f'/admin/commissions/{comm.id}/assign-teacher', - data={'teacher_id': self.docente.id}, - follow_redirects=True - ) - self.assertEqual(res.status_code, 200) - # Verify assignment persisted - db.session.refresh(comm) - self.assertEqual(comm.teacher_id, self.docente.id) - - def test_enroll_student_and_unenroll(self): - """Bedelia can enroll and unenroll an alumno in a commission.""" - if not self.bedelia or not self.alumno_user: - self.skipTest("Need bedelia and alumno users") - self._login(self.bedelia) - comm = Commission.query.filter_by(active=True).first() - if not comm: - self.skipTest("No active commission in DB") - - # Clean up any prior enrollment to ensure idempotent test - prior = StudentEnrollment.query.filter_by( - student_id=self.alumno_user.id, commission_id=comm.id - ).first() - if prior: - db.session.delete(prior) - db.session.commit() - - prev_count = comm.current_students or 0 - - # Enroll - res = self.client.post( - f'/admin/commissions/{comm.id}/enroll-student', - data={'student_id': self.alumno_user.id, 'notes': 'Test'}, - follow_redirects=True - ) - self.assertEqual(res.status_code, 200) - enr = StudentEnrollment.query.filter_by( - student_id=self.alumno_user.id, commission_id=comm.id - ).first() - self.assertIsNotNone(enr, "Inscripción no encontrada tras POST enroll") - self.assertEqual(enr.status, 'activo') - db.session.refresh(comm) - self.assertEqual(comm.current_students, prev_count + 1) - - # Unenroll - res2 = self.client.post( - f'/admin/commissions/{comm.id}/unenroll-student/{self.alumno_user.id}', - follow_redirects=True - ) - self.assertEqual(res2.status_code, 200) - enr_after = StudentEnrollment.query.filter_by( - student_id=self.alumno_user.id, commission_id=comm.id - ).first() - self.assertIsNone(enr_after, "Inscripción debería haber sido eliminada") - db.session.refresh(comm) - self.assertEqual(comm.current_students, prev_count) - - def test_update_enrollment_status(self): - """Bedelia can change enrollment status (activo→condicional→retirado).""" - if not self.bedelia or not self.alumno_user: - self.skipTest("Need bedelia and alumno users") - self._login(self.bedelia) - comm = Commission.query.filter_by(active=True).first() - if not comm: - self.skipTest("No active commission in DB") - - # Ensure enrollment exists - enr = StudentEnrollment.query.filter_by( - student_id=self.alumno_user.id, commission_id=comm.id - ).first() - if not enr: - enr = StudentEnrollment( - student_id=self.alumno_user.id, - commission_id=comm.id, - status='activo' - ) - db.session.add(enr) - db.session.commit() - - for new_status in ['condicional', 'retirado', 'activo']: - res = self.client.post( - f'/admin/commissions/{comm.id}/update-enrollment/{self.alumno_user.id}', - data={'status': new_status}, - follow_redirects=True - ) - self.assertEqual(res.status_code, 200) - db.session.refresh(enr) - self.assertEqual(enr.status, new_status) - - # Cleanup - db.session.delete(enr) - db.session.commit() - - def test_duplicate_enrollment_rejected(self): - """Enrolling the same student twice in the same commission is rejected.""" - if not self.bedelia or not self.alumno_user: - self.skipTest("Need bedelia and alumno users") - self._login(self.bedelia) - comm = Commission.query.filter_by(active=True).first() - if not comm: - self.skipTest("No active commission in DB") - - # Ensure a clean start - prior = StudentEnrollment.query.filter_by( - student_id=self.alumno_user.id, commission_id=comm.id - ).first() - if not prior: - enr = StudentEnrollment( - student_id=self.alumno_user.id, - commission_id=comm.id, - status='activo' - ) - db.session.add(enr) - db.session.commit() - - # Second enroll attempt - res = self.client.post( - f'/admin/commissions/{comm.id}/enroll-student', - data={'student_id': self.alumno_user.id}, - follow_redirects=True - ) - self.assertEqual(res.status_code, 200) - # Should see a warning message, and only one enrollment should exist - count = StudentEnrollment.query.filter_by( - student_id=self.alumno_user.id, commission_id=comm.id - ).count() - self.assertEqual(count, 1) - - # Cleanup - StudentEnrollment.query.filter_by( - student_id=self.alumno_user.id, commission_id=comm.id - ).delete() - db.session.commit() - - -class TestMySubjectsRoutes(unittest.TestCase): - """/mis-materias route tests per role.""" - - def setUp(self): - self.app = create_app() - self.app.config['TESTING'] = True - self.app.config['WTF_CSRF_ENABLED'] = False - self.client = self.app.test_client() - self.ctx = self.app.app_context() - self.ctx.push() - - self.admin = User.query.filter_by(email='admin@edu-space.com').first() - self.bedelia = User.query.filter_by(email='bedelia@edu-space.com').first() - self.docente = User.query.filter_by(email='docente@edu-space.com').first() - self.alumno_user = User.query.filter_by(email='alumno@edu-space.com').first() - - def tearDown(self): - self.ctx.pop() - - def _login(self, user): - _login_as(self.client, user.id) - - def test_mis_materias_requires_login(self): - """Unauthenticated request to /mis-materias/ must redirect to login.""" - res = self.client.get('/mis-materias/') - self.assertIn(res.status_code, [302, 401]) - - def test_mis_materias_admin_ok(self): - """Admin sees all commissions at /mis-materias/.""" - self._login(self.admin) - res = self.client.get('/mis-materias/') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn('Todas las Comisiones', html) - - def test_mis_materias_bedelia_ok(self): - """Bedelia sees all commissions at /mis-materias/.""" - if not self.bedelia: - self.skipTest("No bedelia user") - self._login(self.bedelia) - res = self.client.get('/mis-materias/') - self.assertEqual(res.status_code, 200) - - def test_mis_materias_docente_ok(self): - """Docente gets 200 at /mis-materias/ (even with no assigned commissions).""" - if not self.docente: - self.skipTest("No docente user") - self._login(self.docente) - res = self.client.get('/mis-materias/') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn('Mis Materias', html) - - def test_mis_materias_alumno_ok(self): - """Alumno gets 200 at /mis-materias/ (even with no enrollments).""" - if not self.alumno_user: - self.skipTest("No alumno user") - self._login(self.alumno_user) - res = self.client.get('/mis-materias/') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn('Mis Materias', html) - - def test_mis_materias_detail_enrolled_alumno(self): - """Alumno enrolled in a commission can see its detail page.""" - if not self.alumno_user: - self.skipTest("No alumno user") - - comm = Commission.query.filter_by(active=True).first() - if not comm: - self.skipTest("No active commission in DB") - - # Create temporary enrollment - enr = StudentEnrollment.query.filter_by( - student_id=self.alumno_user.id, commission_id=comm.id - ).first() - cleanup_needed = False - if not enr: - enr = StudentEnrollment( - student_id=self.alumno_user.id, - commission_id=comm.id, - status='activo' - ) - db.session.add(enr) - db.session.commit() - cleanup_needed = True - - self._login(self.alumno_user) - res = self.client.get(f'/mis-materias/{comm.id}') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn('Tu inscripción', html) - - if cleanup_needed: - db.session.delete(enr) - db.session.commit() - - def test_mis_materias_detail_forbidden_for_unenrolled_alumno(self): - """Alumno NOT enrolled in a commission gets 403 on its detail page.""" - if not self.alumno_user: - self.skipTest("No alumno user") - - comm = Commission.query.filter_by(active=True).first() - if not comm: - self.skipTest("No active commission in DB") - - # Ensure alumno is NOT enrolled - StudentEnrollment.query.filter_by( - student_id=self.alumno_user.id, commission_id=comm.id - ).delete() - db.session.commit() - - self._login(self.alumno_user) - res = self.client.get(f'/mis-materias/{comm.id}') - self.assertEqual(res.status_code, 403) - - def test_mis_materias_detail_docente_own_commission(self): - """Docente can see detail of their own assigned commission.""" - if not self.docente: - self.skipTest("No docente user") - - comm = Commission.query.filter_by(active=True).first() - if not comm: - self.skipTest("No active commission in DB") - - # Assign commission to docente - prev_teacher = comm.teacher_id - comm.teacher_id = self.docente.id - db.session.commit() - - self._login(self.docente) - res = self.client.get(f'/mis-materias/{comm.id}') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn(comm.subject.name, html) - - # Restore - comm.teacher_id = prev_teacher - db.session.commit() - - def test_mis_materias_detail_docente_unauthorized_commission(self): - """Docente cannot see a commission they are not assigned to.""" - if not self.docente: - self.skipTest("No docente user") - - # Find a commission where docente is NOT the teacher - comm = Commission.query.filter( - Commission.active == True, - Commission.teacher_id != self.docente.id - ).first() - if not comm: - self.skipTest("No commission without this docente in DB") - - self._login(self.docente) - res = self.client.get(f'/mis-materias/{comm.id}') - self.assertEqual(res.status_code, 403) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/backend/tests/test_role_dashboard_and_menu.py b/backend/tests/test_role_dashboard_and_menu.py deleted file mode 100644 index fd8166e..0000000 --- a/backend/tests/test_role_dashboard_and_menu.py +++ /dev/null @@ -1,103 +0,0 @@ -import unittest -from app import create_app, db -from app.models.user import User - -class TestRoleDashboardAndMenu(unittest.TestCase): - def setUp(self): - self.app = create_app() - self.app.config['TESTING'] = True - self.app.config['WTF_CSRF_ENABLED'] = False - self.client = self.app.test_client() - self.app_context = self.app.app_context() - self.app_context.push() - - # Usar usuario administrador para autenticación en sesión de pruebas - self.user = User.query.filter_by(is_active=True).first() - if self.user: - with self.client.session_transaction() as sess: - sess['_user_id'] = str(self.user.id) - sess['_fresh'] = True - - def tearDown(self): - self.app_context.pop() - - def test_user_role_methods(self): - """Verifica la normalización y detección de roles en el modelo User""" - u = User(name="Test Teacher", email="prof@test.edu", role="Docente") - self.assertEqual(u.get_institutional_role(), "docente") - self.assertTrue(u.is_docente()) - self.assertFalse(u.is_admin()) - self.assertFalse(u.is_alumno()) - self.assertFalse(u.is_bedelia()) - - u_bedelia = User(name="Test Bedelia", email="bedelia@test.edu", role="Bedelia") - self.assertEqual(u_bedelia.get_institutional_role(), "bedelia") - self.assertTrue(u_bedelia.is_bedelia()) - - u_student = User(name="Test Student", email="student@test.edu", role="Alumno") - self.assertEqual(u_student.get_institutional_role(), "alumno") - self.assertTrue(u_student.is_alumno()) - - u_admin = User(name="Test Admin", email="admin@test.edu", role="ADMIN") - self.assertEqual(u_admin.get_institutional_role(), "admin") - self.assertTrue(u_admin.is_admin()) - - badge = u_admin.role_badge_display - self.assertIn("badge-admin", badge["class"]) - self.assertIn("Administrador", badge["label"]) - - def test_admin_dashboard_rendered(self): - """Verifica que el dashboard administrativo renderiza KPIs y accesos de administración""" - res = self.client.get('/?view_as=admin') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn('Aulas Totales', html) - self.assertIn('Oferta de Materias', html) - self.assertIn('Usuarios Activos', html) - self.assertIn('Módulos de Gestión & Visualización de Menús', html) - self.assertIn('Espacios & Sedes', html) - self.assertIn('Auditoría & RBAC', html) - - def test_bedelia_dashboard_rendered(self): - """Verifica que el dashboard de bedelía renderiza métricas operativas de campus""" - res = self.client.get('/?view_as=bedelia') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn('Aulas Presenciales', html) - self.assertIn('Salas Virtuales', html) - self.assertIn('Acciones Rápidas & Menús Operativos de Bedelía', html) - self.assertIn('Cartelera del Día', html) - self.assertIn('Sincronizar Sheets', html) - - def test_docente_dashboard_rendered(self): - """Verifica que el dashboard del docente renderiza sus clases y accesos directos""" - res = self.client.get('/?view_as=docente') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn('Mis Clases Hoy', html) - self.assertIn('Mis Reservas Activas', html) - self.assertIn('Reservar Aula', html) - self.assertIn('Accesos Directos de Docencia & Visualización de Menús', html) - self.assertIn('Mis Clases Programadas para Hoy', html) - - def test_alumno_dashboard_rendered(self): - """Verifica que el dashboard del alumno muestra la brújula diaria y herramientas de cursada""" - res = self.client.get('/?view_as=alumno') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn('¿Dónde curso hoy? - Cartelera del Alumno', html) - self.assertIn('Menú del Alumno & Herramientas de Cursada', html) - self.assertIn('Cartelera de Hoy', html) - self.assertIn('Cronograma Semanal', html) - self.assertIn('Aulas Virtuales', html) - - def test_audit_logs_view(self): - """Verifica que la vista de auditoría institucional responde correctamente""" - res = self.client.get('/admin/audit-logs') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - self.assertIn('Registro de Auditoría Institucional', html) - self.assertIn('Trazabilidad completa de operaciones', html) - -if __name__ == '__main__': - unittest.main() diff --git a/backend/tests/test_security_chain.py b/backend/tests/test_security_chain.py deleted file mode 100644 index e434526..0000000 --- a/backend/tests/test_security_chain.py +++ /dev/null @@ -1,42 +0,0 @@ -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() diff --git a/backend/tests/test_services_and_dto.py b/backend/tests/test_services_and_dto.py deleted file mode 100644 index 2596fe3..0000000 --- a/backend/tests/test_services_and_dto.py +++ /dev/null @@ -1,60 +0,0 @@ -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() diff --git a/backend/tests/test_sprint4_optimizer_and_concurrency.py b/backend/tests/test_sprint4_optimizer_and_concurrency.py index 3fb3e47..c1ca335 100644 --- a/backend/tests/test_sprint4_optimizer_and_concurrency.py +++ b/backend/tests/test_sprint4_optimizer_and_concurrency.py @@ -13,18 +13,23 @@ from app.models.genetic_algorithm import ( Gene ) from app.services.optimizer_service import OptimizerService, OptimizerJobStatus -from app.services.jwt_service import JWTService +from config.config import Config + + +class TestConfig(Config): + SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' + TESTING = True + WTF_CSRF_ENABLED = False class Sprint4OptimizerAndConcurrencyTestCase(unittest.TestCase): """Pruebas exhaustivas para el Sprint 4: Optimizador Heurístico, Concurrencia y Spring Boot.""" def setUp(self): - self.app = create_app() - self.app.config['TESTING'] = True - self.app.config['WTF_CSRF_ENABLED'] = False + self.app = create_app(TestConfig) self.app_context = self.app.app_context() self.app_context.push() + db.create_all() self.client = self.app.test_client() # Obtener usuario existente o crear @@ -38,6 +43,7 @@ class Sprint4OptimizerAndConcurrencyTestCase(unittest.TestCase): self.user = User( name='Sprint 4 Admin', email='admin_sprint4@edu-space.com', + role='ADMIN', role_id=admin_role.id, is_active=True ) @@ -50,9 +56,9 @@ class Sprint4OptimizerAndConcurrencyTestCase(unittest.TestCase): if not self.classrooms: for i in range(1, 11): room = Classroom( - room_number=f"AULA-{100 + i}", + code=f"AULA-{100 + i}", + building="Edificio Central", capacity=30 + (i * 5), - is_virtual=(i == 10), is_active=True ) db.session.add(room) @@ -68,7 +74,8 @@ class Sprint4OptimizerAndConcurrencyTestCase(unittest.TestCase): } def tearDown(self): - db.session.rollback() + db.session.remove() + db.drop_all() self.app_context.pop() def test_01_fast_fitness_and_conflict_detection(self): diff --git a/backend/tests/test_view_switcher.py b/backend/tests/test_view_switcher.py deleted file mode 100644 index ebaae5e..0000000 --- a/backend/tests/test_view_switcher.py +++ /dev/null @@ -1,45 +0,0 @@ -import unittest -from app import create_app, db -from app.models.user import User - -class TestViewSwitcher(unittest.TestCase): - def setUp(self): - self.app = create_app() - self.app.config['TESTING'] = True - self.app.config['WTF_CSRF_ENABLED'] = False - self.client = self.app.test_client() - self.app_context = self.app.app_context() - self.app_context.push() - - # Login con usuario admin para acceder a las vistas protegidas - self.user = User.query.filter_by(is_active=True).first() - if self.user: - with self.client.session_transaction() as sess: - sess['_user_id'] = str(self.user.id) - sess['_fresh'] = True - - def tearDown(self): - self.app_context.pop() - - def test_classrooms_view_switcher_rendered(self): - res = self.client.get('/classrooms/?view_mode=physical') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - # Verificar que el selector de vista está presente - self.assertIn('view-switcher-group', html) - self.assertIn('data-context="classrooms"', html) - self.assertIn('data-view-container="cards"', html) - self.assertIn('data-view-container="table"', html) - - def test_reservations_view_switcher_rendered(self): - res = self.client.get('/schedule/list') - self.assertEqual(res.status_code, 200) - html = res.get_data(as_text=True) - # Verificar que el selector de vista está presente - self.assertIn('view-switcher-group', html) - self.assertIn('data-context="reservations"', html) - self.assertIn('data-view-container="table"', html) - self.assertIn('data-view-container="cards"', html) - -if __name__ == '__main__': - unittest.main()