- 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).
169 lines
6.2 KiB
Python
169 lines
6.2 KiB
Python
"""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"]
|