socialseed-e2e 0.1.0__py3-none-any.whl → 0.1.2__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.
- socialseed_e2e/__init__.py +184 -20
- socialseed_e2e/__version__.py +2 -2
- socialseed_e2e/cli.py +353 -190
- socialseed_e2e/core/base_page.py +368 -49
- socialseed_e2e/core/config_loader.py +15 -3
- socialseed_e2e/core/headers.py +11 -4
- socialseed_e2e/core/loaders.py +6 -4
- socialseed_e2e/core/test_orchestrator.py +2 -0
- socialseed_e2e/core/test_runner.py +487 -0
- socialseed_e2e/templates/agent_docs/AGENT_GUIDE.md.template +412 -0
- socialseed_e2e/templates/agent_docs/EXAMPLE_TEST.md.template +152 -0
- socialseed_e2e/templates/agent_docs/FRAMEWORK_CONTEXT.md.template +55 -0
- socialseed_e2e/templates/agent_docs/WORKFLOW_GENERATION.md.template +182 -0
- socialseed_e2e/templates/data_schema.py.template +111 -70
- socialseed_e2e/templates/e2e.conf.template +19 -0
- socialseed_e2e/templates/service_page.py.template +82 -27
- socialseed_e2e/templates/test_module.py.template +21 -7
- socialseed_e2e/templates/verify_installation.py +192 -0
- socialseed_e2e/utils/__init__.py +29 -0
- socialseed_e2e/utils/ai_generator.py +463 -0
- socialseed_e2e/utils/pydantic_helpers.py +392 -0
- socialseed_e2e/utils/state_management.py +312 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/METADATA +64 -27
- socialseed_e2e-0.1.2.dist-info/RECORD +38 -0
- socialseed_e2e-0.1.0.dist-info/RECORD +0 -29
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/WHEEL +0 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/entry_points.txt +0 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# 📘 Guía para Agentes de IA - SocialSeed E2E Framework
|
|
2
|
+
|
|
3
|
+
> **Versión 2.0 - Actualizado para evitar errores comunes**
|
|
4
|
+
|
|
5
|
+
Esta guía te permite generar tests E2E funcionales sin errores de importación, serialización o configuración.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🚨 REGLAS DE ORO (Lee esto primero)
|
|
10
|
+
|
|
11
|
+
### 1. **Imports SIEMPRE Absolutos**
|
|
12
|
+
```python
|
|
13
|
+
# ❌ NUNCA uses imports relativos
|
|
14
|
+
from ..data_schema import RegisterRequest # PROHIBIDO
|
|
15
|
+
|
|
16
|
+
# ✅ SIEMPRE uses imports absolutos
|
|
17
|
+
from services.auth_service.data_schema import RegisterRequest
|
|
18
|
+
from services.auth_service.auth_page import AuthServicePage
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. **Serialización con Aliases**
|
|
22
|
+
```python
|
|
23
|
+
# ❌ NUNCA sin by_alias=True (envía refresh_token en lugar de refreshToken)
|
|
24
|
+
data = request.model_dump()
|
|
25
|
+
|
|
26
|
+
# ✅ SIEMPRE con by_alias=True (envía refreshToken como espera Java)
|
|
27
|
+
data = request.model_dump(by_alias=True)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 3. **Modelos Pydantic con Config**
|
|
31
|
+
```python
|
|
32
|
+
class RefreshTokenRequest(BaseModel):
|
|
33
|
+
# SIEMPRE incluye esto
|
|
34
|
+
model_config = {"populate_by_name": True}
|
|
35
|
+
|
|
36
|
+
# Campos compuestos SIEMPRE con alias camelCase
|
|
37
|
+
refresh_token: str = Field(
|
|
38
|
+
...,
|
|
39
|
+
alias="refreshToken",
|
|
40
|
+
serialization_alias="refreshToken"
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 4. **Manejo Manual de Headers**
|
|
45
|
+
```python
|
|
46
|
+
# ❌ NO existe update_headers()
|
|
47
|
+
self.update_headers({"Authorization": f"Bearer {token}"})
|
|
48
|
+
|
|
49
|
+
# ✅ Implementa _get_headers() manualmente
|
|
50
|
+
def _get_headers(self, extra=None):
|
|
51
|
+
headers = {"Content-Type": "application/json"}
|
|
52
|
+
if self.access_token:
|
|
53
|
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
54
|
+
return headers
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 5. **Nombres de Métodos sin Conflicto**
|
|
58
|
+
```python
|
|
59
|
+
# ❌ Conflicto: atributo refresh_token vs método refresh_token()
|
|
60
|
+
self.refresh_token = token # Atributo
|
|
61
|
+
|
|
62
|
+
# ✅ Usa prefijo do_ para métodos
|
|
63
|
+
self.refresh_token = token # Atributo
|
|
64
|
+
self.do_refresh_token() # Método
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 📦 Dependencias Requeridas
|
|
70
|
+
|
|
71
|
+
El archivo `requirements.txt` debe incluir:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
pydantic>=2.0.0
|
|
75
|
+
email-validator>=2.0.0 # REQUERIDO para EmailStr
|
|
76
|
+
dnspython>=2.0.0 # Para validación de email
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Si falta email-validator:**
|
|
80
|
+
```
|
|
81
|
+
ImportError: email-validator is not installed
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Solución:**
|
|
85
|
+
```bash
|
|
86
|
+
pip install -r requirements.txt
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 🎯 Templates Actualizados
|
|
92
|
+
|
|
93
|
+
### data_schema.py - Estructura Base
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
"""Data schema for <service> API.
|
|
97
|
+
|
|
98
|
+
⚠️ IMPORTANTE: Todos los campos compuestos usan camelCase aliases.
|
|
99
|
+
"""
|
|
100
|
+
from pydantic import BaseModel, Field, EmailStr
|
|
101
|
+
from typing import Optional
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class LoginRequest(BaseModel):
|
|
105
|
+
"""Login request."""
|
|
106
|
+
model_config = {"populate_by_name": True}
|
|
107
|
+
|
|
108
|
+
email: EmailStr
|
|
109
|
+
password: str
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class RefreshTokenRequest(BaseModel):
|
|
113
|
+
"""Refresh token request."""
|
|
114
|
+
model_config = {"populate_by_name": True}
|
|
115
|
+
|
|
116
|
+
# ⚠️ camelCase alias requerido para backend Java
|
|
117
|
+
refresh_token: str = Field(
|
|
118
|
+
...,
|
|
119
|
+
alias="refreshToken",
|
|
120
|
+
serialization_alias="refreshToken"
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### service_page.py - Estructura Base
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
"""Page object for <service> API."""
|
|
128
|
+
from typing import Optional, Dict, Any
|
|
129
|
+
from playwright.sync_api import APIResponse
|
|
130
|
+
from socialseed_e2e.core.base_page import BasePage
|
|
131
|
+
|
|
132
|
+
from .data_schema import (
|
|
133
|
+
ENDPOINTS,
|
|
134
|
+
LoginRequest,
|
|
135
|
+
RefreshTokenRequest,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class AuthPage(BasePage):
|
|
140
|
+
"""Page for auth service."""
|
|
141
|
+
|
|
142
|
+
def __init__(self, base_url: str, **kwargs):
|
|
143
|
+
super().__init__(base_url=base_url, **kwargs)
|
|
144
|
+
self.access_token: Optional[str] = None
|
|
145
|
+
self.refresh_token: Optional[str] = None
|
|
146
|
+
|
|
147
|
+
def _get_headers(self, extra: Optional[Dict] = None) -> Dict[str, str]:
|
|
148
|
+
headers = {"Content-Type": "application/json"}
|
|
149
|
+
if self.access_token:
|
|
150
|
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
151
|
+
if extra:
|
|
152
|
+
headers.update(extra)
|
|
153
|
+
return headers
|
|
154
|
+
|
|
155
|
+
def do_login(self, request: LoginRequest) -> APIResponse:
|
|
156
|
+
"""Login and store tokens."""
|
|
157
|
+
response = self.post(
|
|
158
|
+
ENDPOINTS["login"],
|
|
159
|
+
data=request.model_dump(by_alias=True) # ✅ SIEMPRE by_alias=True
|
|
160
|
+
)
|
|
161
|
+
if response.ok:
|
|
162
|
+
data = response.json()["data"]
|
|
163
|
+
self.access_token = data.get("token")
|
|
164
|
+
self.refresh_token = data.get("refreshToken")
|
|
165
|
+
return response
|
|
166
|
+
|
|
167
|
+
def do_refresh_token(self) -> APIResponse:
|
|
168
|
+
"""Refresh access token."""
|
|
169
|
+
if not self.refresh_token:
|
|
170
|
+
raise ValueError("No refresh token")
|
|
171
|
+
|
|
172
|
+
request = RefreshTokenRequest(refresh_token=self.refresh_token)
|
|
173
|
+
response = self.post(
|
|
174
|
+
ENDPOINTS["refresh"],
|
|
175
|
+
data=request.model_dump(by_alias=True) # ✅ SIEMPRE by_alias=True
|
|
176
|
+
)
|
|
177
|
+
if response.ok:
|
|
178
|
+
data = response.json()["data"]
|
|
179
|
+
self.access_token = data.get("token")
|
|
180
|
+
self.refresh_token = data.get("refreshToken")
|
|
181
|
+
return response
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### modules/01_login_flow.py - Estructura Base
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
"""Test module 01: Login flow."""
|
|
188
|
+
from services.auth_service.data_schema import LoginRequest, TEST_USER
|
|
189
|
+
|
|
190
|
+
from typing import TYPE_CHECKING
|
|
191
|
+
if TYPE_CHECKING:
|
|
192
|
+
from services.auth_service.auth_page import AuthPage
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def run(page: "AuthPage"):
|
|
196
|
+
"""Execute login test."""
|
|
197
|
+
print("STEP 01: Testing Login")
|
|
198
|
+
|
|
199
|
+
login_data = LoginRequest(
|
|
200
|
+
email=TEST_USER["email"],
|
|
201
|
+
password=TEST_USER["password"]
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
response = page.do_login(login_data)
|
|
205
|
+
|
|
206
|
+
assert response.ok, f"Login failed: {response.status}"
|
|
207
|
+
assert page.access_token is not None, "Token not stored"
|
|
208
|
+
|
|
209
|
+
print(f"✓ Login successful")
|
|
210
|
+
print(f"✓ Token: {page.access_token[:20]}...")
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 🔧 Solución de Problemas
|
|
216
|
+
|
|
217
|
+
### Problema 1: `ImportError: email-validator is not installed`
|
|
218
|
+
|
|
219
|
+
**Causa:** Falta la dependencia email-validator.
|
|
220
|
+
|
|
221
|
+
**Solución:**
|
|
222
|
+
1. Asegúrate de que `requirements.txt` incluya `email-validator>=2.0.0`
|
|
223
|
+
2. Ejecuta: `pip install -r requirements.txt`
|
|
224
|
+
|
|
225
|
+
### Problema 2: `cannot import name '_01_register_flow'`
|
|
226
|
+
|
|
227
|
+
**Causa:** Los módulos con nombres numéricos no pueden importarse normalmente.
|
|
228
|
+
|
|
229
|
+
**Solución - Tres métodos:**
|
|
230
|
+
|
|
231
|
+
**Método A: importlib (recomendado para scripts)**
|
|
232
|
+
```python
|
|
233
|
+
import importlib.util
|
|
234
|
+
import importlib.machinery
|
|
235
|
+
|
|
236
|
+
def load_module(name, path):
|
|
237
|
+
loader = importlib.machinery.SourceFileLoader(name, path)
|
|
238
|
+
spec = importlib.util.spec_from_loader(name, loader)
|
|
239
|
+
module = importlib.util.module_from_spec(spec)
|
|
240
|
+
loader.exec_module(module)
|
|
241
|
+
return module
|
|
242
|
+
|
|
243
|
+
register_module = load_module(
|
|
244
|
+
"register_flow",
|
|
245
|
+
"/path/to/services/auth/modules/01_register_flow.py"
|
|
246
|
+
)
|
|
247
|
+
register_module.run(page)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Método B: Import directo**
|
|
251
|
+
```python
|
|
252
|
+
from services.auth.modules._01_register_flow import run
|
|
253
|
+
run(page)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Método C: Import función**
|
|
257
|
+
```python
|
|
258
|
+
from services.auth.modules import _01_register_flow
|
|
259
|
+
_01_register_flow.run(page)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Problema 3: `Refresh token is required` (400 error)
|
|
263
|
+
|
|
264
|
+
**Causa:** Pydantic serializa `refresh_token` en lugar de `refreshToken`.
|
|
265
|
+
|
|
266
|
+
**Solución:**
|
|
267
|
+
1. En el modelo, usa alias camelCase:
|
|
268
|
+
```python
|
|
269
|
+
refresh_token: str = Field(
|
|
270
|
+
...,
|
|
271
|
+
alias="refreshToken",
|
|
272
|
+
serialization_alias="refreshToken"
|
|
273
|
+
)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
2. Al serializar, usa by_alias=True:
|
|
277
|
+
```python
|
|
278
|
+
data = request.model_dump(by_alias=True)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Problema 4: `Method declaration "logout" is obscured`
|
|
282
|
+
|
|
283
|
+
**Causa:** Conflicto entre atributo y método con mismo nombre.
|
|
284
|
+
|
|
285
|
+
**Solución:**
|
|
286
|
+
```python
|
|
287
|
+
# Atributo
|
|
288
|
+
self.refresh_token: Optional[str] = None
|
|
289
|
+
|
|
290
|
+
# Método con prefijo do_
|
|
291
|
+
def do_refresh_token(self):
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
def do_logout(self):
|
|
295
|
+
pass
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Problema 5: `update_headers not found`
|
|
299
|
+
|
|
300
|
+
**Causa:** El método `update_headers()` no existe en `BasePage`.
|
|
301
|
+
|
|
302
|
+
**Solución:** Implementa manejo manual:
|
|
303
|
+
```python
|
|
304
|
+
def _get_headers(self, extra=None):
|
|
305
|
+
headers = {"Content-Type": "application/json"}
|
|
306
|
+
if self.access_token:
|
|
307
|
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
308
|
+
if extra:
|
|
309
|
+
headers.update(extra)
|
|
310
|
+
return headers
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## ✅ Checklist Pre-Entrega
|
|
316
|
+
|
|
317
|
+
Antes de decir "terminado", verifica:
|
|
318
|
+
|
|
319
|
+
- [ ] `email-validator` está en requirements.txt
|
|
320
|
+
- [ ] Todos los modelos tienen `model_config = {"populate_by_name": True}`
|
|
321
|
+
- [ ] Campos compuestos tienen `alias` y `serialization_alias` en camelCase
|
|
322
|
+
- [ ] En todos los métodos de page: `request.model_dump(by_alias=True)`
|
|
323
|
+
- [ ] NINGÚN import relativo (`from ..x`)
|
|
324
|
+
- [ ] Todos los imports son absolutos (`from services.xxx`)
|
|
325
|
+
- [ ] Métodos conflictivos usan prefijo `do_` (do_refresh_token, do_logout)
|
|
326
|
+
- [ ] Headers manejados manualmente (no update_headers)
|
|
327
|
+
- [ ] Funciones `run(page)` bien definidas en módulos
|
|
328
|
+
- [ ] Type hints usando `TYPE_CHECKING` para evitar imports circulares
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## 🎯 Flujo de Trabajo Recomendado
|
|
333
|
+
|
|
334
|
+
### Para Agentes de IA:
|
|
335
|
+
|
|
336
|
+
1. **Lee los controladores Java** del servicio target
|
|
337
|
+
2. **Identifica endpoints**: rutas, métodos HTTP, body params
|
|
338
|
+
3. **Crea data_schema.py**:
|
|
339
|
+
- Modelos Pydantic con aliases camelCase
|
|
340
|
+
- Constantes de endpoints
|
|
341
|
+
- Datos de test
|
|
342
|
+
4. **Crea service_page.py**:
|
|
343
|
+
- Hereda de BasePage
|
|
344
|
+
- Implementa _get_headers()
|
|
345
|
+
- Métodos para cada endpoint
|
|
346
|
+
- Usa by_alias=True siempre
|
|
347
|
+
5. **Crea modules/**:
|
|
348
|
+
- 01_*_flow.py: Crear/Login
|
|
349
|
+
- 02_*_flow.py: Operaciones CRUD
|
|
350
|
+
- 99_*_flow.py: Cleanup/Logout
|
|
351
|
+
6. **Verifica** ejecutando: `python verify_installation.py`
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 📚 Recursos Adicionales
|
|
356
|
+
|
|
357
|
+
- **Ejemplo Completo:** Ver `.agent/EXAMPLE_TEST.md`
|
|
358
|
+
- **Flujo de Generación:** Ver `.agent/WORKFLOW_GENERATION.md`
|
|
359
|
+
- **Documentación Framework:** https://daironpf.github.io/socialseed-e2e/
|
|
360
|
+
- **Pydantic v2:** https://docs.pydantic.dev/latest/
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## 🎓 Tips para Éxito
|
|
365
|
+
|
|
366
|
+
1. **Siempre verifica serialización:**
|
|
367
|
+
```python
|
|
368
|
+
print(request.model_dump(by_alias=True))
|
|
369
|
+
# Debe mostrar camelCase: {'refreshToken': 'xxx'}
|
|
370
|
+
# NO snake_case: {'refresh_token': 'xxx'}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
2. **Test incremental:**
|
|
374
|
+
- Ejecuta test 01 primero
|
|
375
|
+
- Verifica que el estado se guarda en `page`
|
|
376
|
+
- Luego ejecuta test 02
|
|
377
|
+
|
|
378
|
+
3. **Manejo de errores:**
|
|
379
|
+
```python
|
|
380
|
+
assert response.ok, f"Failed: {response.status} - {response.text()[:100]}"
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
4. **Compartir estado:**
|
|
384
|
+
- Guarda en `page` (ej: `page.user_id = data["id"]`)
|
|
385
|
+
- Recupera en siguiente test
|
|
386
|
+
|
|
387
|
+
5. **Ejecución Flexible:**
|
|
388
|
+
Si ejecutas desde el root del proyecto y el config está en una subcarpeta:
|
|
389
|
+
```bash
|
|
390
|
+
e2e run -c otrotest/e2e.conf
|
|
391
|
+
```
|
|
392
|
+
El framework encontrará los servicios automáticamente.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## 🚀 Resultado Esperado
|
|
397
|
+
|
|
398
|
+
Siguiendo esta guía, los agentes de IA pueden generar:
|
|
399
|
+
|
|
400
|
+
- ✅ Tests funcionales sin errores de importación
|
|
401
|
+
- ✅ Serialización correcta camelCase ↔ snake_case
|
|
402
|
+
- ✅ Manejo de autenticación entre tests
|
|
403
|
+
- ✅ Tests secuenciales con estado compartido
|
|
404
|
+
|
|
405
|
+
**Tiempo estimado:** 2-5 minutos por servicio
|
|
406
|
+
**Intervención humana:** Mínima o ninguna
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
**Versión:** 2.0
|
|
411
|
+
**Última actualización:** 2026-02-03
|
|
412
|
+
**Framework:** socialseed-e2e v0.1.0+
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Ejemplo Completo de Test Generado
|
|
2
|
+
|
|
3
|
+
Este archivo sirve como referencia "Gold Standard" de lo que se espera generar.
|
|
4
|
+
|
|
5
|
+
**IMPORTANT PRINCIPLES:**
|
|
6
|
+
- NO relative imports (from ..x import y) - use absolute imports from `services.xxx.data_schema`
|
|
7
|
+
- Always use `by_alias=True` when serializing Pydantic models
|
|
8
|
+
- Handle authentication headers manually (no `update_headers` method)
|
|
9
|
+
- Use `do_*` prefix for methods to avoid name conflicts with attributes
|
|
10
|
+
|
|
11
|
+
Supongamos una API de Tareas (TODOs).
|
|
12
|
+
|
|
13
|
+
## 1. services/todo_app/data_schema.py
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
class TodoItem(BaseModel):
|
|
20
|
+
id: Optional[str] = None
|
|
21
|
+
title: str
|
|
22
|
+
completed: bool = False
|
|
23
|
+
|
|
24
|
+
class Config:
|
|
25
|
+
populate_by_name = True
|
|
26
|
+
|
|
27
|
+
ENDPOINTS = {
|
|
28
|
+
"list": "/todos",
|
|
29
|
+
"create": "/todos",
|
|
30
|
+
"get": "/todos/{id}",
|
|
31
|
+
"update": "/todos/{id}",
|
|
32
|
+
"delete": "/todos/{id}"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 2. services/todo_app/todo_app_page.py
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from socialseed_e2e.core.base_page import BasePage
|
|
40
|
+
from playwright.sync_api import APIResponse, APIRequestContext
|
|
41
|
+
from services.todo_app.data_schema import TodoItem, ENDPOINTS
|
|
42
|
+
|
|
43
|
+
class TodoAppPage(BasePage):
|
|
44
|
+
def __init__(self, base_url: str, **kwargs):
|
|
45
|
+
super().__init__(base_url=base_url, **kwargs)
|
|
46
|
+
self.created_todo_id = None # Estado compartido
|
|
47
|
+
self.auth_token = None
|
|
48
|
+
|
|
49
|
+
def do_create_todo(self, item: TodoItem) -> APIResponse:
|
|
50
|
+
"""Create a new todo item."""
|
|
51
|
+
return self.post(
|
|
52
|
+
ENDPOINTS["create"],
|
|
53
|
+
data=item.model_dump(by_alias=True, exclude={"id"})
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def do_get_todo(self, todo_id: str) -> APIResponse:
|
|
57
|
+
"""Retrieve a todo by ID."""
|
|
58
|
+
path = ENDPOINTS["get"].format(id=todo_id)
|
|
59
|
+
return self.get(path)
|
|
60
|
+
|
|
61
|
+
def do_delete_todo(self, todo_id: str) -> APIResponse:
|
|
62
|
+
"""Delete a todo by ID."""
|
|
63
|
+
path = ENDPOINTS["delete"].format(id=todo_id)
|
|
64
|
+
return self.delete(path)
|
|
65
|
+
|
|
66
|
+
def do_authenticate(self, token: str) -> None:
|
|
67
|
+
"""Set authentication token manually."""
|
|
68
|
+
self.auth_token = token
|
|
69
|
+
# Manually handle headers - don't use update_headers()
|
|
70
|
+
self.headers = {**self.headers, "Authorization": f"Bearer {token}"}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 3. services/todo_app/modules/01_create_todo.py
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from services.todo_app.data_schema import TodoItem
|
|
77
|
+
from typing import TYPE_CHECKING
|
|
78
|
+
if TYPE_CHECKING:
|
|
79
|
+
from services.todo_app.todo_app_page import TodoAppPage
|
|
80
|
+
|
|
81
|
+
def run(page: 'TodoAppPage'):
|
|
82
|
+
print("STEP: Create Todo")
|
|
83
|
+
|
|
84
|
+
new_todo = TodoItem(title="Learn SocialSeed E2E", completed=False)
|
|
85
|
+
|
|
86
|
+
response = page.do_create_todo(new_todo)
|
|
87
|
+
assert response.ok, f"Failed to create todo: {response.status} - {response.text()}"
|
|
88
|
+
|
|
89
|
+
data = response.json()
|
|
90
|
+
assert data["title"] == new_todo.title, f"Title mismatch: expected {new_todo.title}, got {data.get('title')}"
|
|
91
|
+
assert "id" in data, "No 'id' field in response"
|
|
92
|
+
|
|
93
|
+
# Guardar ID para el siguiente paso
|
|
94
|
+
page.created_todo_id = data["id"]
|
|
95
|
+
print(f"✓ Created Todo with ID: {page.created_todo_id}")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 4. services/todo_app/modules/02_get_todo.py
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from typing import TYPE_CHECKING
|
|
102
|
+
if TYPE_CHECKING:
|
|
103
|
+
from services.todo_app.todo_app_page import TodoAppPage
|
|
104
|
+
|
|
105
|
+
def run(page: 'TodoAppPage'):
|
|
106
|
+
print("STEP: Get Todo")
|
|
107
|
+
|
|
108
|
+
# Recuperar ID del paso anterior
|
|
109
|
+
todo_id = page.created_todo_id
|
|
110
|
+
assert todo_id, "No created_todo_id found from previous step"
|
|
111
|
+
|
|
112
|
+
response = page.do_get_todo(todo_id)
|
|
113
|
+
assert response.ok, f"Failed to get todo: {response.status} - {response.text()}"
|
|
114
|
+
|
|
115
|
+
data = response.json()
|
|
116
|
+
assert data["id"] == todo_id, f"ID mismatch: expected {todo_id}, got {data.get('id')}"
|
|
117
|
+
print("✓ Todo retrieved successfully")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 5. services/todo_app/modules/03_authenticated_request.py
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from services.todo_app.data_schema import TodoItem
|
|
124
|
+
from typing import TYPE_CHECKING
|
|
125
|
+
if TYPE_CHECKING:
|
|
126
|
+
from services.todo_app.todo_app_page import TodoAppPage
|
|
127
|
+
|
|
128
|
+
def run(page: 'TodoAppPage'):
|
|
129
|
+
print("STEP: Authenticated Request Example")
|
|
130
|
+
|
|
131
|
+
# Perform authentication and store token
|
|
132
|
+
auth_response = page.post("/auth/login", data={
|
|
133
|
+
"username": "test@example.com",
|
|
134
|
+
"password": "password123"
|
|
135
|
+
})
|
|
136
|
+
assert auth_response.ok, f"Authentication failed: {auth_response.status}"
|
|
137
|
+
|
|
138
|
+
token = auth_response.json().get("token")
|
|
139
|
+
assert token, "No token in authentication response"
|
|
140
|
+
|
|
141
|
+
# Manually set the authentication token
|
|
142
|
+
page.do_authenticate(token)
|
|
143
|
+
|
|
144
|
+
# Now make authenticated request
|
|
145
|
+
new_todo = TodoItem(title="Authenticated Todo", completed=False)
|
|
146
|
+
|
|
147
|
+
# Use by_alias=True when serializing
|
|
148
|
+
response = page.do_create_todo(new_todo)
|
|
149
|
+
assert response.ok, f"Failed to create todo: {response.status} - {response.text()}"
|
|
150
|
+
|
|
151
|
+
print("✓ Authenticated request successful")
|
|
152
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Contexto del Framework socialseed-e2e para Agentes de IA
|
|
2
|
+
|
|
3
|
+
Este documento te proporciona el contexto necesario para trabajar con el framework `socialseed-e2e`. Tu objetivo es ayudar al desarrollador a crear pruebas end-to-end (E2E) para APIs REST de manera autónoma.
|
|
4
|
+
|
|
5
|
+
## 1. Arquitectura del Framework
|
|
6
|
+
|
|
7
|
+
El framework sigue una arquitectura hexagonal simplificada para desacoplar la lógica de prueba de la lógica del servicio.
|
|
8
|
+
|
|
9
|
+
### Estructura de Directorios Clave
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
.
|
|
13
|
+
├── e2e.conf # Configuración principal (URLs, timeouts)
|
|
14
|
+
├── services/ # Directorio de definición de servicios
|
|
15
|
+
│ └── <service-name>/ # Cada API/Microservicio tiene su carpeta
|
|
16
|
+
│ ├── __init__.py
|
|
17
|
+
│ ├── config.py # Configuración específica del servicio
|
|
18
|
+
│ ├── data_schema.py # DTOs (Pydantic), Endpoints y Constantes
|
|
19
|
+
│ ├── <name>_page.py # Page Object Model (hereda de BasePage)
|
|
20
|
+
│ └── modules/ # Tests individuales
|
|
21
|
+
│ ├── 01_register_flow.py
|
|
22
|
+
│ ├── 02_login_flow.py
|
|
23
|
+
│ └── ...
|
|
24
|
+
└── tests/ # Tests generales o de integración del propio framework
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 2. Conceptos Principales
|
|
28
|
+
|
|
29
|
+
### Service Page (Page Object)
|
|
30
|
+
Cada servicio tiene una clase "Page" (ej. `UsersApiPage`) que hereda de `BasePage`.
|
|
31
|
+
- **Responsabilidad**: Abstraer las llamadas HTTP.
|
|
32
|
+
- **Ubicación**: `services/<service-name>/<name>_page.py`.
|
|
33
|
+
- **Métodos**: Deben corresponder a acciones de negocio (ej. `register_user`, `get_profile`) y retornar `APIResponse` de Playwright.
|
|
34
|
+
|
|
35
|
+
### Test Modules
|
|
36
|
+
Los tests se dividen en archivos pequeños y numerados dentro de `services/<service-name>/modules/`.
|
|
37
|
+
- **Naming**: `XX_description_flow.py` (ej. `01_register_flow.py`).
|
|
38
|
+
- **Estructura**: Cada módulo debe tener una función `run(context)` o `run(page)`.
|
|
39
|
+
- **Orden**: Se ejecutan secuencialmente según su numeración.
|
|
40
|
+
- **Estado**: El estado se comparte a través de la instancia de la `Page`. Si el test 01 guarda un token en `page.auth_token`, el test 02 puede usarlo.
|
|
41
|
+
|
|
42
|
+
### Data Schema
|
|
43
|
+
- **Ubicación**: `services/<service-name>/data_schema.py`.
|
|
44
|
+
- **Contenido**: Modelos Pydantic para request/response bodies y costantes de rutas.
|
|
45
|
+
|
|
46
|
+
## 3. Tu Rol como Agente
|
|
47
|
+
|
|
48
|
+
Cuando el usuario te pida "generar tests", debes:
|
|
49
|
+
1. **Analizar** el código fuente del controlador REST que te provea el usuario.
|
|
50
|
+
2. **Identificar** los endpoints, métodos HTTP, y estructuras de datos.
|
|
51
|
+
3. **Mapear** estos endpoints a métodos en la `ServicePage`.
|
|
52
|
+
4. **Generar** módulos de prueba secuenciales que cubran flujos lógicos (crear -> obtener -> actualizar -> borrar).
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
Lee `WORKFLOW_GENERATION.md` para instrucciones paso a paso sobre cómo generar código.
|