feat: partage dirigé entre utilisateurs #196
- create_share/shared_with + validation des destinataires
- gate auth sur /s/{token} (+pdf, raw) : 404 opaque hors créateur/admin/destinataires
- GET /api/shares scopé (non-admin = créés + reçus), révocation créateur/admin
- UI : dialogue dirigé/public + destinataires, dashboard « partagé par X »
- i18n FR/EN, tests test_directed_shares.py (9), fiche feature
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# #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
|
||||
Reference in New Issue
Block a user