250 lines
7.6 KiB
Python
250 lines
7.6 KiB
Python
# tests/conftest.py — Shared fixtures for ObsiGate test suite
|
|
import os
|
|
import sys
|
|
import shutil
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
# Add project root to path so we can import backend modules
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_tool_ratelimit():
|
|
"""Isolate the per-identity tool rate limiter between tests."""
|
|
from backend.tools import ratelimit
|
|
|
|
ratelimit.reset()
|
|
yield
|
|
ratelimit.reset()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _disable_web_cache():
|
|
"""Web cache off by default: tests stay hermetic (no cross-test hits).
|
|
|
|
tests/test_web_cache.py re-enables it explicitly with a tmp path.
|
|
"""
|
|
from backend.tools import webcache
|
|
|
|
saved_path = os.environ.get("OBSIGATE_WEB_CACHE_PATH")
|
|
os.environ["OBSIGATE_WEB_CACHE_TTL"] = "0"
|
|
yield
|
|
if saved_path is None:
|
|
os.environ.pop("OBSIGATE_WEB_CACHE_PATH", None)
|
|
else:
|
|
os.environ["OBSIGATE_WEB_CACHE_PATH"] = saved_path
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_env():
|
|
"""Ensure no vault env vars leak between tests — but preserve test vault config."""
|
|
saved = {}
|
|
# Only clean VAULT_N/DIR_N vars, NOT OBSIGATE_ vars needed by the app
|
|
for key in list(os.environ.keys()):
|
|
if key.startswith(("VAULT_", "DIR_")):
|
|
saved[key] = os.environ.pop(key)
|
|
yield
|
|
# Restore
|
|
for key in list(os.environ.keys()):
|
|
if key.startswith(("VAULT_", "DIR_")):
|
|
if key not in saved:
|
|
os.environ.pop(key)
|
|
os.environ.update(saved)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_vault_dir(tmp_path: Path) -> str:
|
|
"""Create a temporary Obsidian-style vault with markdown files."""
|
|
vault = tmp_path / "TestVault"
|
|
vault.mkdir()
|
|
|
|
# Simple markdown file
|
|
(vault / "note1.md").write_text(
|
|
"---\ntags:\n - python\n - tutorial\ntitle: Introduction à Python\n---\n"
|
|
"# Introduction à Python\nPython est un langage de programmation moderne.\n"
|
|
"Il supporte la programmation orientée objet et fonctionnelle.\n"
|
|
"La syntaxe de Python est claire et lisible.\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
# File with inline tags and wikilinks
|
|
(vault / "note2.md").write_text(
|
|
"---\ntags:\n - docker\nstatut: actif\nauteur: Jean Dupont\ntitle: Docker Guide\n---\n"
|
|
"# Docker Guide\nDocker est une plateforme de conteneurisation.\n"
|
|
"Voir aussi [[Introduction à Python]] pour les scripts.\n"
|
|
"Et aussi [[Proxmox Setup]] pour l'infrastructure.\n"
|
|
"Un tag inline #devops pour le fun.\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
# File in subdirectory
|
|
sub = vault / "Projets"
|
|
sub.mkdir()
|
|
(sub / "projet.md").write_text(
|
|
"---\ntags:\n - projet\n - python\ntitle: Mon Projet\n---\n"
|
|
"# Mon Projet\nUtilise Docker et Python.\n"
|
|
"Voir [[Introduction à Python]].\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
# Non-markdown file
|
|
(vault / "config.json").write_text('{"key": "value"}', encoding="utf-8")
|
|
|
|
# Image attachments (roadmap #108) — indexed as metadata-only binaries and
|
|
# listed in the tree / browse endpoint.
|
|
import base64
|
|
|
|
(vault / "chatScreenshot.png").write_bytes(base64.b64decode(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAen63NgAAAAASUVORK5CYII="
|
|
))
|
|
(vault / "vector-icon.svg").write_text(
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2"></svg>',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
# File with accents in title
|
|
(vault / "café_crème.md").write_text(
|
|
"---\ntitle: Café Crème\n---\n# Café Crème\nUn bon café.\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
return str(vault)
|
|
|
|
|
|
@pytest.fixture
|
|
def app_with_vault(test_vault_dir: str):
|
|
"""Create a FastAPI TestClient with a test vault configured.
|
|
|
|
Imports app lazily to avoid side-effects at module load time.
|
|
Disables auth and watcher for testing.
|
|
"""
|
|
os.environ["VAULT_1_NAME"] = "TestVault"
|
|
os.environ["VAULT_1_PATH"] = test_vault_dir
|
|
os.environ["OBSIGATE_AUTH_ENABLED"] = "false"
|
|
|
|
# Prevent watcher from starting (not needed for tests)
|
|
import backend.main
|
|
backend.main._load_config = lambda: {"watcher_enabled": False}
|
|
|
|
from backend.main import app
|
|
from backend.indexer import build_index
|
|
import asyncio
|
|
|
|
# Build the index
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
loop.run_until_complete(build_index())
|
|
|
|
# Build inverted index for search
|
|
from backend.search import init_inverted_index
|
|
init_inverted_index()
|
|
|
|
# Build semantic (embedding) index — uses the dependency-free hash fallback
|
|
from backend.semantic_search import init_semantic_index
|
|
init_semantic_index()
|
|
|
|
client = TestClient(app)
|
|
return client
|
|
|
|
|
|
@ pytest.fixture
|
|
def client(app_with_vault):
|
|
"""Alias for app_with_vault."""
|
|
return app_with_vault
|
|
|
|
|
|
@ pytest.fixture
|
|
def admin_client(tmp_path):
|
|
"""TestClient with auth enabled, isolated temp data, admin user provisioned.
|
|
|
|
Used by tests that need admin authentication (e.g., /api/health/detailed).
|
|
"""
|
|
data_dir = tmp_path / "data"
|
|
data_dir.mkdir()
|
|
|
|
import json
|
|
from backend.auth.password import hash_password
|
|
|
|
pw_hash = hash_password("chab30")
|
|
users = {
|
|
"version": 1,
|
|
"users": {
|
|
"admin": {
|
|
"id": "admin-1",
|
|
"username": "admin",
|
|
"display_name": "admin",
|
|
"password_hash": pw_hash,
|
|
"role": "admin",
|
|
"vaults": ["*"],
|
|
"active": True,
|
|
"created_at": "2026-01-01T00:00:00",
|
|
},
|
|
"normaluser": {
|
|
"id": "user-1",
|
|
"username": "normaluser",
|
|
"display_name": "normal",
|
|
"password_hash": hash_password("normal123"),
|
|
"role": "user",
|
|
"vaults": ["TestVault"],
|
|
"active": True,
|
|
"created_at": "2026-01-01T00:00:00",
|
|
},
|
|
},
|
|
}
|
|
(data_dir / "users.json").write_text(json.dumps(users), encoding="utf-8")
|
|
|
|
src_secret = Path("data/secret.key")
|
|
if src_secret.exists():
|
|
shutil.copy2(str(src_secret), str(data_dir / "secret.key"))
|
|
|
|
orig_cwd = os.getcwd()
|
|
test_vault_path = os.path.abspath("test-vault")
|
|
os.chdir(str(tmp_path))
|
|
|
|
os.environ["VAULT_1_NAME"] = "TestVault"
|
|
os.environ["VAULT_1_PATH"] = test_vault_path
|
|
os.environ["OBSIGATE_AUTH_ENABLED"] = "true"
|
|
os.environ["OBSIGATE_ADMIN_USER"] = "admin"
|
|
os.environ["OBSIGATE_ADMIN_PASSWORD"] = "chab30"
|
|
os.environ["OBSIGATE_WATCHER_ENABLED"] = "false"
|
|
|
|
import backend.main
|
|
backend.main._load_config = lambda: {"watcher_enabled": False}
|
|
|
|
from backend.main import app
|
|
from backend.indexer import build_index, index
|
|
import asyncio
|
|
|
|
for key in list(index.keys()):
|
|
del index[key]
|
|
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
loop.run_until_complete(build_index())
|
|
|
|
from backend.search import init_inverted_index
|
|
init_inverted_index()
|
|
|
|
from backend.semantic_search import init_semantic_index
|
|
init_semantic_index()
|
|
|
|
from fastapi.testclient import TestClient
|
|
client = TestClient(app)
|
|
yield client
|
|
|
|
if hasattr(client, "close"):
|
|
client.close()
|
|
loop.run_until_complete(asyncio.sleep(0))
|
|
|
|
os.chdir(orig_cwd)
|
|
shutil.rmtree(str(tmp_path), ignore_errors=True)
|
|
for k in [
|
|
"VAULT_1_NAME", "VAULT_1_PATH", "OBSIGATE_AUTH_ENABLED",
|
|
"OBSIGATE_ADMIN_USER", "OBSIGATE_ADMIN_PASSWORD", "OBSIGATE_WATCHER_ENABLED",
|
|
]:
|
|
os.environ.pop(k, None)
|