forge-mvc-testing 1.0.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forge_mvc_testing/__init__.py +15 -0
- forge_mvc_testing/fake_request.py +128 -0
- forge_mvc_testing/plugin.py +108 -0
- forge_mvc_testing/py.typed +0 -0
- forge_mvc_testing-1.0.0rc2.dist-info/METADATA +72 -0
- forge_mvc_testing-1.0.0rc2.dist-info/RECORD +10 -0
- forge_mvc_testing-1.0.0rc2.dist-info/WHEEL +5 -0
- forge_mvc_testing-1.0.0rc2.dist-info/entry_points.txt +2 -0
- forge_mvc_testing-1.0.0rc2.dist-info/licenses/LICENSE +36 -0
- forge_mvc_testing-1.0.0rc2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""forge-mvc-testing — infrastructure de test partagée pour Forge (dev only).
|
|
2
|
+
|
|
3
|
+
Fournit `FakeRequest` et, via un plugin pytest (point d'entrée `pytest11`,
|
|
4
|
+
voir `forge_mvc_testing.plugin`), les fixtures partagées : configuration du
|
|
5
|
+
noyau, nettoyage entre tests, et `fake_request`.
|
|
6
|
+
|
|
7
|
+
Ce paquet n'est JAMAIS une dépendance runtime ; il n'est installé qu'en
|
|
8
|
+
développement (ADR-041).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from forge_mvc_testing.fake_request import FakeRequest
|
|
13
|
+
|
|
14
|
+
__all__ = ["FakeRequest"]
|
|
15
|
+
__version__ = "1.0.0rc2"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Requête HTTP simulée pour les tests unitaires de contrôleurs Forge.
|
|
2
|
+
|
|
3
|
+
Portée depuis `tests/fake_request.py` vers le paquet de test-support
|
|
4
|
+
`forge-mvc-testing` (ADR-041), pour que les tests de chaque paquet opt-in y
|
|
5
|
+
accèdent sans dépendre du `tests/` racine.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FakeRequest:
|
|
10
|
+
"""
|
|
11
|
+
Requête HTTP simulée pour les tests unitaires de contrôleurs.
|
|
12
|
+
|
|
13
|
+
Usage :
|
|
14
|
+
req = FakeRequest("GET", "/clients")
|
|
15
|
+
req = FakeRequest("POST", "/clients", body={"Nom": "Dupont"})
|
|
16
|
+
req = FakeRequest("POST", "/api/sync", json_body={"ids": [1, 2]})
|
|
17
|
+
req = FakeRequest("GET", "/clients", session_id="abc123")
|
|
18
|
+
|
|
19
|
+
La fixture `fake_request` (plugin pytest) retourne la classe directement.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, method="GET", path="/", *,
|
|
23
|
+
body=None, json_body=None, params=None,
|
|
24
|
+
session_id=None, ip="127.0.0.1", headers=None, files=None):
|
|
25
|
+
self.original_method = method.upper()
|
|
26
|
+
self.method = self.original_method
|
|
27
|
+
self.path = path
|
|
28
|
+
self.ip = ip
|
|
29
|
+
self.route_params = {}
|
|
30
|
+
self.params = {k: [v] for k, v in (params or {}).items()}
|
|
31
|
+
|
|
32
|
+
# body formulaire (format parse_qs : valeurs en liste)
|
|
33
|
+
self.body = {k: [v] for k, v in (body or {}).items()}
|
|
34
|
+
self.json_body = json_body or {}
|
|
35
|
+
self.files = files or {}
|
|
36
|
+
self._apply_method_override()
|
|
37
|
+
|
|
38
|
+
# session injectée via le cookie
|
|
39
|
+
self.headers = _FakeHeaders(session_id, headers=headers)
|
|
40
|
+
|
|
41
|
+
def _apply_method_override(self):
|
|
42
|
+
if self.original_method != "POST":
|
|
43
|
+
return
|
|
44
|
+
value = self.body.get("_method", [None])
|
|
45
|
+
override = value[0] if isinstance(value, list) and value else value
|
|
46
|
+
if override and str(override).upper() in {"PUT", "PATCH", "DELETE"}:
|
|
47
|
+
self.method = str(override).upper()
|
|
48
|
+
|
|
49
|
+
# ── Convention d'inspection — miroir des accesseurs de `Request` ────────
|
|
50
|
+
#
|
|
51
|
+
# Ajouté par STARTER-BONJOUR-FORGE-001 / API-INSPECTABLE-OBJECTS-CONVENTION-001
|
|
52
|
+
# pour que `FakeRequest` se comporte comme `core.http.request.Request`
|
|
53
|
+
# dans les tests qui exercent les contrôleurs typés.
|
|
54
|
+
|
|
55
|
+
def query(self, key, default=None):
|
|
56
|
+
values = self.params.get(key)
|
|
57
|
+
if not values:
|
|
58
|
+
return default
|
|
59
|
+
return values[0]
|
|
60
|
+
|
|
61
|
+
def header(self, name, default=None):
|
|
62
|
+
return self.headers.get(name, default)
|
|
63
|
+
|
|
64
|
+
def form(self, key, default=None):
|
|
65
|
+
values = self.body.get(key)
|
|
66
|
+
if not values:
|
|
67
|
+
return default
|
|
68
|
+
if isinstance(values, list):
|
|
69
|
+
return values[0] if values else default
|
|
70
|
+
return values
|
|
71
|
+
|
|
72
|
+
def json(self, key, default=None):
|
|
73
|
+
body = self.json_body
|
|
74
|
+
if not isinstance(body, dict):
|
|
75
|
+
return default
|
|
76
|
+
return body.get(key, default)
|
|
77
|
+
|
|
78
|
+
def file(self, key, default=None):
|
|
79
|
+
return self.files.get(key, default)
|
|
80
|
+
|
|
81
|
+
def route(self, key, default=None):
|
|
82
|
+
return self.route_params.get(key, default)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def data(self):
|
|
86
|
+
"""Mini-version de `Request.data` pour les tests de contrôleurs.
|
|
87
|
+
|
|
88
|
+
Reproduit les clés canoniques (method, path, ip, params,
|
|
89
|
+
route_params, headers, body, json_body, files) mais sans le
|
|
90
|
+
masquage avancé (les tests ne devraient pas avoir besoin de
|
|
91
|
+
passer par FakeRequest pour vérifier le masquage — `Request`
|
|
92
|
+
réel est utilisé pour ça)."""
|
|
93
|
+
headers_view = {}
|
|
94
|
+
for key in list(getattr(self.headers, "_headers", {})):
|
|
95
|
+
headers_view[key] = self.headers.get(key)
|
|
96
|
+
return {
|
|
97
|
+
"method": self.method,
|
|
98
|
+
"original_method": self.original_method,
|
|
99
|
+
"path": self.path,
|
|
100
|
+
"ip": self.ip,
|
|
101
|
+
"params": dict(self.params),
|
|
102
|
+
"route_params": dict(self.route_params),
|
|
103
|
+
"headers": headers_view,
|
|
104
|
+
"body": dict(self.body),
|
|
105
|
+
"json_body": dict(self.json_body) if isinstance(self.json_body, dict) else self.json_body,
|
|
106
|
+
"files": {k: {"filename": getattr(f, "filename", None),
|
|
107
|
+
"size": getattr(f, "size", None),
|
|
108
|
+
"content_type": getattr(f, "content_type", None)}
|
|
109
|
+
for k, f in self.files.items()},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _FakeHeaders:
|
|
114
|
+
"""Simule http.client.HTTPMessage pour get("Cookie") et get("Content-Type")."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, session_id=None, headers=None):
|
|
117
|
+
self._session_id = session_id
|
|
118
|
+
self._headers = dict(headers or {})
|
|
119
|
+
self._headers_lower = {key.lower(): value for key, value in self._headers.items()}
|
|
120
|
+
|
|
121
|
+
def get(self, name, default: str | None = ""):
|
|
122
|
+
if name in self._headers:
|
|
123
|
+
return self._headers[name]
|
|
124
|
+
if name.lower() in self._headers_lower:
|
|
125
|
+
return self._headers_lower[name.lower()]
|
|
126
|
+
if name == "Cookie" and self._session_id:
|
|
127
|
+
return f"__Host-session_id={self._session_id}"
|
|
128
|
+
return default
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Plugin pytest de Forge (test-support, ADR-041).
|
|
2
|
+
|
|
3
|
+
Chargé automatiquement via le point d'entrée `pytest11` dès que
|
|
4
|
+
`forge-mvc-testing` est installé dans l'environnement de test. Fournit les
|
|
5
|
+
fixtures partagées autrefois dans `tests/conftest.py` : configuration du noyau,
|
|
6
|
+
nettoyage entre tests, et `fake_request`.
|
|
7
|
+
|
|
8
|
+
Réservé au développement : ce paquet n'est jamais une dépendance runtime, donc
|
|
9
|
+
ces fixtures (notamment l'autouse `configure_forge_kernel`) n'affectent pas les
|
|
10
|
+
suites de tests des utilisateurs de Forge.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
import core.forge as forge
|
|
17
|
+
|
|
18
|
+
from forge_mvc_testing.fake_request import FakeRequest
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
22
|
+
def configure_forge_kernel(tmp_path_factory):
|
|
23
|
+
"""Configure le noyau Forge pour tous les tests — vues et SQL dans tmp_path."""
|
|
24
|
+
views_dir = tmp_path_factory.mktemp("views")
|
|
25
|
+
sql_dir = tmp_path_factory.mktemp("sql")
|
|
26
|
+
# ADR-060 : le cœur ne porte plus de config de connexion BDD ; un backend la
|
|
27
|
+
# lit dans l'environnement. Les tests qui touchent la base posent eux-mêmes
|
|
28
|
+
# les variables DB_* (voir test_sqlite_adapter_001).
|
|
29
|
+
forge.configure(
|
|
30
|
+
app_name="TestForge",
|
|
31
|
+
app_env="dev",
|
|
32
|
+
views_dir=str(views_dir),
|
|
33
|
+
sql_dir=str(sql_dir),
|
|
34
|
+
)
|
|
35
|
+
# ADR-032 : UPLOAD_ROOT est lu depuis l'environnement par forge-mvc-files.
|
|
36
|
+
# Filet de sécurité : un défaut pointant vers un tmp, pour qu'aucun test ne
|
|
37
|
+
# puisse écrire dans le storage réel du dépôt même sans isolation propre.
|
|
38
|
+
mp = pytest.MonkeyPatch()
|
|
39
|
+
mp.setenv("UPLOAD_ROOT", str(tmp_path_factory.mktemp("uploads")))
|
|
40
|
+
yield
|
|
41
|
+
mp.undo()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture(autouse=True)
|
|
45
|
+
def clear_sessions():
|
|
46
|
+
"""Vide le store de sessions entre chaque test."""
|
|
47
|
+
from core.sessions.manager import get_session_store
|
|
48
|
+
|
|
49
|
+
def _purge() -> None:
|
|
50
|
+
# `purge_all` est une capacité réservée aux tests, portée par
|
|
51
|
+
# MemorySessionStore et hors du protocole SessionStore : on l'appelle
|
|
52
|
+
# uniquement si le store configuré l'expose.
|
|
53
|
+
store = get_session_store()
|
|
54
|
+
purge = getattr(store, "purge_all", None)
|
|
55
|
+
if callable(purge):
|
|
56
|
+
purge()
|
|
57
|
+
|
|
58
|
+
_purge()
|
|
59
|
+
yield
|
|
60
|
+
_purge()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.fixture(autouse=True)
|
|
64
|
+
def clear_rate_limits():
|
|
65
|
+
"""Vide les compteurs de tentatives, anti-replay et échecs d'audit entre tests.
|
|
66
|
+
|
|
67
|
+
Compatible core-only : si `forge_mvc_mfa` n'est pas installé, le purge
|
|
68
|
+
anti-replay devient un no-op.
|
|
69
|
+
"""
|
|
70
|
+
from core.auth.rate_limit import purge_all_attempts
|
|
71
|
+
from core.auth.audit import reset_audit_failure_count
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
from forge_mvc_mfa.totp_replay import purge_all as _purge_replay
|
|
75
|
+
except ImportError:
|
|
76
|
+
def _purge_replay(): # type: ignore[misc]
|
|
77
|
+
"""No-op : forge-mvc-mfa n'est pas installé."""
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
purge_all_attempts()
|
|
81
|
+
_purge_replay()
|
|
82
|
+
reset_audit_failure_count()
|
|
83
|
+
yield
|
|
84
|
+
purge_all_attempts()
|
|
85
|
+
_purge_replay()
|
|
86
|
+
reset_audit_failure_count()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.fixture(autouse=True)
|
|
90
|
+
def clear_upload_rate_limits():
|
|
91
|
+
"""Vide le compteur d'uploads entre chaque test.
|
|
92
|
+
|
|
93
|
+
Le rate-limit d'upload vit dans `forge_mvc_files` (ADR-019) ; absent → no-op.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
from forge_mvc_files import rate_limit as _rl
|
|
97
|
+
except ImportError:
|
|
98
|
+
yield
|
|
99
|
+
return
|
|
100
|
+
_rl._compteurs.clear()
|
|
101
|
+
yield
|
|
102
|
+
_rl._compteurs.clear()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.fixture
|
|
106
|
+
def fake_request():
|
|
107
|
+
"""Retourne la classe `FakeRequest` pour construire des requêtes simulées."""
|
|
108
|
+
return FakeRequest
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forge-mvc-testing
|
|
3
|
+
Version: 1.0.0rc2
|
|
4
|
+
Summary: Forge : infrastructure de test partagée (FakeRequest + plugin pytest) pour tester Forge et ses opt-ins.
|
|
5
|
+
Author: Roger Lequette
|
|
6
|
+
License-Expression: LicenseRef-Forge-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/caucrogeGit/Forge
|
|
8
|
+
Project-URL: Repository, https://github.com/caucrogeGit/Forge
|
|
9
|
+
Keywords: python,mvc,forge,testing,pytest
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Framework :: Pytest
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: forge-mvc<2,>=1.0.0rc2
|
|
20
|
+
Requires-Dist: pytest>=7
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# forge-mvc-testing
|
|
24
|
+
|
|
25
|
+
Infrastructure de test partagée pour le framework Forge (ADR-041).
|
|
26
|
+
|
|
27
|
+
Ce paquet réservé au développement fournit de quoi tester Forge, ses opt-ins
|
|
28
|
+
officiels, et les opt-ins ou applications développés par des tiers :
|
|
29
|
+
|
|
30
|
+
- `FakeRequest` : une fausse requête HTTP pour tester contrôleurs et helpers sans serveur ;
|
|
31
|
+
- un plugin pytest (chargé automatiquement à l'installation) dont les fixtures
|
|
32
|
+
autouse configurent le noyau Forge et réinitialisent l'état partagé entre
|
|
33
|
+
tests (compteurs de tentatives, anti-replay, échecs d'audit).
|
|
34
|
+
|
|
35
|
+
## Statut
|
|
36
|
+
|
|
37
|
+
`forge-mvc-testing` est en `Development Status :: 4 - Beta`, aligné sur la
|
|
38
|
+
version du cœur. C'est un outil de **développement** : une application ne
|
|
39
|
+
l'importe jamais à l'exécution.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install --pre forge-mvc-testing
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
En général comme dépendance de développement, par exemple dans
|
|
48
|
+
`requirements-dev.txt` :
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
forge-mvc-testing
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Utilisation
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from forge_mvc_testing import FakeRequest
|
|
58
|
+
|
|
59
|
+
def test_mon_controleur():
|
|
60
|
+
request = FakeRequest(method="GET", query={"page": "2"})
|
|
61
|
+
response = MonControleur.index(request)
|
|
62
|
+
assert response.status == 200
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Le plugin pytest s'active dès que le paquet est installé : ses fixtures autouse
|
|
66
|
+
préparent le noyau Forge et nettoient l'état partagé avant chaque test, sans
|
|
67
|
+
configuration.
|
|
68
|
+
|
|
69
|
+
## Licence
|
|
70
|
+
|
|
71
|
+
Distribué sous licence propriétaire Forge (`LicenseRef-Forge-Proprietary`),
|
|
72
|
+
comme le reste du framework. Voir `LICENSE`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
forge_mvc_testing/__init__.py,sha256=QQa7_b8Uk3104OhXsDtPD3czYcBpK75yTQjS9QNVSos,532
|
|
2
|
+
forge_mvc_testing/fake_request.py,sha256=0T8p7HtGV4cAspwCXMpkJ1U6ZzIww5xBDWmF8esUpoI,5000
|
|
3
|
+
forge_mvc_testing/plugin.py,sha256=hmc8IFr2kj_-m2Mk8Pzw9Fi40BWu6UmwRec_1Vvjprc,3558
|
|
4
|
+
forge_mvc_testing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
forge_mvc_testing-1.0.0rc2.dist-info/licenses/LICENSE,sha256=kT7JQI9x59JeUwpyiFmqyM6hwLhDzdPS0Wczb5iY3G0,1599
|
|
6
|
+
forge_mvc_testing-1.0.0rc2.dist-info/METADATA,sha256=d4euMDkgeca6Qydk4f41wtMTm2WPdg8mQZdO4MUWNPk,2361
|
|
7
|
+
forge_mvc_testing-1.0.0rc2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
8
|
+
forge_mvc_testing-1.0.0rc2.dist-info/entry_points.txt,sha256=aTJtxAa3vA1xJ12PjiSEtjKta0HmRIO-uQHbBXyIypk,56
|
|
9
|
+
forge_mvc_testing-1.0.0rc2.dist-info/top_level.txt,sha256=aSejGBDuvxByKSpU7NM5UoElp8zaglVNI1HBRURevo8,18
|
|
10
|
+
forge_mvc_testing-1.0.0rc2.dist-info/RECORD,,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Forge — Licence propriétaire / source disponible
|
|
2
|
+
|
|
3
|
+
Copyright (c) Roger Lequette.
|
|
4
|
+
Tous droits réservés.
|
|
5
|
+
|
|
6
|
+
Le code source de Forge est mis à disposition uniquement pour lecture, étude,
|
|
7
|
+
évaluation et usage éducatif personnel.
|
|
8
|
+
|
|
9
|
+
Sauf autorisation écrite préalable de Roger Lequette, il est interdit de :
|
|
10
|
+
|
|
11
|
+
- utiliser Forge dans un cadre professionnel, commercial ou institutionnel ;
|
|
12
|
+
- utiliser Forge pour une prestation client ;
|
|
13
|
+
- intégrer Forge dans un produit, service, SaaS, application vendue ou solution
|
|
14
|
+
déployée pour un tiers ;
|
|
15
|
+
- redistribuer Forge, modifié ou non ;
|
|
16
|
+
- publier une version modifiée de Forge ;
|
|
17
|
+
- vendre, louer, sous-licencier ou exploiter commercialement Forge ;
|
|
18
|
+
- supprimer ou modifier les mentions de copyright et de licence.
|
|
19
|
+
|
|
20
|
+
Les usages autorisés sans autorisation écrite préalable sont limités à :
|
|
21
|
+
|
|
22
|
+
- lire le code ;
|
|
23
|
+
- étudier son fonctionnement ;
|
|
24
|
+
- tester Forge à titre personnel ;
|
|
25
|
+
- l'utiliser dans un cadre éducatif personnel ou pédagogique non commercial ;
|
|
26
|
+
- évaluer le framework avant une éventuelle demande d'autorisation.
|
|
27
|
+
|
|
28
|
+
Toute utilisation non explicitement autorisée par cette licence nécessite une
|
|
29
|
+
autorisation écrite préalable de Roger Lequette.
|
|
30
|
+
|
|
31
|
+
Cette licence pourra évoluer ultérieurement. Toute version publiée de Forge
|
|
32
|
+
reste soumise à la licence présente dans son dépôt au moment de sa récupération.
|
|
33
|
+
|
|
34
|
+
LE LOGICIEL EST FOURNI « TEL QUEL », SANS GARANTIE D'AUCUNE SORTE, EXPRESSE OU
|
|
35
|
+
IMPLICITE. EN AUCUN CAS LE DÉTENTEUR DU COPYRIGHT NE POURRA ÊTRE TENU
|
|
36
|
+
RESPONSABLE DE TOUT DOMMAGE DÉCOULANT DE L'UTILISATION DE CE LOGICIEL.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
forge_mvc_testing
|