Files
ObsiGate/tests/test_services.py
bruno 4c4e415975
CI / lint (push) Successful in 58s
CI / security (push) Successful in 40s
CI / test (push) Successful in 1m15s
CI / build (push) Successful in 37s
CI / e2e (push) Successful in 10m15s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
feat(ai): services partages A2 + SSE streaming B4 + confirmations UI B5 (#79)
2026-09-11 17:06:40 -04:00

155 lines
6.7 KiB
Python

# tests/test_services.py — Unit tests for the shared business-logic services (A2)
"""Tests for backend.services: vaults, files, search, and error mapping.
The index-dependent tests reuse the ``client`` fixture from conftest, which
builds the in-memory index for a ``TestVault``.
"""
from pathlib import Path
import pytest
from backend.services.errors import ServiceError
from backend.services.files import read_file_text, read_raw_file
from backend.services.search import list_tags, search_vaults
from backend.services.vaults import browse_directory, list_accessible_vaults
ADMIN = {"username": "admin", "role": "admin", "vaults": ["*"]}
LIMITED = {"username": "limited", "vaults": ["OtherVault"]}
# ═══════════════════════════════════════════════════════════════════
# Vaults
# ═══════════════════════════════════════════════════════════════════
class TestListAccessibleVaults:
def test_admin_sees_vault(self, client):
names = {v["name"] for v in list_accessible_vaults(ADMIN)}
assert "TestVault" in names
def test_limited_user_sees_none(self, client):
assert list_accessible_vaults(LIMITED) == []
def test_metadata_fields(self, client):
vault = next(v for v in list_accessible_vaults(ADMIN) if v["name"] == "TestVault")
assert set(vault) == {"name", "file_count", "tag_count", "type"}
assert vault["file_count"] >= 1
class TestBrowseDirectory:
def test_root_lists_children(self, client):
data = browse_directory("TestVault", "")
names = {i["name"] for i in data["items"]}
assert "note1.md" in names
assert "Projets" in names
def test_missing_path_raises_not_found(self, client):
with pytest.raises(ServiceError) as exc:
browse_directory("TestVault", "nope")
assert exc.value.code == "not_found"
def test_unknown_vault_raises_not_found(self, client):
with pytest.raises(ServiceError) as exc:
browse_directory("MissingVault", "")
assert exc.value.code == "not_found"
# ═══════════════════════════════════════════════════════════════════
# Files
# ═══════════════════════════════════════════════════════════════════
class TestReadFile:
def test_read_text_redacts_secrets(self, client):
from backend.indexer import get_vault_data
root = Path(get_vault_data("TestVault")["path"])
secret = root / "svc_secret.md"
fake_jwt = "eyJ" + "a" * 30 + "." + "b" * 30 + "." + "c" * 30
secret.write_text(f"token: {fake_jwt}\n", encoding="utf-8")
try:
data = read_file_text("TestVault", "svc_secret.md")
assert "[JWT MASQUÉ]" in data["content"]
finally:
secret.unlink()
def test_read_text_too_large(self, client):
from backend.indexer import get_vault_data
root = Path(get_vault_data("TestVault")["path"])
big = root / "svc_big.txt"
big.write_text("x" * 1000, encoding="utf-8")
try:
with pytest.raises(ServiceError) as exc:
read_file_text("TestVault", "svc_big.txt", max_bytes=10)
assert exc.value.code == "file_too_large"
finally:
big.unlink()
def test_read_raw_no_redaction(self, client):
data = read_raw_file("TestVault", "note1.md")
assert "Python" in data["raw"]
def test_read_missing_raises_not_found(self, client):
with pytest.raises(ServiceError) as exc:
read_raw_file("TestVault", "missing.md")
assert exc.value.code == "not_found"
def test_traversal_is_rejected(self, client):
with pytest.raises(ServiceError) as exc:
read_raw_file("TestVault", "../../etc/passwd")
assert exc.value.code == "path_outside_vault"
# ═══════════════════════════════════════════════════════════════════
# Search
# ═══════════════════════════════════════════════════════════════════
class TestSearchService:
def test_pagination(self, client):
data = search_vaults("Python", limit=1, offset=0)
assert data["limit"] == 1
assert len(data["results"]) <= 1
assert data["total"] >= 1
def test_returns_api_shape(self, client):
data = search_vaults("Python")
assert set(data) >= {"query", "vault_filter", "tag_filter", "count", "total", "offset", "limit", "results"}
def test_tags(self, client):
tags = list_tags("TestVault")
assert "python" in tags
# ═══════════════════════════════════════════════════════════════════
# Service → Tool error mapping
# ═══════════════════════════════════════════════════════════════════
class TestServiceErrorMapping:
def test_not_found_maps_to_tool_not_found(self, client):
from backend.tools.api import ToolContext, ToolNotFoundError, call_tool
ctx = ToolContext(user=ADMIN, audit_enabled=False)
with pytest.raises(ToolNotFoundError):
call_tool("read_file", ctx, {"vault": "TestVault", "path": "nope.md"})
def test_file_too_large_preserves_code(self, client, monkeypatch):
from backend.indexer import get_vault_data
from backend.tools import service as service_mod
from backend.tools.api import ToolContext, ToolError, call_tool
monkeypatch.setattr(service_mod, "TOOL_MAX_READ_BYTES", 10)
root = Path(get_vault_data("TestVault")["path"])
big = root / "svc_tool_big.txt"
big.write_text("x" * 1000, encoding="utf-8")
try:
ctx = ToolContext(user=ADMIN, audit_enabled=False)
with pytest.raises(ToolError) as exc:
call_tool("read_file", ctx, {"vault": "TestVault", "path": "svc_tool_big.txt"})
assert exc.value.code == "file_too_large"
finally:
big.unlink()