Picarones / tests /web /test_public_mode_hot_swap.py
Claude
feat(sprint-F)!: web/ → interfaces/web/_legacy/ (Phase 9)
f53c0aa unverified
raw
history blame
4.06 kB
"""Tests Sprint A5 — bascule à chaud du mode public (M-13).
Le mode public est piloté par la variable d'environnement
``PICARONES_PUBLIC_MODE``. ``picarones.web.security.is_public_mode()``
la lit à **chaque appel** plutôt qu'au démarrage, ce qui permet à un
opérateur de basculer le mode sans redémarrer le serveur.
Cette suite vérifie que la bascule à chaud fonctionne :
1. Au démarrage en mode dev, ``assert_engines_allowed`` accepte les
moteurs cloud ; après ``setenv PICARONES_PUBLIC_MODE=1``, le même
appel les refuse.
2. Inversement : démarrage public → bascule dev → cloud autorisé.
3. Aucun cache global ne mémorise l'ancienne valeur.
"""
from __future__ import annotations
import pytest
from picarones.interfaces.web._legacy.security import (
assert_engines_allowed,
assert_llm_provider_allowed,
is_public_mode,
)
def test_public_mode_off_allows_cloud_engines(monkeypatch) -> None:
"""Mode dev : moteurs cloud autorisés sans réserve."""
monkeypatch.delenv("PICARONES_PUBLIC_MODE", raising=False)
assert is_public_mode() is False
# Ne doit pas lever
assert_engines_allowed(["mistral_ocr", "google_vision", "azure_doc_intel"])
def test_public_mode_on_blocks_cloud_engines(monkeypatch) -> None:
"""Mode public : moteurs cloud refusés (clés mutualisées côté serveur)."""
monkeypatch.setenv("PICARONES_PUBLIC_MODE", "1")
assert is_public_mode() is True
with pytest.raises(PermissionError):
assert_engines_allowed(["mistral_ocr"])
def test_hot_swap_dev_to_public(monkeypatch) -> None:
"""Bascule à chaud dev → public. Le même appel passe puis échoue
sans redémarrage du process."""
monkeypatch.delenv("PICARONES_PUBLIC_MODE", raising=False)
# Phase 1 : dev → cloud autorisé
assert_engines_allowed(["mistral_ocr"]) # ne lève pas
# Phase 2 : bascule à chaud
monkeypatch.setenv("PICARONES_PUBLIC_MODE", "1")
with pytest.raises(PermissionError):
assert_engines_allowed(["mistral_ocr"])
def test_hot_swap_public_to_dev(monkeypatch) -> None:
"""Bascule inverse : public → dev. Le même cloud refusé puis accepté."""
monkeypatch.setenv("PICARONES_PUBLIC_MODE", "1")
with pytest.raises(PermissionError):
assert_engines_allowed(["google_vision"])
monkeypatch.delenv("PICARONES_PUBLIC_MODE", raising=False)
assert_engines_allowed(["google_vision"]) # ne lève pas
def test_hot_swap_llm_provider_check(monkeypatch) -> None:
"""``assert_llm_provider_allowed`` doit aussi être sensible à la
bascule à chaud."""
monkeypatch.delenv("PICARONES_PUBLIC_MODE", raising=False)
assert_llm_provider_allowed("openai") # dev : ok
monkeypatch.setenv("PICARONES_PUBLIC_MODE", "1")
with pytest.raises(PermissionError):
assert_llm_provider_allowed("openai")
def test_engines_allowed_partial_block(monkeypatch) -> None:
"""En mode public, si la liste contient cloud + local, l'erreur
doit identifier précisément quel(s) moteur(s) sont refusés."""
monkeypatch.setenv("PICARONES_PUBLIC_MODE", "1")
with pytest.raises(PermissionError) as exc_info:
assert_engines_allowed(["tesseract", "mistral_ocr", "pero_ocr"])
msg = str(exc_info.value)
# Le message doit mentionner le moteur cloud refusé (pour un
# diagnostic clair côté frontend).
assert "mistral_ocr" in msg
def test_empty_engine_list_passes_in_both_modes(monkeypatch) -> None:
"""Une liste vide ne doit jamais lever (même en mode public)."""
monkeypatch.delenv("PICARONES_PUBLIC_MODE", raising=False)
assert_engines_allowed([])
monkeypatch.setenv("PICARONES_PUBLIC_MODE", "1")
assert_engines_allowed([])
def test_local_engines_always_allowed(monkeypatch) -> None:
"""Tesseract / Pero (locaux) ne doivent jamais être bloqués."""
monkeypatch.setenv("PICARONES_PUBLIC_MODE", "1")
assert_engines_allowed(["tesseract"])
assert_engines_allowed(["pero_ocr"])
assert_engines_allowed(["tesseract", "pero_ocr"])