import polars as pl import pytest @pytest.fixture def sample_lff(): """Small LazyFrame with the columns needed by add_links / format_values.""" return pl.LazyFrame( [ { "uid": "u1", "id": "u1", "acheteur_id": "12345678900011", "acheteur_nom": "Mairie de Test", "titulaire_id": "98765432100022", "titulaire_nom": "Entreprise Test", "titulaire_typeIdentifiant": "SIRET", "objet": "Travaux divers", "montant": 12500.0, "dateNotification": "2025-03-15", "codeCPV": "45000000", "dureeRestanteMois": 6, "titulaire_distance": 42.0, } ] ) def test_table_module_imports(): from src.utils import table assert hasattr(table, "prepare_table_data") def test_filter_table_data_does_not_call_track_search(monkeypatch, sample_lff): from src.utils import table calls = [] monkeypatch.setattr(table, "track_search", lambda *a, **kw: calls.append(a)) result = table.filter_table_data( sample_lff, "{objet} icontains travaux", "tableau" ).collect() assert calls == [] assert result.height == 1 def test_normalize_sort_by_handles_empty(): from src.utils.table import normalize_sort_by assert normalize_sort_by(None) == () assert normalize_sort_by([]) == () def test_normalize_sort_by_returns_hashable_tuple(): from src.utils.table import normalize_sort_by sort_by = [ {"column_id": "montant", "direction": "desc"}, {"column_id": "dateNotification", "direction": "asc"}, ] key = normalize_sort_by(sort_by) assert key == (("montant", "desc"), ("dateNotification", "asc")) # Must be hashable so that flask-caching can build a cache key from it hash(key) def test_normalize_sort_by_preserves_order(): """Order matters for sort: [A, B] != [B, A].""" from src.utils.table import normalize_sort_by a_then_b = normalize_sort_by( [{"column_id": "a", "direction": "asc"}, {"column_id": "b", "direction": "asc"}] ) b_then_a = normalize_sort_by( [{"column_id": "b", "direction": "asc"}, {"column_id": "a", "direction": "asc"}] ) assert a_then_b != b_then_a @pytest.fixture(scope="module") def flask_app(): """Minimal Flask app with SimpleCache so @cache.memoize() works in tests.""" from flask import Flask from utils.cache import cache app = Flask(__name__) cache.init_app(app, config={"CACHE_TYPE": "SimpleCache"}) return app @pytest.fixture(autouse=True) def reset_cache(flask_app): """Ensure the flask-caching backend is empty between tests so that cache-hit assertions are meaningful. Falls back to no-op when no Flask app context is active (NullCache).""" from utils.cache import cache with flask_app.app_context(): try: cache.clear() except (RuntimeError, AttributeError): # No app context — cache is NullCache, nothing to clear pass yield def test_load_filter_sort_postprocess_returns_dataframe( flask_app, monkeypatch, sample_lff ): from src.utils import table monkeypatch.setattr(table, "query_marches", lambda: sample_lff.collect()) with flask_app.app_context(): df = table._load_filter_sort_postprocess(filter_query=None, sort_by_key=()) assert isinstance(df, pl.DataFrame) assert df.height == 1 # All values must be strings after post-processing for col in df.columns: assert df.schema[col] == pl.String def test_load_filter_sort_postprocess_applies_filter( flask_app, monkeypatch, sample_lff ): from src.utils import table monkeypatch.setattr(table, "query_marches", lambda: sample_lff.collect()) with flask_app.app_context(): df = table._load_filter_sort_postprocess( filter_query="{objet} icontains travaux", sort_by_key=() ) assert df.height == 1 df_empty = table._load_filter_sort_postprocess( filter_query="{objet} icontains nonexistent", sort_by_key=() ) assert df_empty.height == 0 def test_load_filter_sort_postprocess_adds_links(flask_app, monkeypatch, sample_lff): from src.utils import table monkeypatch.setattr(table, "query_marches", lambda: sample_lff.collect()) with flask_app.app_context(): df = table._load_filter_sort_postprocess(filter_query=None, sort_by_key=()) # add_links injects an wrapper around uid, acheteur_nom, titulaire_nom assert "= 32 # uuid4 hex string def test_prepare_table_data_with_external_data_does_not_use_cache( monkeypatch, flask_app, sample_lff ): """When a caller passes data (acheteur/titulaire/observatoire path), bypass the memoized helper entirely.""" from src.utils import table sentinel = {"called": False} def should_not_be_called(*a, **kw): sentinel["called"] = True raise AssertionError("Memoized helper must not be called when data is provided") monkeypatch.setattr(table, "_load_filter_sort_postprocess", should_not_be_called) with flask_app.app_context(): table.prepare_table_data( data=sample_lff, # external LazyFrame data_timestamp=0, filter_query=None, page_current=0, page_size=20, sort_by=[], source_table="acheteur", ) assert sentinel["called"] is False