chore: depurar scripts obsoletos de migracion y pruebas legacy no requeridas

This commit is contained in:
2026-09-23 14:05:48 -03:00
parent ed97fb5995
commit 902e30a261
15 changed files with 16 additions and 1168 deletions
+1 -1
View File
@@ -223,4 +223,4 @@ legacy_admin-edu-space/
.vscode/
.schemathesis/
AlumnosLS
AlumnosLS
+1 -1
View File
@@ -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
-44
View File
@@ -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()
-69
View File
@@ -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()
-24
View File
@@ -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.")
-29
View File
@@ -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()
-79
View File
@@ -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!")
-48
View File
@@ -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")
-77
View File
@@ -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('===============================================================')
@@ -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)
@@ -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()
-42
View File
@@ -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()
-60
View File
@@ -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()
@@ -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):
-45
View File
@@ -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()