chore: depurar scripts obsoletos de migracion y pruebas legacy no requeridas
This commit is contained in:
@@ -99,7 +99,7 @@ class Individual:
|
|||||||
score += conflict_score * 0.2
|
score += conflict_score * 0.2
|
||||||
|
|
||||||
# Factor 4: Resource matching (10% weight)
|
# 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
|
score += resource_score * 0.1
|
||||||
|
|
||||||
self.fitness = score
|
self.fitness = score
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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.")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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!")
|
|
||||||
@@ -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")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
Gene
|
||||||
)
|
)
|
||||||
from app.services.optimizer_service import OptimizerService, OptimizerJobStatus
|
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):
|
class Sprint4OptimizerAndConcurrencyTestCase(unittest.TestCase):
|
||||||
"""Pruebas exhaustivas para el Sprint 4: Optimizador Heurístico, Concurrencia y Spring Boot."""
|
"""Pruebas exhaustivas para el Sprint 4: Optimizador Heurístico, Concurrencia y Spring Boot."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.app = create_app()
|
self.app = create_app(TestConfig)
|
||||||
self.app.config['TESTING'] = True
|
|
||||||
self.app.config['WTF_CSRF_ENABLED'] = False
|
|
||||||
self.app_context = self.app.app_context()
|
self.app_context = self.app.app_context()
|
||||||
self.app_context.push()
|
self.app_context.push()
|
||||||
|
db.create_all()
|
||||||
self.client = self.app.test_client()
|
self.client = self.app.test_client()
|
||||||
|
|
||||||
# Obtener usuario existente o crear
|
# Obtener usuario existente o crear
|
||||||
@@ -38,6 +43,7 @@ class Sprint4OptimizerAndConcurrencyTestCase(unittest.TestCase):
|
|||||||
self.user = User(
|
self.user = User(
|
||||||
name='Sprint 4 Admin',
|
name='Sprint 4 Admin',
|
||||||
email='admin_sprint4@edu-space.com',
|
email='admin_sprint4@edu-space.com',
|
||||||
|
role='ADMIN',
|
||||||
role_id=admin_role.id,
|
role_id=admin_role.id,
|
||||||
is_active=True
|
is_active=True
|
||||||
)
|
)
|
||||||
@@ -50,9 +56,9 @@ class Sprint4OptimizerAndConcurrencyTestCase(unittest.TestCase):
|
|||||||
if not self.classrooms:
|
if not self.classrooms:
|
||||||
for i in range(1, 11):
|
for i in range(1, 11):
|
||||||
room = Classroom(
|
room = Classroom(
|
||||||
room_number=f"AULA-{100 + i}",
|
code=f"AULA-{100 + i}",
|
||||||
|
building="Edificio Central",
|
||||||
capacity=30 + (i * 5),
|
capacity=30 + (i * 5),
|
||||||
is_virtual=(i == 10),
|
|
||||||
is_active=True
|
is_active=True
|
||||||
)
|
)
|
||||||
db.session.add(room)
|
db.session.add(room)
|
||||||
@@ -68,7 +74,8 @@ class Sprint4OptimizerAndConcurrencyTestCase(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
db.session.rollback()
|
db.session.remove()
|
||||||
|
db.drop_all()
|
||||||
self.app_context.pop()
|
self.app_context.pop()
|
||||||
|
|
||||||
def test_01_fast_fitness_and_conflict_detection(self):
|
def test_01_fast_fitness_and_conflict_detection(self):
|
||||||
|
|||||||
@@ -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()
|
|
||||||
Reference in New Issue
Block a user