L'admin (vaults: ["*"]) voyait le home de chaque utilisateur dans sa barre latérale : "*" ouvrait tous les vaults, home-* compris. - backend/auth/middleware.py : check_vault_access exige un octroi explicite pour tout vault home-* (nouveau is_home_vault()). - Filtres « * » en dur remplacés par check_vault_access : dashboard, conflits, liens retour, favoris, abonnements push. - /api/search : search_vaults(is_allowed=…) filtre les bruts avant pagination (total et page restent justes). - backend/user_home.py : _grant n'écarte plus les comptes « * » — l'admin reçoit son propre home-admin (auto-réparé au démarrage). - Tests : test_user_home.py +2, assertion API inversée dans test_auth_api.py (admin ne voit plus home-alice).
267 lines
9.1 KiB
Python
267 lines
9.1 KiB
Python
# tests/test_user_home.py — #194 dossier personnel par utilisateur
|
|
import asyncio
|
|
import json
|
|
import os
|
|
|
|
import pytest
|
|
|
|
|
|
def _run(coro):
|
|
loop = asyncio.new_event_loop()
|
|
try:
|
|
return loop.run_until_complete(coro)
|
|
finally:
|
|
loop.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def home_env(tmp_path, monkeypatch):
|
|
"""Racine Home + registre vaults + users.json isolés, globals sauvegardés."""
|
|
root = tmp_path / "Home"
|
|
root.mkdir()
|
|
monkeypatch.setenv("OBSIGATE_HOME_ROOT", str(root))
|
|
|
|
from backend import indexer
|
|
|
|
monkeypatch.setattr(indexer, "DYNAMIC_VAULTS_FILE", tmp_path / "vaults.json")
|
|
|
|
from backend.auth import user_store
|
|
|
|
monkeypatch.setattr(user_store, "USERS_FILE", tmp_path / "users.json")
|
|
|
|
from backend import watcher_state
|
|
|
|
monkeypatch.setattr(watcher_state, "get_watcher", lambda: None)
|
|
|
|
saved_index = dict(indexer.index)
|
|
saved_config = dict(indexer.vault_config)
|
|
yield root
|
|
indexer.index.clear()
|
|
indexer.index.update(saved_index)
|
|
indexer.vault_config.clear()
|
|
indexer.vault_config.update(saved_config)
|
|
|
|
|
|
def _create_user(name: str) -> dict:
|
|
from backend.auth.user_store import create_user
|
|
|
|
return create_user(name, "Motdepasse1!", role="user")
|
|
|
|
|
|
def _registry() -> dict:
|
|
"""Contenu du registre dynamique (chemin monkeypatché par home_env)."""
|
|
from backend import indexer
|
|
|
|
return json.loads(indexer.DYNAMIC_VAULTS_FILE.read_text(encoding="utf-8"))
|
|
|
|
|
|
class TestEnsureUserHome:
|
|
def test_creates_folder_vault_and_grant(self, home_env):
|
|
from backend.auth.user_store import get_user
|
|
from backend.indexer import index
|
|
from backend.user_home import ensure_user_home
|
|
|
|
_create_user("alice")
|
|
name = _run(ensure_user_home("alice"))
|
|
|
|
assert name == "home-alice"
|
|
assert (home_env / "alice").is_dir()
|
|
assert "home-alice" in index
|
|
assert "home-alice" in get_user("alice")["vaults"]
|
|
# Registre persistant rempli → survit au prochain rebuild/redémarrage
|
|
assert "home-alice" in _registry()["vaults"]
|
|
|
|
def test_is_idempotent(self, home_env):
|
|
from backend.auth.user_store import get_user
|
|
from backend.indexer import index
|
|
from backend.user_home import ensure_user_home
|
|
|
|
_create_user("bob")
|
|
_run(ensure_user_home("bob"))
|
|
index_snapshot = dict(index)
|
|
_run(ensure_user_home("bob"))
|
|
|
|
assert get_user("bob")["vaults"].count("home-bob") == 1
|
|
assert index == index_snapshot
|
|
|
|
def test_invalid_username_never_touches_disk(self, home_env):
|
|
from backend.indexer import index
|
|
from backend.user_home import ensure_user_home
|
|
|
|
assert _run(ensure_user_home("../evil")) is None
|
|
assert _run(ensure_user_home("a/b")) is None
|
|
assert not (home_env.parent / "evil").exists()
|
|
assert not any(name.startswith("home-") for name in index)
|
|
|
|
def test_disabled_without_env(self, home_env, monkeypatch):
|
|
from backend.user_home import ensure_user_home
|
|
|
|
monkeypatch.delenv("OBSIGATE_HOME_ROOT")
|
|
assert _run(ensure_user_home("alice")) is None
|
|
assert not (home_env / "alice").exists()
|
|
|
|
|
|
class TestPersistence:
|
|
def test_registry_reloaded_by_load_vault_config(self, home_env):
|
|
"""data/vaults.json doit être relu par load_vault_config (rebuild/restart)."""
|
|
from backend import indexer
|
|
|
|
_create_user("carol")
|
|
from backend.user_home import ensure_user_home
|
|
|
|
_run(ensure_user_home("carol"))
|
|
|
|
# Simule un redémarrage : vault_config vidé, rechargé depuis la source
|
|
indexer.vault_config.clear()
|
|
config = indexer.load_vault_config()
|
|
assert config["home-carol"]["path"] == str(home_env / "carol")
|
|
|
|
def test_unpersist_drops_registry_entry(self, home_env):
|
|
from backend import indexer
|
|
from backend.user_home import ensure_user_home
|
|
|
|
_create_user("dan")
|
|
_run(ensure_user_home("dan"))
|
|
indexer.unpersist_vault("home-dan")
|
|
assert "home-dan" not in _registry()["vaults"]
|
|
|
|
|
|
class TestRelease:
|
|
def test_release_closes_vault_but_keeps_folder(self, home_env):
|
|
from backend.indexer import index
|
|
from backend.user_home import ensure_user_home, release_user_home
|
|
|
|
_create_user("erin")
|
|
_run(ensure_user_home("erin"))
|
|
assert "home-erin" in index
|
|
|
|
_run(release_user_home("erin"))
|
|
assert "home-erin" not in index
|
|
assert (home_env / "erin").is_dir() # décision : dossier conservé
|
|
assert "home-erin" not in _registry()["vaults"]
|
|
|
|
|
|
class TestOrphanSweep:
|
|
def test_orphan_home_released_when_user_deleted_outside_route(self, home_env):
|
|
"""create_admin.py delete ne passe pas par la route → réparé au boot."""
|
|
from backend.auth.user_store import delete_user
|
|
from backend.indexer import index
|
|
from backend.user_home import ensure_all_user_homes, ensure_user_home
|
|
|
|
_create_user("hank")
|
|
_run(ensure_user_home("hank"))
|
|
delete_user("hank")
|
|
assert "home-hank" in index
|
|
|
|
_run(ensure_all_user_homes())
|
|
assert "home-hank" not in index
|
|
assert (home_env / "hank").is_dir()
|
|
|
|
def test_vault_named_home_outside_root_is_left_alone(self, home_env):
|
|
"""Un vault admin nommé « home-… » ailleurs dans le FS n'est pas touché."""
|
|
from backend.indexer import index
|
|
from backend.user_home import ensure_all_user_homes
|
|
|
|
other = home_env.parent / "Elsewhere"
|
|
other.mkdir()
|
|
_run(add_vault("home-keep", str(other)))
|
|
|
|
_run(ensure_all_user_homes())
|
|
assert "home-keep" in index
|
|
|
|
|
|
def add_vault(name: str, path: str):
|
|
from backend.indexer import add_vault_to_index
|
|
|
|
return add_vault_to_index(name, path)
|
|
|
|
|
|
class TestIsolation:
|
|
def test_user_cannot_access_another_home_vault(self, home_env):
|
|
from backend.auth.middleware import check_vault_access
|
|
from backend.auth.user_store import get_user
|
|
from backend.user_home import ensure_user_home
|
|
|
|
_create_user("alice")
|
|
_create_user("bob")
|
|
_run(ensure_user_home("alice"))
|
|
_run(ensure_user_home("bob"))
|
|
|
|
assert check_vault_access("home-alice", get_user("alice")) is True
|
|
assert check_vault_access("home-alice", get_user("bob")) is False
|
|
assert check_vault_access("home-bob", get_user("alice")) is False
|
|
|
|
def test_wildcard_admin_does_not_cover_homes(self, home_env):
|
|
"""#194 : « * » n'inclut JAMAIS un dossier perso, même pour l'admin."""
|
|
from backend.auth.middleware import check_vault_access
|
|
from backend.auth.user_store import create_user, get_user
|
|
from backend.user_home import ensure_user_home
|
|
|
|
create_user("root", "Motdepasse1!", role="admin", vaults=["*"])
|
|
_create_user("alice")
|
|
_run(ensure_user_home("alice"))
|
|
|
|
admin = get_user("root")
|
|
assert check_vault_access("home-alice", admin) is False # pas le sien
|
|
assert check_vault_access("TestVault", admin) is True # « * » intact
|
|
|
|
# L'octroi explicite suffit (y compris pour l'admin, cf. _grant)
|
|
_run(ensure_user_home("root"))
|
|
assert check_vault_access("home-root", get_user("root")) is True
|
|
|
|
|
|
class TestSearchFiltering:
|
|
def test_search_service_filters_before_pagination(self, monkeypatch):
|
|
"""#194 : /api/search ne doit pas renvoyer les notes d'un autre user."""
|
|
import backend.search as search_mod
|
|
from backend.services import search as search_service
|
|
|
|
hits = [
|
|
{"vault": "home-bruno", "path": "secret.md"},
|
|
{"vault": "TestVault", "path": "note.md"},
|
|
{"vault": "home-bruno", "path": "autre.md"},
|
|
]
|
|
monkeypatch.setattr(
|
|
search_mod, "search",
|
|
lambda q, vault_filter="all", tag_filter=None: list(hits),
|
|
)
|
|
|
|
page = search_service.search_vaults("x", limit=50, is_allowed=lambda v: v == "TestVault")
|
|
assert page["total"] == 1, "total = résultats filtrés"
|
|
assert [r["vault"] for r in page["results"]] == ["TestVault"]
|
|
|
|
# Sans filtre (tool layer, qui filtre lui-même) : inchangé.
|
|
assert search_service.search_vaults("x", limit=50)["total"] == 3
|
|
|
|
|
|
class TestEnsureAll:
|
|
def test_repairs_every_existing_user(self, home_env):
|
|
from backend.auth.user_store import get_user
|
|
from backend.user_home import ensure_all_user_homes
|
|
|
|
_create_user("fab")
|
|
_create_user("gab")
|
|
# Dossier supprimé à la main + user créé hors API (pas de vault)
|
|
(home_env / "fab").mkdir()
|
|
(home_env / "fab").rmdir()
|
|
|
|
created = _run(ensure_all_user_homes())
|
|
|
|
assert created == 2
|
|
assert (home_env / "fab").is_dir()
|
|
assert (home_env / "gab").is_dir()
|
|
assert "home-fab" in get_user("fab")["vaults"]
|
|
assert "home-gab" in get_user("gab")["vaults"]
|
|
|
|
|
|
def test_home_root_env_contract():
|
|
"""OBSIGATE_HOME_ROOT absent → fonctionnalité inactive (dev/tests/desktop)."""
|
|
from backend.user_home import home_root
|
|
|
|
saved = os.environ.pop("OBSIGATE_HOME_ROOT", None)
|
|
try:
|
|
assert home_root() is None
|
|
finally:
|
|
if saved is not None:
|
|
os.environ["OBSIGATE_HOME_ROOT"] = saved
|