"""Migrations Alembic : installation neuve et reprise d'une base existante.""" from __future__ import annotations import sqlite3 from pathlib import Path from sqlalchemy import create_engine, inspect, text from app.db.migrations import run_migrations # Schema exact d'avant l'introduction d'Alembic (aucune colonne expires_at). LEGACY_SCHEMA = """ CREATE TABLE shared_maps ( id INTEGER NOT NULL, share_token VARCHAR(64) NOT NULL, management_token_hash VARCHAR(64) NOT NULL, title VARCHAR(255) NOT NULL, map_version INTEGER NOT NULL, map_data TEXT NOT NULL, created_at DATETIME, updated_at DATETIME, PRIMARY KEY (id) ); CREATE UNIQUE INDEX ix_shared_maps_share_token ON shared_maps (share_token); """ LEGACY_ROWS = [ (1, "tokenA", "hashA", "Infrastructure", 3, '{"version":3,"nodes":[]}'), (2, "tokenB", "hashB", "Projet", 4, '{"version":4,"nodes":[{"id":"a"}]}'), (3, "tokenC", "hashC", "Reunion", 1, '{"version":1,"nodes":[]}'), ] def _make_legacy_database(path: Path) -> None: connection = sqlite3.connect(path) try: connection.executescript(LEGACY_SCHEMA) connection.executemany( "INSERT INTO shared_maps " "(id, share_token, management_token_hash, title, map_version, map_data," " created_at, updated_at) " "VALUES (?, ?, ?, ?, ?, ?, '2026-01-01 10:00:00', '2026-01-02 11:00:00')", LEGACY_ROWS, ) connection.commit() finally: connection.close() def _columns(engine) -> set[str]: # type: ignore[no-untyped-def] with engine.connect() as connection: return {column["name"] for column in inspect(connection).get_columns("shared_maps")} def test_fresh_database_gets_the_full_schema(tmp_path: Path) -> None: url = f"sqlite:///{tmp_path / 'fresh.db'}" engine = create_engine(url, future=True) run_migrations(engine, url) assert "expires_at" in _columns(engine) with engine.connect() as connection: tables = set(inspect(connection).get_table_names()) assert {"shared_maps", "alembic_version"} <= tables def test_migrations_are_idempotent(tmp_path: Path) -> None: url = f"sqlite:///{tmp_path / 'twice.db'}" engine = create_engine(url, future=True) run_migrations(engine, url) run_migrations(engine, url) assert "expires_at" in _columns(engine) def test_legacy_database_is_upgraded_without_data_loss(tmp_path: Path) -> None: path = tmp_path / "legacy.db" _make_legacy_database(path) url = f"sqlite:///{path}" engine = create_engine(url, future=True) # Etat de depart : ancienne structure, sans table de version Alembic. assert "expires_at" not in _columns(engine) with engine.connect() as connection: assert "alembic_version" not in set(inspect(connection).get_table_names()) run_migrations(engine, url) assert "expires_at" in _columns(engine) with engine.connect() as connection: rows = connection.execute( text( "SELECT id, share_token, management_token_hash, title, map_version," " map_data, expires_at FROM shared_maps ORDER BY id" ) ).all() assert len(rows) == len(LEGACY_ROWS) for row, expected in zip(rows, LEGACY_ROWS, strict=True): assert row.id == expected[0] assert row.share_token == expected[1] assert row.management_token_hash == expected[2] assert row.title == expected[3] assert row.map_version == expected[4] assert row.map_data == expected[5] # Tous les partages existants restent valides : aucune expiration. assert row.expires_at is None def test_legacy_database_keeps_working_after_upgrade(tmp_path: Path) -> None: """Apres migration, les anciennes lignes restent lisibles par l'ORM.""" path = tmp_path / "legacy2.db" _make_legacy_database(path) url = f"sqlite:///{path}" engine = create_engine(url, future=True) run_migrations(engine, url) from sqlalchemy.orm import Session from app.models.share import SharedMap with Session(engine) as session: share = session.query(SharedMap).filter_by(share_token="tokenB").one() assert share.title == "Projet" assert share.expires_at is None assert share.is_expired() is False