fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
- Panneau peek: les bindings Alpine (x-data absent du conteneur) rendaient loovverture et le redimensionnement inoperants -> cblage direct sur le document. - Helper unique window.fdWirePeekResize (app.js): pointer capture, 300px-90vw, clic=fermer, largeur persiste fd_peek_width partagee entre les 4 peeks. - database-table-container margin:0 (tableau colle a gauche, marge Library). - .lib-container remonte dans app.css (trash etait pleine largeur), .db-index 1100px. - ObsiGate verifie sans code: creation .xlsx OK (openpyxl, #186).
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
"""FlowDeck — bouton « Home » de la sidebar : le paramètre ``?ws=`` est respecté.
|
||||
|
||||
Le sidebar construit ``/local-workspace?ws=<id>`` (premier workspace possédé,
|
||||
trié par nom). La route déclarait seulement ``folder`` : ``ws`` était ignoré et
|
||||
la page affichait le workspace **actif** (cookie), donc Home pouvait montrer un
|
||||
workspace différent de celui annoncé dans l'URL.
|
||||
|
||||
Ces tests verrouillent le contrat :
|
||||
* ``?ws=`` affiche bien le workspace demandé (et aligne le cookie) ;
|
||||
* un ``?ws=`` appartenant à quelqu'un d'autre est ignoré — aucune fuite ;
|
||||
* sans ``?ws=``, le comportement précédent (workspace actif) est inchangé.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from conftest import login_test_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
db_file = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
|
||||
db_path = db_file.name
|
||||
db_file.close()
|
||||
|
||||
os.environ["DATABASE_URL"] = f"sqlite:///{db_path}"
|
||||
os.environ["APP_SECRET_KEY"] = "test-secret-for-tests"
|
||||
os.environ["RATE_LIMIT_ENABLED"] = "false"
|
||||
os.environ["LLM_PROVIDER"] = "offline"
|
||||
|
||||
from app.config import settings
|
||||
from app.db import init_db
|
||||
from app.main import app
|
||||
|
||||
settings.database_url = f"sqlite:///{db_path}"
|
||||
settings.llm_provider = "offline"
|
||||
init_db()
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
client = login_test_client(TestClient(app))
|
||||
try:
|
||||
yield client
|
||||
finally:
|
||||
try:
|
||||
os.unlink(db_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def _make_workspace(client, name: str) -> dict:
|
||||
r = client.post("/api/workspaces", json={"name": name})
|
||||
assert r.status_code == 200, r.text
|
||||
return r.json()
|
||||
|
||||
|
||||
def _workspace_id_in(client, workspace_id: int) -> int | None:
|
||||
"""Lit le workspace_id embarqué dans le HTML rendu (lw-config)."""
|
||||
r = client.get(f"/local-workspace?ws={workspace_id}")
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.text
|
||||
marker = '"workspace_id":'
|
||||
idx = body.find(marker)
|
||||
assert idx != -1, "lw-config absent de la page"
|
||||
rest = body[idx + len(marker):].lstrip()
|
||||
digits = ""
|
||||
for ch in rest:
|
||||
if ch.isdigit():
|
||||
digits += ch
|
||||
else:
|
||||
break
|
||||
return int(digits) if digits else None
|
||||
|
||||
|
||||
def test_home_ws_param_is_honoured(client):
|
||||
"""Home doit afficher le workspace qu'il annonce, pas celui du cookie."""
|
||||
client.post("/auth/register", json={
|
||||
"email": "[email protected]", "password": "secret123", "name": "Home1"})
|
||||
zeta = _make_workspace(client, "Zeta Project") # id plus élevé
|
||||
alpha = _make_workspace(client, "Alpha Project") # premier par nom
|
||||
|
||||
# Le cookie « workspace actif » pointe sur Zeta…
|
||||
client.post(f"/api/workspaces/{zeta['id']}/select")
|
||||
|
||||
# …mais Home annonce le premier par nom (Alpha).
|
||||
assert _workspace_id_in(client, alpha["id"]) == alpha["id"]
|
||||
assert _workspace_id_in(client, zeta["id"]) == zeta["id"]
|
||||
|
||||
|
||||
def test_home_ws_param_title_matches_content(client):
|
||||
"""Le titre affiché doit être celui du workspace demandé, pas celui du cookie.
|
||||
|
||||
Régression : la sidebar est calculée avant le changement de workspace, donc
|
||||
`active_ws_name` désignait l'ancien workspace — le contenu aurait été celui
|
||||
de Zeta sous le titre « Alpha Project ».
|
||||
"""
|
||||
client.post("/auth/register", json={
|
||||
"email": "[email protected]", "password": "secret123", "name": "Home5"})
|
||||
zeta = _make_workspace(client, "Zeta Project")
|
||||
alpha = _make_workspace(client, "Alpha Project")
|
||||
client.post(f"/api/workspaces/{zeta['id']}/select")
|
||||
|
||||
r = client.get(f"/local-workspace?ws={alpha['id']}")
|
||||
assert r.status_code == 200
|
||||
# La sidebar liste légitimement tous les workspaces : on cible le <title>,
|
||||
# qui reflète le workspace dont le contenu est rendu.
|
||||
import re
|
||||
|
||||
title = re.search(r"<title>(.*?)</title>", r.text, re.S).group(1)
|
||||
assert "Alpha Project" in title
|
||||
assert "Zeta Project" not in title
|
||||
|
||||
|
||||
def test_home_ws_param_aligns_cookie(client):
|
||||
"""Après un Home vers ?ws=, le cookie doit suivre le workspace affiché."""
|
||||
client.post("/auth/register", json={
|
||||
"email": "[email protected]", "password": "secret123", "name": "Home2"})
|
||||
a = _make_workspace(client, "AAA")
|
||||
b = _make_workspace(client, "BBB")
|
||||
client.post(f"/api/workspaces/{a['id']}/select")
|
||||
|
||||
r = client.get(f"/local-workspace?ws={b['id']}")
|
||||
assert r.status_code == 200
|
||||
assert client.cookies.get("flowdeck_workspace") == str(b["id"])
|
||||
|
||||
|
||||
def test_foreign_ws_param_is_ignored(client):
|
||||
"""Un ?ws= appartenant à un autre utilisateur ne doit rien divulguer."""
|
||||
client.post("/auth/register", json={
|
||||
"email": "[email protected]", "password": "secret123", "name": "Home3"})
|
||||
mine = _make_workspace(client, "Mine")
|
||||
|
||||
# Workspace appartenant à un AUTRE utilisateur (FK users respectée)
|
||||
from app.db import get_conn
|
||||
from app.password_utils import hash_password
|
||||
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO users (login, email, full_name, password_hash, is_active) "
|
||||
"VALUES ('ghost', '[email protected]', 'Ghost', ?, 1)",
|
||||
(hash_password("secret123"),))
|
||||
conn.commit()
|
||||
ghost_id = conn.execute("SELECT id FROM users WHERE login='ghost'").fetchone()["id"]
|
||||
conn.execute("INSERT INTO workspaces (name, owner_id) VALUES (?, ?)",
|
||||
("Secret Corp", ghost_id))
|
||||
conn.commit()
|
||||
foreign = conn.execute(
|
||||
"SELECT id FROM workspaces WHERE name='Secret Corp'").fetchone()["id"]
|
||||
|
||||
r = client.get(f"/local-workspace?ws={foreign}")
|
||||
assert r.status_code == 200
|
||||
# Le workspace demandé n'est pas rendu : on retombe sur celui du visiteur.
|
||||
assert _workspace_id_in(client, foreign) == mine["id"]
|
||||
assert client.cookies.get("flowdeck_workspace") != str(foreign)
|
||||
assert "Secret Corp" not in r.text
|
||||
|
||||
|
||||
def test_without_ws_param_active_workspace_is_used(client):
|
||||
"""Sans ?ws=, le comportement historique est inchangé."""
|
||||
client.post("/auth/register", json={
|
||||
"email": "[email protected]", "password": "secret123", "name": "Home4"})
|
||||
_make_workspace(client, "Active")
|
||||
b = _make_workspace(client, "Other")
|
||||
client.post(f"/api/workspaces/{b['id']}/select")
|
||||
|
||||
assert _workspace_id_in(client, b["id"]) == b["id"]
|
||||
Reference in New Issue
Block a user