Files
ObsiGate/tests/test_directed_shares.py
T
bruno de6bde1613
CI / lint (push) Successful in 3m1s
CI / security (push) Failing after 1m58s
CI / test (push) Successful in 4m43s
CI / build (push) Successful in 1m39s
CI / e2e (push) Successful in 20m28s
fix: partage dirigé — dossier Partage virtuel + ouverture en onglet #196
- _resolve_shared_file: home-<user>/Partage/<f> -> fichier source (/api/file, raw, download)
- clic dossier Partage: dépliage sans navigation (fix Directory not found)
- fichiers reçus ouverts en onglet applicatif (arbre, recherche, dashboard)
- POST /api/share: path vide/null rejeté (400) — un share path:null cassait /api/shares
- +2 tests (13 total)
2026-10-10 21:48:46 -04:00

200 lines
8.3 KiB
Python

# #196 — Partage dirigé entre utilisateurs : gate des pages /s/*, scope de
# /api/shares, révocabilité, validation des destinataires.
# Fixture admin_client (conftest.py) : users admin (role admin, vaults ["*"])
# et normaluser (role user, vaults ["TestVault"]), auth activée.
import os
from pathlib import Path
import pytest
def _logged_client(username, password):
"""Fresh TestClient with a session (cookie) — /s/* reads the access_token cookie."""
from fastapi.testclient import TestClient
from backend.main import app
c = TestClient(app)
resp = c.post("/api/auth/login", json={"username": username, "password": password})
assert resp.status_code == 200, resp.text
return c
def _ensure_shared_file():
# admin_client chdir(tmp_path) mais le vault indexé est VAULT_1_PATH
# (absolu, résolu avant le chdir) → écrire là, pas dans cwd.
vault = Path(os.environ["VAULT_1_PATH"])
vault.mkdir(parents=True, exist_ok=True)
target = vault / "share_directed.md"
target.write_text("# Partagé\n\ncontenu dirigé\n", encoding="utf-8")
return "share_directed.md"
@pytest.fixture
def sessions(admin_client):
"""admin (creator) and normaluser (recipient) clients, logged in via cookie."""
return _logged_client("admin", "chab30"), _logged_client("normaluser", "normal123")
class TestDirectedShareGate:
def test_create_directed_share(self, sessions):
admin_client, _ = sessions
path = _ensure_shared_file()
resp = admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["normaluser"],
})
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["shared_with"] == ["normaluser"]
def test_create_directed_unknown_recipient_rejected(self, sessions):
admin_client, _ = sessions
path = _ensure_shared_file()
resp = admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["ghost"],
})
assert resp.status_code == 400
def test_recipient_can_view_but_anon_cannot(self, sessions):
admin_client, user_client = sessions
path = _ensure_shared_file()
resp = admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["normaluser"],
})
token = resp.json()["token"]
# Anonymous → 404 (pas 401/403 : on ne fuite pas l'existence du token)
from fastapi.testclient import TestClient
from backend.main import app
anon = TestClient(app)
assert anon.get(f"/s/{token}").status_code == 404
# Recipient (cookie de session via Bearer) → 200
assert user_client.get(f"/s/{token}").status_code == 200
# Creator → 200
assert admin_client.get(f"/s/{token}").status_code == 200
def test_other_user_cannot_view(self, sessions):
admin_client, _ = sessions
path = _ensure_shared_file()
resp = admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["admin"], # dirigé, mais pas normaluser
})
token = resp.json()["token"]
other = _logged_client("normaluser", "normal123")
assert other.get(f"/s/{token}").status_code == 404
def test_public_share_still_anonymous(self, sessions):
admin_client, _ = sessions
path = _ensure_shared_file()
resp = admin_client.post("/api/share/TestVault", json={"path": path})
token = resp.json()["token"]
from fastapi.testclient import TestClient
from backend.main import app
anon = TestClient(app)
assert anon.get(f"/s/{token}").status_code == 200
def test_shares_list_scoped_for_user(self, sessions):
admin_client, user_client = sessions
path = _ensure_shared_file()
admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["normaluser"],
})
# normaluser (non-admin) voit le partage reçu
resp = user_client.get("/api/shares")
assert resp.status_code == 200
tokens = [s["token"] for s in resp.json()]
assert tokens, "destinataire doit voir le partage reçu"
# Un admin voit tout
resp = admin_client.get("/api/shares")
assert resp.status_code == 200
assert len(resp.json()) >= 1
def test_recipient_cannot_revoke(self, sessions):
admin_client, user_client = sessions
path = _ensure_shared_file()
resp = admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["normaluser"],
})
sid = resp.json()["id"]
resp = user_client.delete(f"/api/share/{sid}")
assert resp.status_code == 403
def test_creator_can_revoke(self, sessions):
admin_client, _ = sessions
path = _ensure_shared_file()
resp = admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["normaluser"],
})
sid = resp.json()["id"]
assert admin_client.delete(f"/api/share/{sid}").status_code == 200
def test_revoke_cuts_recipient_access(self, sessions):
admin_client, user_client = sessions
path = _ensure_shared_file()
resp = admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["normaluser"],
})
body = resp.json()
assert user_client.get(f"/s/{body['token']}").status_code == 200
admin_client.delete(f"/api/share/{body['id']}")
assert user_client.get(f"/s/{body['token']}").status_code == 404
def test_search_finds_received_share_for_recipient(self, sessions):
# #196 : le contenu du document reçu est cherchable par le destinataire.
admin_client, user_client = sessions
path = _ensure_shared_file()
resp = admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["normaluser"],
})
token = resp.json()["token"]
resp = user_client.get("/api/search", params={"q": "dirigé", "vault": "all"})
assert resp.status_code == 200
hits = [r for r in resp.json()["results"] if r.get("share_token") == token]
assert hits, "le destinataire doit trouver le document reçu via la recherche"
assert hits[0]["vault"] == "home-normaluser"
assert hits[0]["path"].startswith("Partage/")
def test_search_does_not_leak_share_to_other_user(self, sessions):
# Un utilisateur non destinataire ne voit JAMAIS le contenu partagé
# dans ses résultats (le vault source lui est interdit).
admin_client, user_client = sessions
path = _ensure_shared_file()
admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["admin"], # dirigé à admin seul
})
resp = user_client.get("/api/search", params={"q": "dirigé", "vault": "all"})
assert resp.status_code == 200
assert not [r for r in resp.json()["results"] if r.get("share_token")]
def test_recipient_opens_shared_file_in_app(self, sessions):
# #196 : /api/file résout home-<user>/Partage/<fichier> vers la source.
admin_client, user_client = sessions
path = _ensure_shared_file()
resp = admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["normaluser"],
})
assert resp.status_code == 200
r = user_client.get("/api/file/home-normaluser", params={"path": "Partage/share_directed.md"})
assert r.status_code == 200, r.text
body = r.json()
assert body["is_markdown"] is True
assert "dirigé" in body["html"]
# raw endpoint too
r = user_client.get("/api/file/home-normaluser/raw", params={"path": "Partage/share_directed.md"})
assert r.status_code == 200
assert "dirigé" in r.json()["raw"]
def test_shared_file_resolution_is_user_scoped(self, sessions):
# home-admin/Partage/x.md demandé par normaluser → pas de mapping
# (le home d'un autre user lui est interdit, 403 avant tout).
admin_client, user_client = sessions
path = _ensure_shared_file()
admin_client.post("/api/share/TestVault", json={
"path": path, "shared_with": ["normaluser"],
})
r = user_client.get("/api/file/home-admin", params={"path": "Partage/share_directed.md"})
assert r.status_code == 403