Merge vues sauvegardées #95
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def users_db_path(monkeypatch, tmp_path):
|
||||
from src.auth.db import reset_conn_for_tests
|
||||
|
||||
db_path = tmp_path / "users.test.sqlite"
|
||||
monkeypatch.setenv("USERS_DB_PATH", str(db_path))
|
||||
reset_conn_for_tests()
|
||||
yield db_path
|
||||
reset_conn_for_tests()
|
||||
@@ -0,0 +1,34 @@
|
||||
import urllib.parse
|
||||
|
||||
from src.utils.table import build_view_query
|
||||
|
||||
|
||||
def test_empty_inputs_give_empty_string():
|
||||
assert build_view_query(None, None, None) == ""
|
||||
assert build_view_query("", [], []) == ""
|
||||
|
||||
|
||||
def test_filter_only():
|
||||
q = build_view_query("{objet} icontains route", None, None)
|
||||
params = urllib.parse.parse_qs(q)
|
||||
assert params["filtres"] == ["{objet} icontains route"]
|
||||
assert "tris" not in params
|
||||
assert "colonnes" not in params
|
||||
|
||||
|
||||
def test_sort_is_json_encoded():
|
||||
sort_by = [{"column_id": "montant", "direction": "desc"}]
|
||||
q = build_view_query(None, sort_by, None)
|
||||
params = urllib.parse.parse_qs(q)
|
||||
import json
|
||||
|
||||
assert json.loads(params["tris"][0]) == sort_by
|
||||
|
||||
|
||||
def test_hidden_columns_become_visible_csv():
|
||||
# build_view_query reçoit les colonnes MASQUÉES et stocke les VISIBLES
|
||||
q = build_view_query(None, None, ["objet"])
|
||||
params = urllib.parse.parse_qs(q)
|
||||
visible = params["colonnes"][0].split(",")
|
||||
assert "objet" not in visible
|
||||
assert len(visible) > 0
|
||||
@@ -0,0 +1,17 @@
|
||||
from src.pages import _compte_shell as shell
|
||||
|
||||
|
||||
def test_vues_section_is_gated_subscription():
|
||||
section = next(s for s in shell.SECTIONS if s["key"] == "vues")
|
||||
assert section["href"] == "/compte/vues"
|
||||
assert section["require_subscription"] is True
|
||||
|
||||
|
||||
def test_vues_hidden_without_subscription():
|
||||
keys = {s["key"] for s in shell.visible_sections(has_subscription=False)}
|
||||
assert "vues" not in keys
|
||||
|
||||
|
||||
def test_vues_visible_with_subscription():
|
||||
keys = {s["key"] for s in shell.visible_sections(has_subscription=True)}
|
||||
assert "vues" in keys
|
||||
@@ -0,0 +1,77 @@
|
||||
from src.auth import db as auth_db
|
||||
from src.saved_views import db
|
||||
|
||||
|
||||
def _make_user(email="u@ex.fr"):
|
||||
auth_db.init_schema()
|
||||
return auth_db.create_user(email, "hash")
|
||||
|
||||
|
||||
def test_init_schema_creates_table(users_db_path):
|
||||
db.init_schema()
|
||||
conn = auth_db.get_conn()
|
||||
tables = {
|
||||
row[0]
|
||||
for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
}
|
||||
assert "saved_views" in tables
|
||||
|
||||
|
||||
def test_upsert_creates_and_lists(users_db_path):
|
||||
db.init_schema()
|
||||
uid = _make_user()
|
||||
db.upsert(uid, "tableau", "Ma vue", "filtres=foo")
|
||||
views = db.list_views(uid, "tableau")
|
||||
assert len(views) == 1
|
||||
assert views[0]["name"] == "Ma vue"
|
||||
assert views[0]["query"] == "filtres=foo"
|
||||
|
||||
|
||||
def test_upsert_same_name_overwrites(users_db_path):
|
||||
db.init_schema()
|
||||
uid = _make_user()
|
||||
db.upsert(uid, "tableau", "Ma vue", "filtres=foo")
|
||||
db.upsert(uid, "tableau", "Ma vue", "filtres=bar")
|
||||
views = db.list_views(uid, "tableau")
|
||||
assert len(views) == 1
|
||||
assert views[0]["query"] == "filtres=bar"
|
||||
|
||||
|
||||
def test_list_views_is_isolated_per_user(users_db_path):
|
||||
db.init_schema()
|
||||
uid1 = _make_user("a@ex.fr")
|
||||
uid2 = _make_user("b@ex.fr")
|
||||
db.upsert(uid1, "tableau", "Vue A", "filtres=a")
|
||||
assert db.list_views(uid2, "tableau") == []
|
||||
|
||||
|
||||
def test_rename_only_affects_owner(users_db_path):
|
||||
db.init_schema()
|
||||
uid1 = _make_user("a@ex.fr")
|
||||
uid2 = _make_user("b@ex.fr")
|
||||
db.upsert(uid1, "tableau", "Vue A", "filtres=a")
|
||||
view_id = db.list_views(uid1, "tableau")[0]["id"]
|
||||
db.rename(view_id, uid2, "Pirate") # mauvais propriétaire → no-op
|
||||
assert db.get(view_id, uid1)["name"] == "Vue A"
|
||||
db.rename(view_id, uid1, "Vue B")
|
||||
assert db.get(view_id, uid1)["name"] == "Vue B"
|
||||
|
||||
|
||||
def test_delete_only_affects_owner(users_db_path):
|
||||
db.init_schema()
|
||||
uid1 = _make_user("a@ex.fr")
|
||||
uid2 = _make_user("b@ex.fr")
|
||||
db.upsert(uid1, "tableau", "Vue A", "filtres=a")
|
||||
view_id = db.list_views(uid1, "tableau")[0]["id"]
|
||||
db.delete(view_id, uid2) # mauvais propriétaire → no-op
|
||||
assert db.get(view_id, uid1) is not None
|
||||
db.delete(view_id, uid1)
|
||||
assert db.get(view_id, uid1) is None
|
||||
|
||||
|
||||
def test_views_deleted_on_user_cascade(users_db_path):
|
||||
db.init_schema()
|
||||
uid = _make_user()
|
||||
db.upsert(uid, "tableau", "Vue A", "filtres=a")
|
||||
auth_db.delete_user(uid)
|
||||
assert db.list_views(uid, "tableau") == []
|
||||
@@ -0,0 +1,59 @@
|
||||
from src.saved_views import ui
|
||||
|
||||
|
||||
class _Row(dict):
|
||||
"""Imite un sqlite3.Row : accès par clé."""
|
||||
|
||||
|
||||
def _view(view_id, name, query):
|
||||
return _Row(id=view_id, name=name, query=query)
|
||||
|
||||
|
||||
def test_bar_style_hidden_for_non_subscriber():
|
||||
assert ui.bar_style(False) == {"display": "none"}
|
||||
assert ui.bar_style(True) == {}
|
||||
|
||||
|
||||
def test_clean_view_name_strips_and_empties():
|
||||
assert ui.clean_view_name(" Ma vue ") == "Ma vue"
|
||||
assert ui.clean_view_name(" ") == ""
|
||||
assert ui.clean_view_name(None) == ""
|
||||
|
||||
|
||||
def test_prepare_refuses_non_subscriber():
|
||||
name, err = ui.prepare_view_to_save(False, "Ma vue")
|
||||
assert name is None
|
||||
assert err
|
||||
|
||||
|
||||
def test_prepare_refuses_empty_name():
|
||||
name, err = ui.prepare_view_to_save(True, " ")
|
||||
assert name is None
|
||||
assert err
|
||||
|
||||
|
||||
def test_prepare_accepts_valid():
|
||||
name, err = ui.prepare_view_to_save(True, " Ma vue ")
|
||||
assert name == "Ma vue"
|
||||
assert err is None
|
||||
|
||||
|
||||
def test_saved_views_items_build_links():
|
||||
items = ui.saved_views_items(
|
||||
[_view(1, "Vue A", "filtres=a"), _view(2, "Vue B", "tris=b")]
|
||||
)
|
||||
assert len(items) == 2
|
||||
assert items[0].href == "/tableau?filtres=a"
|
||||
assert items[0].children == "Vue A"
|
||||
|
||||
|
||||
def test_views_table_empty_state():
|
||||
out = ui.views_table([])
|
||||
# un Div non vide (message d'état) sans item de suppression
|
||||
assert out is not None
|
||||
|
||||
|
||||
def test_views_table_lists_views():
|
||||
out = ui.views_table([_view(1, "Vue A", "filtres=a")])
|
||||
text = str(out)
|
||||
assert "Vue A" in text
|
||||
Reference in New Issue
Block a user