from pathlib import Path def test_login_page_does_not_disclose_demo_passwords(client): response = client.get('/auth/login') assert response.status_code == 200 assert b'admin' not in response.data assert b'adminadmin' not in response.data def test_login_rejects_external_next_url(client, admin_user): response = client.post( '/auth/login?next=//example.net', data={'username': admin_user['username'], 'password': admin_user['password']}, ) assert response.status_code == 302 assert 'example.net' not in response.headers['Location'] def test_setup_requires_token(client, monkeypatch, tmp_path): from app_new.core.routes import setup_wizard monkeypatch.setattr(setup_wizard, 'DATA_DIR', str(tmp_path)) assert client.get('/setup-wizard/').status_code == 403 assert client.get('/setup-wizard/?token=invalid').status_code == 403 def test_setup_token_is_removed_from_url(client, monkeypatch, tmp_path): from app_new.core.routes import setup_wizard monkeypatch.setattr(setup_wizard, 'DATA_DIR', str(tmp_path)) token = 'test-setup-token-with-at-least-32-characters' response = client.get(f'/setup-wizard/?token={token}') assert response.status_code == 302 assert 'token=' not in response.headers['Location'] assert client.get('/setup-wizard/').status_code == 200 def test_completed_setup_rejects_mutation(client, monkeypatch, tmp_path): from app_new.core.routes import setup_wizard monkeypatch.setattr(setup_wizard, 'DATA_DIR', str(tmp_path)) Path(tmp_path, '.setup_complete').write_text('complete', encoding='utf-8') response = client.post('/setup-wizard/api/step/1', json={ 'username': 'new-admin', 'password': 'a-strong-password', }) assert response.status_code == 403 def _authorize_setup(client, monkeypatch, tmp_path): from app_new.core.routes import setup_wizard monkeypatch.setattr(setup_wizard, 'DATA_DIR', str(tmp_path)) token = 'test-setup-token-with-at-least-32-characters' response = client.get(f'/setup-wizard/?token={token}') assert response.status_code == 302 def test_setup_updates_buildings_without_deleting_existing(client, app, monkeypatch, tmp_path): from app_new import db from app_new.core.models.college import Building _authorize_setup(client, monkeypatch, tmp_path) with app.app_context(): existing = Building(name='Bâtiment conservé', description='Ancienne description') untouched = Building(name='Bâtiment hors formulaire') db.session.add_all([existing, untouched]) db.session.commit() existing_id, untouched_id = existing.id, untouched.id response = client.post('/setup-wizard/api/step/3', json={'buildings': [ {'id': existing_id, 'name': 'Bâtiment conservé', 'description': 'Mise à jour'}, {'name': 'Nouveau bâtiment', 'description': ''}, ]}) assert response.status_code == 200 with app.app_context(): assert db.session.get(Building, existing_id).description == 'Mise à jour' assert db.session.get(Building, untouched_id) is not None assert Building.query.filter_by(name='Nouveau bâtiment').count() == 1 def test_setup_creates_zones_and_rooms_with_valid_links(client, app, monkeypatch, tmp_path): from app_new import db from app_new.core.models.college import Building, Room, Zone _authorize_setup(client, monkeypatch, tmp_path) with app.app_context(): building = Building(name='Bâtiment relations wizard') db.session.add(building) db.session.commit() building_id = building.id zone_response = client.post('/setup-wizard/api/step/4', json={'zones': [ {'name': 'Zone wizard', 'building_id': building_id}, ]}) assert zone_response.status_code == 200 zone_id = zone_response.get_json()['zones'][0]['id'] room_response = client.post('/setup-wizard/api/step/5', json={'rooms': [ {'name': 'Salle wizard', 'code': 'W01', 'building_id': building_id, 'zone_id': zone_id, 'floor': 1}, ]}) assert room_response.status_code == 200 with app.app_context(): zone = db.session.get(Zone, zone_id) room = Room.query.filter_by(name='Salle wizard').one() assert zone.building_id == building_id assert room.building_id == building_id assert room.zone_id == zone_id assert room.floor == 1 def test_setup_imports_embedded_lot_catalog_idempotently(client, app, monkeypatch, tmp_path): from app_new.core.models.equipment import EquipmentCategory from app_new.core.models.maintenance import Lot from app_new.core.setup_catalog import EQUIPMENT_CATEGORIES, LOTS _authorize_setup(client, monkeypatch, tmp_path) first = client.post('/setup-wizard/api/step/6', json={}) second = client.post('/setup-wizard/api/step/6', json={}) assert first.status_code == second.status_code == 200 assert first.get_json()['categories_count'] == len(EQUIPMENT_CATEGORIES) assert first.get_json()['lots_count'] == len(LOTS) with app.app_context(): for name in EQUIPMENT_CATEGORIES: assert EquipmentCategory.query.filter_by(name=name).count() == 1 for name, category_name in LOTS: lot = Lot.query.filter_by(name=name).one() assert lot.category.name == category_name def test_setup_saves_exact_work_days_inside_school_holidays(client, app, monkeypatch, tmp_path): from datetime import date, time from app_new.core.models.planning import CollegeClosure, ClosureWorkDay from app_new.core.services.planning_service import PlanningService _authorize_setup(client, monkeypatch, tmp_path) response = client.post('/setup-wizard/api/step/7', json={ 'zone': 'C', 'annee': 2026, 'import_vacances': True, 'import_feries': True, 'vacation_periods': [{ 'name': 'Petites vacances test wizard', 'start_date': '2026-10-19', 'end_date': '2026-10-30', 'work_days': [ { 'work_date': '2026-10-19', 'start_time': '08:00', 'end_time': '16:00', 'lunch_start': '12:00', 'lunch_end': '12:45', 'notes': 'Début des vacances', }, { 'work_date': '2026-10-30', 'start_time': '09:15', 'end_time': '13:30', 'lunch_start': '', 'lunch_end': '', 'notes': 'Fin des vacances', }, ], }], }) assert response.status_code == 200 with app.app_context(): closure = CollegeClosure.query.filter_by(name='Petites vacances test wizard').one() assert closure.work_hours_type == 'none' assert ClosureWorkDay.query.filter_by(closure_id=closure.id).count() == 2 assert PlanningService.get_working_hours(date(2026, 10, 20)) is None assert PlanningService.get_working_hours(date(2026, 10, 19)) == ( time(8, 0), time(16, 0), time(12, 0), time(12, 45) ) assert PlanningService.get_working_hours(date(2026, 10, 30)) == ( time(9, 15), time(13, 30), None, None ) def test_setup_rejects_work_day_outside_vacation_period(client, monkeypatch, tmp_path): _authorize_setup(client, monkeypatch, tmp_path) response = client.post('/setup-wizard/api/step/7', json={ 'zone': 'C', 'annee': 2026, 'vacation_periods': [{ 'name': 'Vacances invalides', 'start_date': '2026-12-20', 'end_date': '2027-01-03', 'work_days': [{'work_date': '2026-12-10', 'start_time': '08:00', 'end_time': '16:00'}], }], }) assert response.status_code == 400 assert 'hors de la période' in response.get_json()['error']