Rend une instance exposee sur Internet exploitable sans la transformer en infrastructure : le produit reste local-first, sans compte et sans telemetrie. Protection du service : limitation de debit en memoire par client et par famille d'endpoint (health et ready jamais limites), interrupteur SHARING_ENABLED, capacite maximale de partages, taille propre aux partages. X-Forwarded-For n'est cru que si TRUST_PROXY_HEADERS l'autorise, et on lit alors la derniere valeur, celle ajoutee par le proxy. Expiration facultative des partages (24 h a 90 jours, ou jamais) : un lien expire repond 410 et la page publique le dit, sans autre detail. Le proprietaire peut prolonger ; aucune purge automatique. Protection des liens non repertories : robots.txt, X-Robots-Tag et meta robots sur /s/ et /api/, Referrer-Policy no-referrer sur tout le site, redaction des jetons dans les journaux d'acces, share_token porte a 128 bits (les anciens restent valides). Schema gere par Alembic, applique au demarrage. Une base creee avant Alembic est estampillee puis migree : verifie sur la base reelle de developpement, 4 partages inchanges et expires_at NULL. Administration par CLI locale au conteneur (stats, liste, suppression, purge, sauvegarde a chaud via l'API SQLite, restauration prudente) : aucune interface /admin exposee. Le jeton de gestion peut enfin etre exporte dans un fichier .openmind-share puis reimporte depuis un autre navigateur, apres verification aupres du serveur. Il n'est toujours jamais ajoute a un export .mmap. Ajoute une page /privacy dont le texte est centralise, et corrige un bug trouve en observant les logs : Alembic, lance dans le processus applicatif, reappliquait la configuration de logs d'alembic.ini et desactivait les loggers existants, ce qui faisait disparaitre les journaux d'acces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
"""Fixtures de test : base SQLite temporaire, isolee du volume de production."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
from collections.abc import Iterator
|
|
|
|
import pytest
|
|
|
|
# Doit etre defini AVANT le premier import de l'application.
|
|
_TMP_DIR = tempfile.mkdtemp(prefix="openmind-tests-")
|
|
os.environ["DATABASE_URL"] = f"sqlite:///{_TMP_DIR}/test.db"
|
|
os.environ["STATIC_DIR"] = f"{_TMP_DIR}/static"
|
|
os.environ["APP_NAME"] = "OpenMind"
|
|
os.environ["MAX_NODES"] = "5000"
|
|
os.environ["MAX_MAP_SIZE_MB"] = "10"
|
|
# Les limites de debit sont desactivees par defaut dans les tests : chaque test
|
|
# qui les concerne les active explicitement via la fixture `configure`.
|
|
os.environ["SHARE_CREATE_RATE_LIMIT"] = "0"
|
|
os.environ["SHARE_READ_RATE_LIMIT"] = "0"
|
|
os.environ["SHARE_WRITE_RATE_LIMIT"] = "0"
|
|
|
|
from fastapi.testclient import TestClient # noqa: E402
|
|
from sqlalchemy import text # noqa: E402
|
|
|
|
from app.config import get_settings # noqa: E402
|
|
from app.db.base import Base # noqa: E402
|
|
from app.db.migrations import run_migrations # noqa: E402
|
|
from app.db.session import engine # noqa: E402
|
|
from app.main import app # noqa: E402
|
|
from app.services.rate_limit import limiter # noqa: E402
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _fresh_database() -> Iterator[None]:
|
|
"""Base neuve avant chaque test, construite par les vraies migrations.
|
|
|
|
On passe deliberement par Alembic plutot que par `create_all` : le schema
|
|
teste est exactement celui qu'obtiendra une installation reelle.
|
|
"""
|
|
get_settings.cache_clear()
|
|
Base.metadata.drop_all(bind=engine)
|
|
with engine.begin() as connection:
|
|
connection.execute(text("DROP TABLE IF EXISTS alembic_version"))
|
|
run_migrations(engine)
|
|
|
|
# Les compteurs de debit vivent dans le processus : sans cette remise a
|
|
# zero, les tests se limiteraient les uns les autres.
|
|
limiter.reset()
|
|
yield
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.fixture()
|
|
def configure(monkeypatch: pytest.MonkeyPatch): # type: ignore[no-untyped-def]
|
|
"""Surcharge des reglages le temps d'un test.
|
|
|
|
Les valeurs sont posees dans l'environnement puis le cache de `get_settings`
|
|
est vide, exactement comme au demarrage de l'application.
|
|
"""
|
|
|
|
def _apply(**values: object) -> None:
|
|
for key, value in values.items():
|
|
monkeypatch.setenv(key.upper(), str(value))
|
|
get_settings.cache_clear()
|
|
|
|
yield _apply
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.fixture()
|
|
def client() -> Iterator[TestClient]:
|
|
with TestClient(app) as test_client:
|
|
yield test_client
|
|
|
|
|
|
def build_map(node_count: int = 3, name: str = "Carte de test") -> dict:
|
|
"""Construit un document carte minimal mais valide."""
|
|
nodes = [
|
|
{
|
|
"id": "root",
|
|
"type": "root",
|
|
"text": name,
|
|
"position": {"x": 0, "y": 0},
|
|
"parentId": None,
|
|
}
|
|
]
|
|
edges = []
|
|
for index in range(1, node_count):
|
|
node_id = f"n{index}"
|
|
nodes.append(
|
|
{
|
|
"id": node_id,
|
|
"type": "standard",
|
|
"text": f"Noeud {index}",
|
|
"position": {"x": 200, "y": index * 80},
|
|
"parentId": "root",
|
|
}
|
|
)
|
|
edges.append({"id": f"e-root-{node_id}", "source": "root", "target": node_id})
|
|
|
|
return {
|
|
"version": 1,
|
|
"id": "11111111-1111-4111-8111-111111111111",
|
|
"name": name,
|
|
"createdAt": "2026-01-01T00:00:00.000Z",
|
|
"updatedAt": "2026-01-01T00:00:00.000Z",
|
|
"nodes": nodes,
|
|
"edges": edges,
|
|
"viewport": {"x": 0, "y": 0, "zoom": 1},
|
|
"settings": {"theme": "default", "layout": "mindmap-right", "branchStyle": "bezier"},
|
|
}
|
|
|
|
|
|
@pytest.fixture()
|
|
def created_share(client: TestClient) -> dict:
|
|
response = client.post("/api/shares", json={"title": "Carte de test", "map": build_map()})
|
|
assert response.status_code == 201, response.text
|
|
return response.json()
|