docs(web): smoke test checklist UDT-002 — login, refresh, logout, reuse detection
This commit is contained in:
108
docs/smoke-test-udt-002.md
Normal file
108
docs/smoke-test-udt-002.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Smoke Test — UDT-002: Logout + Refresh Token
|
||||
|
||||
**Branch**: feature/UDT-002
|
||||
**Fecha**: 2026-04-14
|
||||
**Prerequisito**: backend corriendo en `http://localhost:5212`, BD `SIGCM2` con migración V002 aplicada.
|
||||
|
||||
---
|
||||
|
||||
## Escenario 1 — Login y persistencia de tokens
|
||||
|
||||
- [ ] Abrir la app en `http://localhost:5173`
|
||||
- [ ] Ingresar con credenciales válidas (admin / password)
|
||||
- [ ] Verificar que el login redirige al home
|
||||
- [ ] Abrir DevTools → Application → Local Storage → `auth-storage`
|
||||
- [ ] Confirmar que el objeto contiene: `accessToken`, `refreshToken`, `expiresAt`, `user`
|
||||
- [ ] Verificar que `expiresAt` es aproximadamente `Date.now() + 3600000` (1 hora)
|
||||
|
||||
---
|
||||
|
||||
## Escenario 2 — Refresh transparente en 401
|
||||
|
||||
**Opción A (esperar expiración natural — requiere token con TTL corto):**
|
||||
|
||||
- [ ] Modificar `Jwt:AccessTokenMinutes` a `1` en `appsettings.Development.json` y reiniciar el backend
|
||||
- [ ] Hacer login
|
||||
- [ ] Esperar 1 minuto para que el access token expire
|
||||
- [ ] Realizar cualquier request autenticado (ej: navegar a una sección que llame a la API)
|
||||
- [ ] Verificar que el request se completa sin error visible para el usuario
|
||||
- [ ] Verificar en DevTools → Network que hubo una llamada a `POST /api/v1/auth/refresh` seguida del request original reenviado con un nuevo Bearer
|
||||
|
||||
**Opción B (manipulación manual del token):**
|
||||
|
||||
- [ ] Después del login, abrir DevTools → Application → Local Storage → `auth-storage`
|
||||
- [ ] Editar el JSON y reemplazar `accessToken` con un valor inválido (ej: `"expired"`)
|
||||
- [ ] Realizar cualquier request autenticado
|
||||
- [ ] El interceptor de axiosClient recibe 401, llama a `/refresh` con el `refreshToken` real
|
||||
- [ ] El request original se reintenta automáticamente con el nuevo `accessToken`
|
||||
- [ ] El usuario no ve ningún error
|
||||
|
||||
---
|
||||
|
||||
## Escenario 3 — Refresh de 3 requests paralelos (singleton promise)
|
||||
|
||||
- [ ] Con el access token vencido (opción B del escenario 2)
|
||||
- [ ] Abrir una página que dispare múltiples llamadas API simultáneas
|
||||
- [ ] Verificar en DevTools → Network que hay exactamente **1** llamada a `POST /api/v1/auth/refresh`
|
||||
- [ ] Verificar que todos los requests subsiguientes retornan con éxito
|
||||
|
||||
---
|
||||
|
||||
## Escenario 4 — Logout
|
||||
|
||||
- [ ] Con sesión activa, hacer click en el botón de logout
|
||||
- [ ] Verificar que redirige a `/login`
|
||||
- [ ] Verificar en DevTools → Network que se llamó a `POST /api/v1/auth/logout`
|
||||
- [ ] Verificar en Local Storage que `auth-storage` tiene `user: null`, `accessToken: null`, `refreshToken: null`
|
||||
- [ ] Intentar navegar a una ruta protegida — debería redirigir a login
|
||||
|
||||
---
|
||||
|
||||
## Escenario 5 — Reuso de refresh token después del logout (reuse detection)
|
||||
|
||||
- [ ] Hacer login y copiar el valor de `refreshToken` del Local Storage
|
||||
- [ ] Hacer logout
|
||||
- [ ] Intentar llamar manualmente al endpoint de refresh con el token anterior:
|
||||
```bash
|
||||
curl -X POST http://localhost:5212/api/v1/auth/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"accessToken": "<access-anterior>", "refreshToken": "<refresh-anterior>"}'
|
||||
```
|
||||
- [ ] Verificar que el backend responde `401` con `{ "error": "invalid_token" }`
|
||||
- [ ] Verificar en la BD que todos los tokens de la familia fueron revocados:
|
||||
```sql
|
||||
SELECT * FROM dbo.RefreshToken WHERE RevokedAt IS NOT NULL ORDER BY Id DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Escenario 6 — Refresh token expirado (7 días)
|
||||
|
||||
- [ ] Modificar `ExpiresAt` de un token en la BD `SIGCM2_Test` a una fecha pasada
|
||||
- [ ] Intentar refresh con ese token — debería responder `401`
|
||||
- [ ] Verificar que el frontend redirige a `/login` y limpia el Local Storage
|
||||
|
||||
---
|
||||
|
||||
## Escenario 7 — Refresh con access token de otro usuario (mismatch)
|
||||
|
||||
- [ ] Crear dos usuarios en la BD (o usar admin + otro)
|
||||
- [ ] Hacer login con usuario A, guardar el `accessToken`
|
||||
- [ ] Hacer login con usuario B, guardar el `refreshToken`
|
||||
- [ ] Intentar refresh con accessToken de A + refreshToken de B:
|
||||
```bash
|
||||
curl -X POST http://localhost:5212/api/v1/auth/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"accessToken": "<access-usuario-A>", "refreshToken": "<refresh-usuario-B>"}'
|
||||
```
|
||||
- [ ] Verificar que el backend responde `401`
|
||||
|
||||
---
|
||||
|
||||
## Notas de verificación
|
||||
|
||||
| Check | Comando |
|
||||
|-------|---------|
|
||||
| Tokens en BD | `SELECT Id, UsuarioId, FamilyId, IssuedAt, ExpiresAt, RevokedAt FROM dbo.RefreshToken ORDER BY Id DESC` |
|
||||
| Familias revocadas | `SELECT FamilyId, COUNT(*) as Total, SUM(CASE WHEN RevokedAt IS NOT NULL THEN 1 ELSE 0 END) as Revoked FROM dbo.RefreshToken GROUP BY FamilyId` |
|
||||
| Usuario activo | `SELECT Id, Username, Activo FROM dbo.Usuario` |
|
||||
@@ -172,8 +172,8 @@ describe('authStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('legacy logout compatibility', () => {
|
||||
it('clears user and accessToken from state', async () => {
|
||||
describe('legacy logout compatibility (via clearAuth)', () => {
|
||||
it('clearAuth clears user and accessToken from state', () => {
|
||||
useAuthStore.getState().setAuth({
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin' },
|
||||
accessToken: 'some-token',
|
||||
@@ -181,14 +181,14 @@ describe('authStore', () => {
|
||||
expiresIn: 3600,
|
||||
})
|
||||
|
||||
await useAuthStore.getState().logout()
|
||||
useAuthStore.getState().clearAuth()
|
||||
|
||||
const state = useAuthStore.getState()
|
||||
expect(state.user).toBeNull()
|
||||
expect(state.accessToken).toBeNull()
|
||||
})
|
||||
|
||||
it('removes auth-storage from localStorage on logout', async () => {
|
||||
it('clearAuth removes auth-storage from localStorage', () => {
|
||||
useAuthStore.getState().setAuth({
|
||||
user: { id: 1, username: 'admin', nombre: 'Admin', rol: 'admin' },
|
||||
accessToken: 'some-token',
|
||||
@@ -196,7 +196,7 @@ describe('authStore', () => {
|
||||
expiresIn: 3600,
|
||||
})
|
||||
|
||||
await useAuthStore.getState().logout()
|
||||
useAuthStore.getState().clearAuth()
|
||||
|
||||
const stored = localStorage.getItem('auth-storage')
|
||||
if (stored !== null) {
|
||||
|
||||
Reference in New Issue
Block a user