Files
flowdeck/tests/test_home_workspace_param.py
bruno 1d1cdbd618
FlowDeck CI / test (push) Failing after 3h13m58s
FlowDeck CI / lint (push) Successful in 2m12s
FlowDeck CI / docker (push) Skipped
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).
2026-10-05 22:47:36 -04:00

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"]