refactor: make ARNES external-repo based with ticket publish flow
This commit is contained in:
@@ -10,13 +10,16 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Este directorio contiene especificaciones BDD en formato Gherkin.
|
||||
Los archivos `.feature` sirven como especificación ejecutable.
|
||||
Este directorio contiene las especificaciones BDD fuente en formato Gherkin.
|
||||
|
||||
Separación recomendada:
|
||||
- `spec/bdd/features/` = source-of-truth de escenarios
|
||||
- `features/` = assets ejecutables del runner (steps, config)
|
||||
|
||||
### naming conventions
|
||||
|
||||
```
|
||||
features/
|
||||
```text
|
||||
spec/bdd/features/
|
||||
├── <domain>/
|
||||
│ ├── <feature-name>.feature
|
||||
│ └── <feature-name>.feature
|
||||
|
||||
0
spec/bdd/features/.gitkeep
Normal file
0
spec/bdd/features/.gitkeep
Normal file
@@ -1,58 +1,12 @@
|
||||
# Features BDD
|
||||
# BDD feature files
|
||||
|
||||
Este directorio contiene los archivos `.feature` organizados por dominio.
|
||||
Put Gherkin `.feature` files here.
|
||||
|
||||
## Estructura
|
||||
Example:
|
||||
- `spec/bdd/features/checkout/purchase.feature`
|
||||
- `spec/bdd/features/common/error-handling.feature`
|
||||
|
||||
```
|
||||
features/
|
||||
├── auth/
|
||||
│ ├── login.feature
|
||||
│ └── registration.feature
|
||||
├── dashboard/
|
||||
│ └── dashboard.feature
|
||||
├── common/
|
||||
│ ├── navigation.feature
|
||||
│ └── error-handling.feature
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Tags comunes
|
||||
|
||||
Usar estos tags en todos los features:
|
||||
|
||||
| Tag | Descripción |
|
||||
|-----|-------------|
|
||||
| `@F-XXX` | Link a feature ID del backlog |
|
||||
| `@smoke` | Test crítico |
|
||||
| `@regression` | Regresión |
|
||||
|
||||
## Example
|
||||
|
||||
```gherkin
|
||||
@F-001 @auth @smoke
|
||||
Feature: Inicio de sesión
|
||||
|
||||
Como usuario registrado
|
||||
Quiero iniciar sesión con mis credenciales
|
||||
Para acceder a mi cuenta personal
|
||||
|
||||
@positive
|
||||
Scenario: Login exitoso con credenciales válidas
|
||||
Given un usuario con email "user@example.com" y password "Password123"
|
||||
And el usuario no tiene sesión activa
|
||||
When el usuario ingresa email "user@example.com"
|
||||
And ingresa password "Password123"
|
||||
And presiona el botón "Iniciar sesión"
|
||||
Then el sistema redirige al dashboard
|
||||
And muestra mensaje de bienvenida
|
||||
|
||||
@negative
|
||||
Scenario: Login fallido con password incorrecto
|
||||
Given un usuario con email "user@example.com" y password "Password123"
|
||||
When el usuario ingresa email "user@example.com"
|
||||
And ingresa password "WrongPassword"
|
||||
And presiona el botón "Iniciar sesión"
|
||||
Then el sistema muestra mensaje de error "Credenciales inválidas"
|
||||
And permanece en la página de login
|
||||
```
|
||||
Use tags like:
|
||||
- `@F-001`
|
||||
- `@smoke`
|
||||
- `@regression`
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
@F-004 @auth @login
|
||||
Feature: User Login
|
||||
|
||||
Background:
|
||||
Given the user "alice@example.com" exists with password "SecurePass123!"
|
||||
|
||||
@positive
|
||||
Scenario: Successful login with valid credentials
|
||||
Given I have valid email "alice@example.com" and password "SecurePass123!"
|
||||
When I attempt to login
|
||||
Then I should receive an access token
|
||||
And the access token should contain user_id claim
|
||||
And the access token should contain email claim
|
||||
And the access token should not be expired
|
||||
|
||||
@positive
|
||||
Scenario: Login returns refresh token
|
||||
Given I have valid credentials for "alice@example.com"
|
||||
When I login successfully
|
||||
Then I should receive a refresh token
|
||||
And the refresh token should be different from access token
|
||||
And the refresh token should have longer expiration
|
||||
|
||||
@positive
|
||||
Scenario: Login email is case-insensitive
|
||||
Given a user exists with email "bob@test.com" and password "TestPass99!"
|
||||
When I login with email "BOB@TEST.COM" and password "TestPass99!"
|
||||
Then login should be successful
|
||||
|
||||
@negative
|
||||
Scenario: Login with wrong password
|
||||
Given I have email "alice@example.com" and password "WrongPassword123!"
|
||||
When I attempt to login
|
||||
Then I should receive error "Credenciales inválidas"
|
||||
And I should not receive any token
|
||||
|
||||
@negative
|
||||
Scenario: Login with nonexistent user
|
||||
Given I have email "nonexistent@test.com" and password "AnyPass123!"
|
||||
When I attempt to login
|
||||
Then I should receive error "Credenciales inválidas"
|
||||
And I should not receive any token
|
||||
|
||||
@negative
|
||||
Scenario: Login with empty password
|
||||
Given I have email "alice@example.com" and empty password
|
||||
When I attempt to login
|
||||
Then I should receive validation error
|
||||
And I should not receive any token
|
||||
|
||||
@negative
|
||||
Scenario: Login with invalid email format
|
||||
Given I have email "not-an-email" and password "ValidPass123!"
|
||||
When I attempt to login
|
||||
Then I should receive validation error
|
||||
And I should not receive any token
|
||||
|
||||
@security @rate-limit
|
||||
Scenario: Login blocked after 10 failed attempts
|
||||
Given I have email "alice@example.com" and password "WrongPassword!"
|
||||
When I attempt to login 10 times with wrong password
|
||||
Then account should be temporarily locked
|
||||
And next login attempt should return error "Cuenta bloqueada"
|
||||
|
||||
@smoke
|
||||
Scenario: Login endpoint responds with JSON
|
||||
Given I have valid credentials for "alice@example.com"
|
||||
When I send a POST request to "/api/v1/auth/login"
|
||||
Then response should be JSON format
|
||||
And response should have correct content-type header
|
||||
@@ -1,58 +0,0 @@
|
||||
@F-004 @auth @logout
|
||||
Feature: User Logout
|
||||
|
||||
Background:
|
||||
Given the user "alice@example.com" exists with password "SecurePass123!"
|
||||
And I am authenticated as "alice@example.com"
|
||||
|
||||
@positive
|
||||
Scenario: Successful logout invalidates current session
|
||||
Given my current access token is valid
|
||||
When I logout
|
||||
Then I should receive confirmation
|
||||
And my session should be marked as revoked
|
||||
And my access token should no longer be valid
|
||||
|
||||
@positive
|
||||
Scenario: Logout with refresh token also invalidates access
|
||||
Given I have a valid refresh token
|
||||
When I logout
|
||||
Then both access and refresh tokens should be invalid
|
||||
And I should not be able to get new access token with refresh
|
||||
|
||||
@positive
|
||||
Scenario: Logout all sessions for user
|
||||
Given I am logged in from device "desktop"
|
||||
And I am logged in from device "mobile"
|
||||
When I logout from all devices
|
||||
Then all my sessions should be invalidated
|
||||
And I should not be able to use any previous token
|
||||
|
||||
@negative
|
||||
Scenario: Using token after logout returns unauthorized
|
||||
Given I previously logged in successfully
|
||||
And I have logged out
|
||||
When I try to use my old access token
|
||||
Then I should receive 401 Unauthorized
|
||||
And I should not have access to protected resources
|
||||
|
||||
@negative
|
||||
Scenario: Logout with invalid token does nothing
|
||||
Given I have an invalid/expired token
|
||||
When I attempt to logout
|
||||
Then logout should not fail
|
||||
But no session should be affected
|
||||
|
||||
@security
|
||||
Scenario: Concurrent logout requests are handled correctly
|
||||
Given my session is valid
|
||||
When I send multiple logout requests simultaneously
|
||||
Then only one logout operation should occur
|
||||
And token should be invalidated only once
|
||||
|
||||
@smoke
|
||||
Scenario: Logout endpoint returns 200 on success
|
||||
Given I am authenticated as "alice@example.com"
|
||||
When I send POST request to "/api/v1/auth/logout"
|
||||
Then response should be 200 OK
|
||||
And response should indicate success
|
||||
@@ -1,36 +0,0 @@
|
||||
# Common Features
|
||||
|
||||
Features que se reutilizan en múltiples dominios.
|
||||
|
||||
## Navigation
|
||||
|
||||
```gherkin
|
||||
@common @navigation
|
||||
Feature: Navegación entre páginas
|
||||
|
||||
Scenario: Navegar a través del menú
|
||||
Given el usuario está en la página principal
|
||||
When hace clic en el elemento de menú "Dashboard"
|
||||
Then la URL cambia a "/dashboard"
|
||||
And el título de la página muestra "Dashboard"
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```gherkin
|
||||
@common @error-handling
|
||||
Feature: Manejo de errores
|
||||
|
||||
Scenario: Mostrar error de red
|
||||
Given la conexión a internet está disponible
|
||||
And el servidor no responde
|
||||
When el usuario realiza una acción que requiere red
|
||||
Then el sistema muestra toast "Error de conexión"
|
||||
And ofrece opción de reintentar
|
||||
|
||||
Scenario: Timeout de solicitud
|
||||
Given el usuario tiene sesión activa
|
||||
When realiza una solicitud que excede 30 segundos
|
||||
Then el sistema muestra indicador de carga
|
||||
And después de timeout muestra error "Solicitud expirada"
|
||||
```
|
||||
@@ -1,171 +0,0 @@
|
||||
@F-003 @password
|
||||
Feature: Cambio de Contraseña
|
||||
|
||||
Como usuario autenticado
|
||||
Quiero cambiar mi contraseña
|
||||
Para mantener mi cuenta segura con credenciales actualizadas
|
||||
|
||||
# ====================
|
||||
# HAPPY PATH
|
||||
# ====================
|
||||
|
||||
@smoke @positive
|
||||
Scenario: Cambiar contraseña exitosamente
|
||||
Given un usuario autenticado con email "user@example.com"
|
||||
And su contraseña actual es "OldPass123!"
|
||||
When el usuario solicita cambiar contraseña
|
||||
And ingresa contraseña actual "OldPass123!"
|
||||
And ingresa nueva contraseña "NewPass456@"
|
||||
And confirma nueva contraseña "NewPass456@"
|
||||
Then el sistema valida la contraseña actual correctamente
|
||||
And guarda la nueva contraseña hasheada
|
||||
And invalida todas las sesiones existentes
|
||||
And muestra mensaje de confirmación "Contraseña actualizada exitosamente"
|
||||
|
||||
@positive
|
||||
Scenario: Contraseña con todos los caracteres especiales permitidos
|
||||
Given un usuario autenticado
|
||||
When cambia contraseña a "!@#$%^&*()_+-=[]{}|;':\",./<>?abc123ABC"
|
||||
Then el sistema acepta la contraseña
|
||||
And la guarda correctamente
|
||||
|
||||
# ====================
|
||||
# PASSWORD VALIDATION
|
||||
# ====================
|
||||
|
||||
@negative
|
||||
Scenario: Nueva contraseña muy corta (menos de 8 caracteres)
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar contraseña a "Ab1!"
|
||||
Then el sistema muestra error "La contraseña debe tener al menos 8 caracteres"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
@negative
|
||||
Scenario: Nueva contraseña muy larga (más de 128 caracteres)
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar contraseña a "A" repetido 129 veces más "a1!"
|
||||
Then el sistema muestra error "La contraseña debe tener máximo 128 caracteres"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
@negative
|
||||
Scenario: Nueva contraseña sin mayúscula
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar contraseña a "password123!"
|
||||
Then el sistema muestra error "La contraseña debe contener al menos una mayúscula"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
@negative
|
||||
Scenario: Nueva contraseña sin minúscula
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar contraseña a "PASSWORD123!"
|
||||
Then el sistema muestra error "La contraseña debe contener al menos una minúscula"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
@negative
|
||||
Scenario: Nueva contraseña sin número
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar contraseña a "PasswordABC!"
|
||||
Then el sistema muestra error "La contraseña debe contener al menos un número"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
@negative
|
||||
Scenario: Nueva contraseña sin carácter especial
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar contraseña a "Password123"
|
||||
Then el sistema muestra error "La contraseña debe contener al menos un carácter especial (!@#$%^&*...)"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
# ====================
|
||||
# CURRENT PASSWORD
|
||||
# ====================
|
||||
|
||||
@negative
|
||||
Scenario: Contraseña actual incorrecta
|
||||
Given un usuario autenticado con contraseña actual "CorrectPass123!"
|
||||
When intenta cambiar contraseña con actual "WrongPass456!"
|
||||
And nueva contraseña "NewPass789@"
|
||||
Then el sistema muestra error "La contraseña actual es incorrecta"
|
||||
And la contraseña no es cambiada
|
||||
And no se invalidan sesiones
|
||||
|
||||
@negative
|
||||
Scenario: Contraseña actual vacía
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar contraseña con actual ""
|
||||
And nueva contraseña "NewPass123@"
|
||||
Then el sistema muestra error "La contraseña actual es requerida"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
# ====================
|
||||
# PASSWORD MISMATCH
|
||||
# ====================
|
||||
|
||||
@negative
|
||||
Scenario: Nueva contraseña y confirmación no coinciden
|
||||
Given un usuario autenticado
|
||||
When ingresa contraseña actual correcta
|
||||
And ingresa nueva contraseña "NewPass123@"
|
||||
But confirma con "DifferentPass456!"
|
||||
Then el sistema muestra error "Las contraseñas no coinciden"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
# ====================
|
||||
# REUSE DETECTION
|
||||
# ====================
|
||||
|
||||
@negative @security
|
||||
Scenario: Reutilizar contraseña anterior
|
||||
Given un usuario autenticado con contraseña actual "MyPass123!"
|
||||
And historial de contraseñas incluye "MyPass123!"
|
||||
When intenta cambiar contraseña a "MyPass123!"
|
||||
Then el sistema muestra error "La nueva contraseña no puede ser igual a la anterior"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
# ====================
|
||||
# AUTHORIZATION
|
||||
# ====================
|
||||
|
||||
@negative @security
|
||||
Scenario: Usuario no autenticado intenta cambiar contraseña
|
||||
Given un usuario no autenticado
|
||||
When intenta cambiar contraseña
|
||||
Then el sistema retorna error 401 "No autorizado"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
@negative @security
|
||||
Scenario: Token expirado al cambiar contraseña
|
||||
Given un usuario con sesión expirada
|
||||
When intenta cambiar contraseña
|
||||
Then el sistema retorna error 401 "Sesión expirada"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
@negative @security
|
||||
Scenario: Intentar cambiar contraseña de otro usuario
|
||||
Given un usuario autenticado con ID "user-123"
|
||||
When intenta cambiar contraseña del usuario "user-456"
|
||||
Then el sistema retorna error 403 "No tienes permiso para modificar esta cuenta"
|
||||
And la contraseña no es cambiada
|
||||
|
||||
# ====================
|
||||
# RATE LIMITING
|
||||
# ====================
|
||||
|
||||
@negative @security
|
||||
Scenario: Superar límite de intentos (rate limit)
|
||||
Given un usuario autenticado
|
||||
And ya realizó 5 intentos fallidos en la última hora
|
||||
When intenta cambiar contraseña una vez más
|
||||
Then el sistema retorna error 429 "Demasiados intentos. Intenta de nuevo en 1 hora"
|
||||
And todas las solicitudes son bloqueadas hasta que pase el tiempo
|
||||
|
||||
# ====================
|
||||
# SUCCESSFUL REAUTHENTICATION
|
||||
# ====================
|
||||
|
||||
@positive
|
||||
Scenario: Cambio de contraseña seguido de login exitoso
|
||||
Given un usuario con contraseña "OldPass123!"
|
||||
When cambia su contraseña a "NewPass456@"
|
||||
And luego intenta iniciar sesión con "NewPass456@"
|
||||
Then el login es exitoso
|
||||
And el usuario accede a su cuenta
|
||||
@@ -1,159 +0,0 @@
|
||||
@F-002 @profile
|
||||
Feature: Gestión de Perfil de Usuario
|
||||
|
||||
Como usuario autenticado
|
||||
Quiero gestionar mi perfil
|
||||
Para mantener mis datos personales actualizados y personalizar mi experiencia
|
||||
|
||||
# ====================
|
||||
# VIEW PROFILE
|
||||
# ====================
|
||||
|
||||
@smoke @positive
|
||||
Scenario: Ver perfil de usuario exitosamente
|
||||
Given un usuario autenticado con ID "user-123" y nombre "Juan Pérez"
|
||||
And el usuario tiene avatar "https://cdn.example.com/avatar-123.jpg"
|
||||
And el idioma configurado es "es"
|
||||
When el usuario solicita ver su perfil
|
||||
Then el sistema retorna los datos completos del perfil
|
||||
And incluye id "user-123", nombre "Juan Pérez"
|
||||
And incluye avatar_url y language "es"
|
||||
|
||||
@negative
|
||||
Scenario: Ver perfil sin autenticación
|
||||
Given un usuario no autenticado
|
||||
When el usuario solicita ver su perfil
|
||||
Then el sistema retorna error 401 "No autorizado"
|
||||
And no retorna datos del perfil
|
||||
|
||||
@negative
|
||||
Scenario: Ver perfil de usuario inexistente
|
||||
Given un usuario autenticado
|
||||
When solicita ver perfil de ID "nonexistent-user"
|
||||
Then el sistema retorna error 404 "Usuario no encontrado"
|
||||
|
||||
# ====================
|
||||
# UPDATE NAME
|
||||
# ====================
|
||||
|
||||
@smoke @positive
|
||||
Scenario: Editar nombre del perfil exitosamente
|
||||
Given un usuario autenticado con ID "user-123"
|
||||
And el perfil tiene nombre "Juan"
|
||||
When el usuario actualiza su nombre a "Pedro"
|
||||
Then el perfil muestra nombre "Pedro"
|
||||
And la fecha de updated_at se actualiza
|
||||
|
||||
@positive
|
||||
Scenario: Editar nombre con caracteres unicode válidos
|
||||
Given un usuario autenticado
|
||||
When cambia su nombre a "José García"
|
||||
Then el sistema acepta el cambio
|
||||
And el nombre se guarda como "José García"
|
||||
|
||||
@negative
|
||||
Scenario: Editar nombre con caracteres inválidos
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar nombre a "Juan@123!"
|
||||
Then el sistema muestra error de validación "Nombre inválido: solo letras y espacios"
|
||||
And el nombre permanece sin cambios
|
||||
|
||||
@negative
|
||||
Scenario: Editar nombre con menos de 2 caracteres
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar nombre a "J"
|
||||
Then el sistema muestra error "Nombre debe tener al menos 2 caracteres"
|
||||
|
||||
@negative
|
||||
Scenario: Editar nombre con más de 50 caracteres
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar nombre a "A" repetido 51 veces
|
||||
Then el sistema muestra error "Nombre debe tener máximo 50 caracteres"
|
||||
|
||||
# ====================
|
||||
# UPDATE AVATAR
|
||||
# ====================
|
||||
|
||||
@smoke @positive
|
||||
Scenario: Cambiar avatar exitosamente
|
||||
Given un usuario autenticado con avatar actual "https://cdn.example.com/old.jpg"
|
||||
When el usuario sube un nuevo avatar "https://cdn.example.com/new.jpg"
|
||||
Then el perfil muestra avatar_url "https://cdn.example.com/new.jpg"
|
||||
|
||||
@negative
|
||||
Scenario: Cambiar avatar con URL inválida
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar avatar a "not-a-valid-url"
|
||||
Then el sistema muestra error "URL de avatar inválida"
|
||||
And el avatar permanece sin cambios
|
||||
|
||||
@negative
|
||||
Scenario: Cambiar avatar con URL de protocolo no permitido
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar avatar a "ftp://malicious.com/file.jpg"
|
||||
Then el sistema muestra error "Solo se permiten URLs http o https"
|
||||
And el avatar permanece sin cambios
|
||||
|
||||
# ====================
|
||||
# UPDATE LANGUAGE
|
||||
# ====================
|
||||
|
||||
@smoke @positive
|
||||
Scenario: Cambiar idioma a español exitosamente
|
||||
Given un usuario autenticado con idioma "en"
|
||||
When el usuario cambia idioma a "es"
|
||||
Then el idioma se guarda como "es"
|
||||
And el sistema confirma el cambio
|
||||
|
||||
@positive
|
||||
Scenario: Cambiar idioma a francés
|
||||
Given un usuario autenticado
|
||||
When cambia idioma a "fr"
|
||||
Then el sistema acepta "fr" como idioma válido
|
||||
|
||||
@positive
|
||||
Scenario: Cambiar idioma a alemán
|
||||
Given un usuario autenticado
|
||||
When cambia idioma a "de"
|
||||
Then el sistema acepta "de" como idioma válido
|
||||
|
||||
@negative
|
||||
Scenario: Cambiar idioma a idioma no soportado
|
||||
Given un usuario autenticado
|
||||
When intenta cambiar idioma a "zh"
|
||||
Then el sistema muestra error "Idioma no soportado"
|
||||
And el idioma permanece sin cambios
|
||||
|
||||
# ====================
|
||||
# PARTIAL UPDATE
|
||||
# ====================
|
||||
|
||||
@positive
|
||||
Scenario: Actualizar solo nombre sin cambiar avatar
|
||||
Given un usuario autenticado con nombre "Juan" y avatar "https://cdn.com/img.jpg"
|
||||
When el usuario solo actualiza nombre a "Pedro"
|
||||
Then el nombre cambia a "Pedro"
|
||||
And el avatar_url permanece "https://cdn.com/img.jpg"
|
||||
|
||||
@positive
|
||||
Scenario: Actualizar múltiples campos en una petición
|
||||
Given un usuario autenticado
|
||||
When envía actualización con nombre "María", avatar "https://cdn.com/maria.jpg", idioma "es"
|
||||
Then todos los campos se actualizan correctamente
|
||||
And el perfil refleja todos los cambios
|
||||
|
||||
# ====================
|
||||
# AUTHORIZATION
|
||||
# ====================
|
||||
|
||||
@negative @security
|
||||
Scenario: Usuario intenta editar perfil de otro usuario
|
||||
Given un usuario autenticado con ID "user-123"
|
||||
When intenta actualizar perfil de usuario "user-456"
|
||||
Then el sistema retorna error 403 "No tienes permiso para editar este perfil"
|
||||
|
||||
@negative @security
|
||||
Scenario: Token expirado al editar perfil
|
||||
Given un usuario con token expirado
|
||||
When intenta actualizar su perfil
|
||||
Then el sistema retorna error 401 "Sesión expirada"
|
||||
@@ -1,4 +1,4 @@
|
||||
# SDD/BBD Guide — System Design Document & Behavior Driven Development
|
||||
# SDD/BDD Guide — System Design Document & Behavior Driven Development
|
||||
|
||||
Guía para crear y mantener SDD (System Design Document) y BDD (Behavior Driven Development) specs dentro del framework ARNES.
|
||||
|
||||
@@ -25,10 +25,13 @@ spec/
|
||||
│ ├── architecture.md
|
||||
│ ├── components/
|
||||
│ └── decisions/
|
||||
└── bdd/ # Behavior Driven Development
|
||||
└── bdd/ # Behavior Driven Development source-of-truth
|
||||
├── README.md
|
||||
├── features/
|
||||
└── step_definitions/
|
||||
└── features/
|
||||
|
||||
features/ # optional executable BDD runner assets
|
||||
├── behave.ini
|
||||
└── steps/
|
||||
```
|
||||
|
||||
---
|
||||
@@ -152,6 +155,11 @@ spec/bdd/features/
|
||||
│ └── purchase.feature
|
||||
└── common/
|
||||
└── error-handling.feature
|
||||
|
||||
features/
|
||||
├── behave.ini
|
||||
└── steps/
|
||||
└── login_steps.py
|
||||
```
|
||||
|
||||
### Tags para trazabilidad
|
||||
@@ -224,8 +232,10 @@ Tags disponibles:
|
||||
|
||||
```bash
|
||||
# Estructura
|
||||
spec/bdd/features/
|
||||
└── login.feature
|
||||
|
||||
features/
|
||||
├── login.feature
|
||||
└── steps/
|
||||
└── login_steps.py
|
||||
|
||||
@@ -237,8 +247,10 @@ behave features/
|
||||
|
||||
```bash
|
||||
# Estructura
|
||||
spec/bdd/features/
|
||||
└── login.feature
|
||||
|
||||
features/
|
||||
├── login.feature
|
||||
└── step_definitions/
|
||||
└── login_steps.js
|
||||
|
||||
|
||||
0
spec/sdd/components/.gitkeep
Normal file
0
spec/sdd/components/.gitkeep
Normal file
@@ -1,74 +0,0 @@
|
||||
# Component: <Nombre>
|
||||
|
||||
## Responsabilidad
|
||||
Descripción clara de qué hace este componente.
|
||||
|
||||
## Tipo
|
||||
- [ ] Microservicio
|
||||
- [ ] Library/Biblioteca
|
||||
- [ ] Shared Component
|
||||
- [ ] External Integration
|
||||
|
||||
## Interfaces
|
||||
|
||||
### API (si aplica)
|
||||
```
|
||||
Method: GET/POST/PUT/DELETE /endpoint
|
||||
Input: { ... }
|
||||
Output: { ... }
|
||||
Errors: 400, 401, 404, 500
|
||||
```
|
||||
|
||||
### Eventos (si aplica)
|
||||
- `topic.name.v1` — descripción del evento
|
||||
|
||||
## Dependencias
|
||||
|
||||
| Servicio/Biblioteca | Tipo | Notas |
|
||||
|---------------------|------|-------|
|
||||
| | | |
|
||||
|
||||
## Límites
|
||||
|
||||
### Alcance
|
||||
- ✅ Qué hace
|
||||
- ❌ Qué NO hace
|
||||
|
||||
### Constraints
|
||||
- Timeout máximo: Xms
|
||||
- Rate limit: Y req/min
|
||||
|
||||
## Criterios de éxito
|
||||
|
||||
| Criterio | Métrica | Target |
|
||||
|----------|---------|--------|
|
||||
| Disponibilidad | uptime | 99.9% |
|
||||
| Latencia | p99 | < 200ms |
|
||||
|
||||
## Diagrama
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Input] --> B[Component]
|
||||
B --> C[Output]
|
||||
```
|
||||
|
||||
## Estados
|
||||
|
||||
| Estado | Trigger | Acción |
|
||||
|--------|---------|--------|
|
||||
| Initial | created | ... |
|
||||
| Active | running | ... |
|
||||
| Error | failure | ... |
|
||||
|
||||
## Seguridad
|
||||
|
||||
- Authentication: ...
|
||||
- Authorization: ...
|
||||
- Rate limiting: ...
|
||||
|
||||
## Observabilidad
|
||||
|
||||
- Metrics: ...
|
||||
- Logs: ...
|
||||
- Traces: ...
|
||||
8
spec/sdd/components/README.md
Normal file
8
spec/sdd/components/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# SDD components
|
||||
|
||||
Put one markdown file per technical component.
|
||||
|
||||
Example:
|
||||
- `api-gateway.md`
|
||||
- `order-service.md`
|
||||
- `cart-repository.md`
|
||||
@@ -1,65 +0,0 @@
|
||||
# AuthService Component
|
||||
|
||||
## Purpose
|
||||
Handle user authentication (login/logout) with JWT tokens.
|
||||
|
||||
## Public API
|
||||
|
||||
### Methods
|
||||
|
||||
#### login(email: str, password: str) -> AuthResult
|
||||
Authenticate user with email and password.
|
||||
|
||||
**Parameters:**
|
||||
- `email`: User email address
|
||||
- `password`: User password
|
||||
|
||||
**Returns:**
|
||||
- `AuthResult` with access_token, refresh_token, expires_in
|
||||
|
||||
**Raises:**
|
||||
- `InvalidCredentialsError`: Email or password incorrect
|
||||
- `AccountLockedError`: Account temporarily locked
|
||||
- `ValidationError`: Invalid input format
|
||||
|
||||
#### logout(user_id: str, token_id: str) -> bool
|
||||
Invalidate a specific session/token.
|
||||
|
||||
**Parameters:**
|
||||
- `user_id`: User ID
|
||||
- `token_id`: JWT jti (token identifier)
|
||||
|
||||
**Returns:** True if successful
|
||||
|
||||
#### logout_all(user_id: str) -> int
|
||||
Invalidate all sessions for a user.
|
||||
|
||||
**Parameters:**
|
||||
- `user_id`: User ID
|
||||
|
||||
**Returns:** Number of sessions invalidated
|
||||
|
||||
#### refresh(refresh_token: str) -> AuthResult
|
||||
Get new access token from refresh token.
|
||||
|
||||
**Parameters:**
|
||||
- `refresh_token`: Valid refresh token
|
||||
|
||||
**Returns:** New AuthResult with access_token
|
||||
|
||||
**Raises:**
|
||||
- `InvalidTokenError`: Token expired or invalid
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
- `TokenService`: JWT generation/validation
|
||||
- `SessionStore`: Track active sessions
|
||||
- `UserRepository`: Fetch user data
|
||||
- `PasswordService`: Verify password (from F-003)
|
||||
|
||||
## Configuration
|
||||
```python
|
||||
LOGIN_RATE_LIMIT = 10 # attempts per window
|
||||
RATE_LIMIT_WINDOW = 900 # 15 minutes
|
||||
ACCOUNT_LOCKOUT_DURATION = 1800 # 30 minutes
|
||||
@@ -1,114 +0,0 @@
|
||||
# Component: PasswordService
|
||||
|
||||
## Responsabilidad
|
||||
Gestionar el cambio de contraseña de usuarios autenticados. Validar contraseña actual, verificar requisitos de seguridad de la nueva contraseña, y invalidar sesiones existentes.
|
||||
|
||||
## Tipo
|
||||
- [x] Microservicio
|
||||
- [ ] Library/Biblioteca
|
||||
- [ ] Shared Component
|
||||
- [ ] External Integration
|
||||
|
||||
## Interfaces
|
||||
|
||||
### API REST
|
||||
|
||||
```
|
||||
POST /api/v1/users/{user_id}/change-password
|
||||
Authorization: Bearer <token>
|
||||
Input: {
|
||||
"current_password": string,
|
||||
"new_password": string,
|
||||
"confirm_password": string
|
||||
}
|
||||
Output: { "success": true, "message": "Contraseña actualizada" }
|
||||
Errors:
|
||||
- 400: Validation errors (password too weak, mismatch)
|
||||
- 401: Current password incorrect
|
||||
- 403: Not owner
|
||||
- 404: User not found
|
||||
```
|
||||
|
||||
## Dependencias
|
||||
|
||||
| Servicio/Biblioteca | Tipo | Notas |
|
||||
|---------------------|------|-------|
|
||||
| PostgreSQL | Database | Almacenamiento de usuarios |
|
||||
| Redis | Cache | Invalidation de sesiones |
|
||||
| AuthService | Internal | Verificación de token |
|
||||
|
||||
## Validaciones de contraseña
|
||||
|
||||
| Regla | Requisito | Mensaje de error |
|
||||
|-------|-----------|------------------|
|
||||
| Longitud mínima | 8 caracteres | "La contraseña debe tener al menos 8 caracteres" |
|
||||
| Longitud máxima | 128 caracteres | "La contraseña debe tener máximo 128 caracteres" |
|
||||
| Mayúsculas | Al menos 1 | "La contraseña debe contener al menos una mayúscula" |
|
||||
| Minúsculas | Al menos 1 | "La contraseña debe contener al menos una minúscula" |
|
||||
| Números | Al menos 1 | "La contraseña debe contener al menos un número" |
|
||||
| Caracteres especiales | Al menos 1 | "La contraseña debe contener al menos un carácter especial (!@#$%^&*...)" |
|
||||
| No usar password anterior | Diferente | "La nueva contraseña no puede ser igual a la anterior" |
|
||||
|
||||
## Límites
|
||||
|
||||
### Alcance
|
||||
- ✅ Cambio de contraseña con validación
|
||||
- ✅ Requisitos de seguridad
|
||||
- ✅ Invalidación de sesiones
|
||||
- ❌ NO maneja recuperación de contraseña (ver ForgotPasswordService)
|
||||
- ❌ NO maneja reset forzado por admin (ver AdminService)
|
||||
|
||||
### Constraints
|
||||
- Rate limit: 5 intentos por hora por usuario
|
||||
- Timeout máximo: 1 segundo
|
||||
- Máximo 3 passwords válidas en historial (evitar reutilización inmediata)
|
||||
|
||||
## Criterios de éxito
|
||||
|
||||
| Criterio | Métrica | Target |
|
||||
|----------|---------|--------|
|
||||
| Disponibilidad | uptime | 99.9% |
|
||||
| Latencia | p99 change_password | < 500ms |
|
||||
| Rate limit | blocked attempts | 100% |
|
||||
| Sesiones invalidées | después de cambio | 100% |
|
||||
|
||||
## Diagrama
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Client] -->|POST /change-password| B[PasswordService]
|
||||
B -->|validate current| C[(PostgreSQL)]
|
||||
B -->|validate new| D[PasswordValidator]
|
||||
D -->|strong enough?| E{Valid}
|
||||
E -->|yes| F[Hash + Save]
|
||||
E -->|no| G[Return error]
|
||||
F -->|invalidate| H[(Redis)]
|
||||
H -->|remove sessions| I[All user tokens]
|
||||
```
|
||||
|
||||
## Estados
|
||||
|
||||
| Estado | Trigger | Acción |
|
||||
|--------|---------|--------|
|
||||
| Initial | started | Connect to DB |
|
||||
| Ready | db_connected | Accept requests |
|
||||
| RateLimited | >5 attempts/hour | Return 429 |
|
||||
| Error | db_failure | Return 503 |
|
||||
|
||||
## Seguridad
|
||||
|
||||
- **Password hashing**: bcrypt, cost 12 (nuevo), verificar contra hash existente
|
||||
- **Timing attack prevention**: usar constant-time comparison
|
||||
- **Rate limiting**: 5 req/hour por user_id
|
||||
- **Sesiones**: invalidar TODAS las sesiones del usuario tras cambio
|
||||
- **Logs**: NO registrar passwords, solo intentos fallidos (user_id anonymized)
|
||||
|
||||
## Observabilidad
|
||||
|
||||
- Metrics: `password_change_total`, `password_change_failed`, `password_change_latency`
|
||||
- Logs: structured JSON con request_id
|
||||
- Traces: OpenTelemetry span por request
|
||||
|
||||
## Tests BDD
|
||||
|
||||
- Ver `spec/bdd/features/password/change-password.feature`
|
||||
@@ -1,75 +0,0 @@
|
||||
# SessionStore Component
|
||||
|
||||
## Purpose
|
||||
Manage active user sessions in Redis for fast authentication and revocation.
|
||||
|
||||
## Public API
|
||||
|
||||
### Methods
|
||||
|
||||
#### create_session(user_id: str, token_id: str, metadata: dict) -> bool
|
||||
Store a new active session.
|
||||
|
||||
**Parameters:**
|
||||
- `user_id`: User identifier
|
||||
- `token_id`: JWT jti (unique token ID)
|
||||
- `metadata`: Optional data (IP, user agent, device)
|
||||
|
||||
**Returns:** True if created
|
||||
|
||||
#### get_session(token_id: str) -> Session | None
|
||||
Retrieve active session info.
|
||||
|
||||
**Parameters:**
|
||||
- `token_id`: JWT jti
|
||||
|
||||
**Returns:** Session object or None if expired/revoked
|
||||
|
||||
#### revoke_session(token_id: str) -> bool
|
||||
Invalidate a specific session.
|
||||
|
||||
**Parameters:**
|
||||
- `token_id`: JWT jti
|
||||
|
||||
**Returns:** True if revoked
|
||||
|
||||
#### revoke_all_user_sessions(user_id: str) -> int
|
||||
Invalidate all sessions for a user.
|
||||
|
||||
**Parameters:**
|
||||
- `user_id`: User identifier
|
||||
|
||||
**Returns:** Count of sessions revoked
|
||||
|
||||
#### get_user_session_count(user_id: str) -> int
|
||||
Count active sessions for a user.
|
||||
|
||||
**Parameters:**
|
||||
- `user_id`: User identifier
|
||||
|
||||
**Returns:** Number of active sessions
|
||||
|
||||
---
|
||||
|
||||
## Redis Keys Structure
|
||||
|
||||
```
|
||||
session:{user_id}:{token_id} -> JSON session metadata
|
||||
user_sessions:{user_id} -> SET of active token_ids
|
||||
rate_limit:login:{ip} -> COUNT with TTL
|
||||
```
|
||||
|
||||
## TTL
|
||||
- Session tokens: 15 minutes (synced with access token)
|
||||
- Rate limit counters: 15 minutes
|
||||
|
||||
## Dependencies
|
||||
- Redis connection (via aioredis)
|
||||
- TokenService (for token ID generation)
|
||||
|
||||
## Configuration
|
||||
```python
|
||||
SESSION_TTL = 900 # 15 minutes
|
||||
MAX_SESSIONS_PER_USER = 10
|
||||
RATE_LIMIT_WINDOW = 900 # 15 minutes
|
||||
```
|
||||
@@ -1,69 +0,0 @@
|
||||
# TokenService Component
|
||||
|
||||
## Purpose
|
||||
Generate, validate, and manage JWT tokens.
|
||||
|
||||
## Public API
|
||||
|
||||
### Methods
|
||||
|
||||
#### create_access_token(user: User) -> str
|
||||
Generate a new JWT access token.
|
||||
|
||||
**Parameters:**
|
||||
- `user`: User object with id, email, role
|
||||
|
||||
**Returns:** JWT token string
|
||||
|
||||
**Token claims:**
|
||||
```json
|
||||
{
|
||||
"sub": user.id,
|
||||
"email": user.email,
|
||||
"role": user.role,
|
||||
"iat": current_timestamp,
|
||||
"exp": current_timestamp + 900, # 15 min
|
||||
"jti": uuid4()
|
||||
}
|
||||
```
|
||||
|
||||
#### create_refresh_token(user: User) -> str
|
||||
Generate a new refresh token.
|
||||
|
||||
**Returns:** JWT refresh token (7 day expiration)
|
||||
|
||||
#### verify_token(token: str) -> TokenPayload
|
||||
Validate and decode a JWT token.
|
||||
|
||||
**Parameters:**
|
||||
- `token`: JWT token string
|
||||
|
||||
**Returns:** TokenPayload with claims
|
||||
|
||||
**Raises:**
|
||||
- `ExpiredSignatureError`: Token expired
|
||||
- `InvalidTokenError`: Token invalid/malformed
|
||||
|
||||
#### revoke_token(token_id: str, user_id: str) -> bool
|
||||
Mark a token as revoked in session store.
|
||||
|
||||
**Parameters:**
|
||||
- `token_id`: JWT jti claim
|
||||
- `user_id`: User ID
|
||||
|
||||
**Returns:** True if revoked
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
```python
|
||||
ACCESS_TOKEN_EXPIRE = 900 # 15 minutes
|
||||
REFRESH_TOKEN_EXPIRE = 604800 # 7 days
|
||||
ALGORITHM = "HS256" # or RS256 with key pair
|
||||
SECRET_KEY = os.getenv("JWT_SECRET")
|
||||
```
|
||||
|
||||
## Security
|
||||
- Tokens include unique `jti` claim for revocation tracking
|
||||
- Short access token duration minimizes theft window
|
||||
- Refresh tokens stored in Redis for fast revocation
|
||||
@@ -1,111 +0,0 @@
|
||||
# Component: UserProfileService
|
||||
|
||||
## Responsabilidad
|
||||
Gestionar el perfil de usuario: consulta, actualización de datos básicos (nombre, avatar) y preferencias (idioma).
|
||||
|
||||
## Tipo
|
||||
- [x] Microservicio
|
||||
- [ ] Library/Biblioteca
|
||||
- [ ] Shared Component
|
||||
- [ ] External Integration
|
||||
|
||||
## Interfaces
|
||||
|
||||
### API REST
|
||||
|
||||
```
|
||||
GET /api/v1/users/{user_id}/profile
|
||||
Authorization: Bearer <token>
|
||||
Output: {
|
||||
"id": string,
|
||||
"name": string,
|
||||
"avatar_url": string,
|
||||
"language": "en" | "es" | "fr" | "de",
|
||||
"created_at": ISO8601,
|
||||
"updated_at": ISO8601
|
||||
}
|
||||
Errors: 401 (unauthorized), 404 (user not found)
|
||||
|
||||
PUT /api/v1/users/{user_id}/profile
|
||||
Authorization: Bearer <token>
|
||||
Input: {
|
||||
"name": string (optional),
|
||||
"avatar_url": string (optional),
|
||||
"language": string (optional)
|
||||
}
|
||||
Output: { perfil actualizado }
|
||||
Errors: 400 (validation), 401, 403 (not owner), 404
|
||||
```
|
||||
|
||||
### Eventos (si aplica)
|
||||
- `profile.updated.v1` — publicado cuando perfil se actualiza
|
||||
|
||||
## Dependencias
|
||||
|
||||
| Servicio/Biblioteca | Tipo | Notas |
|
||||
|---------------------|------|-------|
|
||||
| PostgreSQL | Database | Datos de usuarios y perfiles |
|
||||
| Redis | Cache | Cache de perfil (TTL 5min) |
|
||||
| Storage Service | External | Almacenamiento de avatares |
|
||||
|
||||
## Límites
|
||||
|
||||
### Alcance
|
||||
- ✅ CRUD de perfil de usuario
|
||||
- ✅ Cambio de idioma
|
||||
- ❌ NO maneja autenticación (AuthService)
|
||||
- ❌ NO maneja permisos de otros usuarios
|
||||
|
||||
### Constraints
|
||||
- Timeout máximo: 300ms
|
||||
- Rate limit: 50 req/min por usuario
|
||||
- name: 2-50 caracteres, solo letras y espacios
|
||||
- avatar_url: max 500 caracteres, URL válida (http/https)
|
||||
- language: uno de ['en', 'es', 'fr', 'de']
|
||||
|
||||
## Criterios de éxito
|
||||
|
||||
| Criterio | Métrica | Target |
|
||||
|----------|---------|--------|
|
||||
| Disponibilidad | uptime | 99.9% |
|
||||
| Latencia | p99 get_profile | < 100ms |
|
||||
| Latencia | p99 update_profile | < 200ms |
|
||||
| Cache hit rate | | > 80% |
|
||||
|
||||
## Diagrama
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Client] -->|GET /profile| B[UserProfileService]
|
||||
B -->|cache| C[(Redis)]
|
||||
B -->|fetch| D[(PostgreSQL)]
|
||||
|
||||
E[Client] -->|PUT /profile| B
|
||||
B -->|validate| F[Storage]
|
||||
```
|
||||
|
||||
## Estados
|
||||
|
||||
| Estado | Trigger | Acción |
|
||||
|--------|---------|--------|
|
||||
| Initial | started | Connect to DB, Redis |
|
||||
| Ready | all_connected | Accept requests |
|
||||
| Degraded | redis_down | Fallback to DB-only |
|
||||
| Error | db_failure | Return 503 + alert |
|
||||
|
||||
## Seguridad
|
||||
|
||||
- Authentication: JWT Bearer token required
|
||||
- Authorization: Solo el dueño puede modificar su perfil
|
||||
- Input validation: Pydantic, sanitización XSS
|
||||
- Rate limiting: 50 req/min por user_id
|
||||
|
||||
## Observabilidad
|
||||
|
||||
- Metrics: `profile_get_total`, `profile_update_total`, `profile_latency_ms`
|
||||
- Logs: structured JSON con user_id (masked)
|
||||
- Traces: OpenTelemetry span por request
|
||||
|
||||
## Tests BDD
|
||||
|
||||
- Ver `spec/bdd/features/profile/user-profile.feature`
|
||||
0
spec/sdd/decisions/.gitkeep
Normal file
0
spec/sdd/decisions/.gitkeep
Normal file
@@ -1,48 +0,0 @@
|
||||
# ADR-XXX: Título de la Decisión
|
||||
|
||||
## Estado
|
||||
Aceptado | Propuesto | Deprecado
|
||||
|
||||
## Fecha
|
||||
YYYY-MM-DD
|
||||
|
||||
## Contexto
|
||||
_Descripción del problema o situación que motiva esta decisión._
|
||||
|
||||
## Decisión
|
||||
_Qué se decidió y por qué._
|
||||
|
||||
## Justificación
|
||||
_Razones que fundamentan la decisión._
|
||||
|
||||
## Consecuencias
|
||||
|
||||
### ✅ Positivas
|
||||
- ...
|
||||
|
||||
### ❌ Negativas
|
||||
- ...
|
||||
|
||||
### 🔄 Neutrales
|
||||
- ...
|
||||
|
||||
## Alternativas Consideradas
|
||||
|
||||
### Opción A
|
||||
- **Descripción**: ...
|
||||
- **Pros**: ...
|
||||
- **Contras**: ...
|
||||
- **Razón de descarte**: ...
|
||||
|
||||
### Opción B
|
||||
- **Descripción**: ...
|
||||
- **Pros**: ...
|
||||
- **Contras**: ...
|
||||
- **Razón de descarte**: ...
|
||||
|
||||
## Notas
|
||||
_Información adicional o follow-ups._
|
||||
|
||||
## Relacionado con
|
||||
- ADR-YYY
|
||||
- Feature F-XXX
|
||||
@@ -1,63 +0,0 @@
|
||||
# ADR-001: Selección de Stack Técnico
|
||||
|
||||
## Estado
|
||||
Aceptado
|
||||
|
||||
## Fecha
|
||||
2026-05-06
|
||||
|
||||
## Contexto
|
||||
Necesitamos seleccionar el stack tecnológico inicial para el proyecto. El equipo tiene experiencia en Python y JavaScript/TypeScript, y requiere:
|
||||
- Rápido bootstrap
|
||||
- Testing BDD nativo
|
||||
- Compatibilidad con el framework ARNES
|
||||
|
||||
## Decisión
|
||||
Usar **Python + Behave** para BDD y **FastAPI** para el backend.
|
||||
|
||||
## Justificación
|
||||
1. **Behave** tiene sintaxis Gherkin nativa y integración simple con Python
|
||||
2. **FastAPI** ofrece validación automática con Pydantic y tests con pytest
|
||||
3. Ambos tienen ecosistema maduro y documentación extensa
|
||||
4. Comunidad activa y soporte a largo plazo
|
||||
|
||||
## Consecuencias
|
||||
|
||||
### ✅ Positivas
|
||||
- Curva de aprendizaje baja (Python)
|
||||
- BDD nativo con Behave (Gherkin)
|
||||
- Type hints en todo el stack
|
||||
- FastAPI: auto-generated docs (Swagger/ReDoc)
|
||||
- Testing integrado con pytest
|
||||
|
||||
### ❌ Negativas
|
||||
- GIL限制了多线程性能 (puede mitigated with async)
|
||||
- Menos opciones de hosting que Node.js
|
||||
|
||||
### 🔄 Neutrales
|
||||
- Requiere Python 3.10+ mínimo
|
||||
|
||||
## Alternativas Consideradas
|
||||
|
||||
### Opción A: Node.js + Cucumber
|
||||
- **Pros**: Más opciones de hosting, JSON nativo, ecosistema npm enorme
|
||||
- **Contras**: TypeScript requiere más setup, testing E2E más complejo
|
||||
- **Razón de descarte**: Mayor complejidad inicial, menor familiaridad del equipo con TS
|
||||
|
||||
### Opción B: Java + Cucumber-JVM
|
||||
- **Pros**: Tipo estático, robusto, enterprise-grade
|
||||
- **Contras**: Verbose, setup pesado, curva de aprendizaje alta
|
||||
- **Razón de descarte**: Over-engineering para MVP
|
||||
|
||||
### Opción C: Go + Godog
|
||||
- **Pros**: Binarios estáticos, excelente performance
|
||||
- **Contras**: BDD tooling inmaduro, less ecosystem para testing
|
||||
- **Razón de descarte**: BDD ecosystem no maduro
|
||||
|
||||
## Notas
|
||||
- Re-evaluar si el proyecto escala a más de 50 servicios
|
||||
- Considerar microservices framework si es necesario
|
||||
|
||||
## Relacionado con
|
||||
- Feature F-001
|
||||
- Stack: Python 3.11+, FastAPI, Behave, PostgreSQL
|
||||
@@ -1,69 +0,0 @@
|
||||
# ADR-002: Almacenamiento de Avatares
|
||||
|
||||
## Estado
|
||||
Aceptado
|
||||
|
||||
## Fecha
|
||||
2026-05-06
|
||||
|
||||
## Contexto
|
||||
Los usuarios pueden subir avatares personalizados. Necesitamos decidir dónde y cómo almacenar las imágenes de perfil para optimizar costo, rendimiento y mantenimiento.
|
||||
|
||||
## Decisión
|
||||
Usar **Storage Service externo (S3-compatible)** con URLs firmadas para avatares.
|
||||
|
||||
## Justificación
|
||||
1. **Simplicidad**: No requerimos procesar imágenes en nuestro servidor
|
||||
2. **Costo**: S3-like storage es económico ($0.023/GB)
|
||||
3. **CDN**: Los avatares se sirven desde CDN automáticamente
|
||||
4. **Seguridad**: URLs firmadas con expiración evitan hotlinking
|
||||
5. **Mantenimiento**: No requiere gestión de sistema de archivos
|
||||
|
||||
## Consecuencias
|
||||
|
||||
### ✅ Positivas
|
||||
- No hay infraestructura de archivos que mantener
|
||||
- Escalabilidad automática
|
||||
- URLs firmadas = más seguridad
|
||||
- Cache CDN = mejor performance
|
||||
|
||||
### ❌ Negativas
|
||||
- Dependencia de proveedor externo
|
||||
- Costo de storage + egress
|
||||
- Latencia extra por redirect a CDN
|
||||
|
||||
### 🔄 Neutrales
|
||||
- Requiere configuración de CORS
|
||||
|
||||
## Alternativas Consideradas
|
||||
|
||||
### Opción A: Almacenamiento local en servidor
|
||||
- **Pros**: Sin dependencia externa, rápido para lecturas
|
||||
- **Contras**: No escala horizontalmente, requiere backup, problemas de disco
|
||||
- **Razón de descarte**: No escala bien con múltiples instancias
|
||||
|
||||
### Opción B: Base de datos como BLOB
|
||||
- **Pros**: Todo en un lugar, transacciones integradas
|
||||
- **Contras**: PostgreSQL no optimizado para archivos grandes, backup lento
|
||||
- **Razón de descarte**: degrada performance de DB, backups muy pesados
|
||||
|
||||
### Opción C: Servicio dedicado de imágenes (Cloudinary/Imgix)
|
||||
- **Pros**: Transformación de imágenes, CDN incluido, optimización automática
|
||||
- **Contras**: Más costoso ($50+/mes), vendor lock-in
|
||||
- **Razón de descarte**: Over-engineering para avatares simples
|
||||
|
||||
## Implementación
|
||||
|
||||
1. Cliente sube imagen a `/api/v1/profile/upload` (multipart)
|
||||
2. Servicio valida tipo (jpg/png/webp) y tamaño (<5MB)
|
||||
3. Servicio sube a S3 con nombre `avatars/{user_id}/{timestamp}.{ext}`
|
||||
4. Servicio genera URL firmada (7 días validez)
|
||||
5. URL se guarda en campo `avatar_url` del perfil
|
||||
|
||||
## Notas
|
||||
- Considerar WebP en el futuro para optimización
|
||||
- Implementar cleanup de avatares huérfanos (job semanal)
|
||||
|
||||
## Relacionado con
|
||||
- Feature F-002
|
||||
- Componente: UserProfileService
|
||||
@@ -1,83 +0,0 @@
|
||||
# ADR-003: Hashing de Contraseñas
|
||||
|
||||
## Estado
|
||||
Aceptado
|
||||
|
||||
## Fecha
|
||||
2026-05-06
|
||||
|
||||
## Contexto
|
||||
Necesitamos guardar contraseñas de usuarios de forma segura. La decisión debe considerar:
|
||||
- Resistencia a ataques de fuerza bruta y rainbow tables
|
||||
- Performance (se ejecuta en cada login y cambio de password)
|
||||
- Compatibilidad con estándares de la industria
|
||||
|
||||
## Decisión
|
||||
Usar **bcrypt** con cost factor 12 para hashing de contraseñas.
|
||||
|
||||
## Justificación
|
||||
1. **bcrypt** es diseñado específicamente para password hashing lento
|
||||
2. **Cost factor configurable**: permite aumentar resistencia en el futuro
|
||||
3. **Resistente a GPU/rainbow attacks**: diseñado para ser lento intencionalmente
|
||||
4. **Incorpora salt**: cada password tiene salt único, evitando rainbow tables
|
||||
5. **Estándar de industria**: ampliamente usado (Django, Rails, bcrypt)
|
||||
|
||||
## Consecuencias
|
||||
|
||||
### ✅ Positivas
|
||||
- Resistente a ataques de fuerza bruta
|
||||
- Salt automático evitar rainbow tables
|
||||
- Configurable (cost factor)
|
||||
- Librerías maduras en todos los lenguajes
|
||||
|
||||
### ❌ Negativas
|
||||
- Más lento que MD5/SHA (es el punto, pero afecta latency)
|
||||
- Enorme payload si se guarda en cookies/token
|
||||
|
||||
### 🔄 Neutrales
|
||||
- Requiere Python 3.11+ para bcrypt moderno
|
||||
|
||||
## Implementación
|
||||
|
||||
```python
|
||||
import bcrypt
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""Hash password with bcrypt, cost 12."""
|
||||
return bcrypt.hashpw(
|
||||
password.encode('utf-8'),
|
||||
bcrypt.gensalt(rounds=12)
|
||||
).decode('utf-8')
|
||||
|
||||
def verify_password(password: str, hashed: str) -> bool:
|
||||
"""Verify password using constant-time comparison."""
|
||||
return bcrypt.checkpw(
|
||||
password.encode('utf-8'),
|
||||
hashed.encode('utf-8')
|
||||
)
|
||||
```
|
||||
|
||||
## Alternativas Consideradas
|
||||
|
||||
### Opción A: SHA-256 (con salt)
|
||||
- **Pros**: Rápido, simple
|
||||
- **Contras**: No es lento, vulnerable a GPU attacks, diseñado para speed no security
|
||||
- **Razón de descarte**: No es resistente a hardware moderno
|
||||
|
||||
### Opción B: Argon2
|
||||
- **Pros**: Ganador PHC 2015, configurable memory/CPU
|
||||
- **Contras**: Más complejo de implementar, menos soporte de librerías
|
||||
- **Razón de descarte**: bcrypt es más simple y suficiente para nuestro caso de uso
|
||||
|
||||
### Opción C: scrypt
|
||||
- **Pros**: Diseñado para ser memory-hard
|
||||
- **Contras**: Más lento de configurar, configuración compleja
|
||||
- **Razón de descarte**: bcrypt es más simple y ampliamente soportado
|
||||
|
||||
## Notas
|
||||
- Si en el futuro,我们需要 mayor seguridad, migrar a Argon2
|
||||
- No guardar passwords en logs bajo ninguna circunstancia
|
||||
|
||||
## Relacionado con
|
||||
- Feature F-003
|
||||
- Componente: PasswordService
|
||||
@@ -1,68 +0,0 @@
|
||||
# ADR-004: JWT Authentication Strategy
|
||||
|
||||
## Status
|
||||
ACCEPTED
|
||||
|
||||
## Context
|
||||
We need a stateless authentication mechanism for the API that:
|
||||
1. Allows users to login with email/password
|
||||
2. Provides secure token-based sessions
|
||||
3. Supports token revocation (logout)
|
||||
4. Handles token refresh without re-login
|
||||
|
||||
## Decision
|
||||
|
||||
We will use **JWT (JSON Web Tokens)** with the following configuration:
|
||||
|
||||
### Token Structure
|
||||
- **Access Token**: 15 minute expiration, contains user identity
|
||||
- **Refresh Token**: 7 day expiration, used to obtain new access tokens
|
||||
|
||||
### Algorithm
|
||||
- **HS256** for signing (symmetric, simpler setup)
|
||||
- Secret key loaded from environment variable `JWT_SECRET`
|
||||
|
||||
### Claims
|
||||
```json
|
||||
{
|
||||
"sub": "user_uuid",
|
||||
"email": "user@example.com",
|
||||
"role": "user",
|
||||
"iat": 1715030400,
|
||||
"exp": 1715031300,
|
||||
"jti": "unique-token-id"
|
||||
}
|
||||
```
|
||||
|
||||
### Session Management
|
||||
- Active sessions tracked in **Redis** (keyed by `jti`)
|
||||
- Sessions invalidated on logout
|
||||
- All user sessions invalidated on password change (from F-003)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Stateless = horizontal scaling friendly
|
||||
- Short-lived access tokens limit damage if compromised
|
||||
- Refresh tokens allow long sessions without storing passwords
|
||||
- Redis-based session tracking enables instant revocation
|
||||
|
||||
### Negative
|
||||
- Cannot revoke individual refresh tokens (need blocklist)
|
||||
- Token size larger than session IDs
|
||||
- Clock sync required between services
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
| Alternative | Why Rejected |
|
||||
|-------------|--------------|
|
||||
| Session cookies | Not API-friendly, CSRF issues |
|
||||
| OAuth2/OIDC | Overkill for simple auth |
|
||||
| PASETO | Less battle-tested |
|
||||
| opaque tokens | Requires DB lookup on every request |
|
||||
|
||||
## Implementation Notes
|
||||
- JWT library: PyJWT
|
||||
- Redis client: aioredis for async
|
||||
- Both tokens stored in HttpOnly cookies for browser clients
|
||||
- Access token in Authorization header for API clients
|
||||
7
spec/sdd/decisions/README.md
Normal file
7
spec/sdd/decisions/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# SDD decisions
|
||||
|
||||
Put ADRs (Architecture Decision Records) here.
|
||||
|
||||
Example:
|
||||
- `001-use-flask.md`
|
||||
- `002-use-mariadb.md`
|
||||
Reference in New Issue
Block a user