UDT-002: Logout + Refresh Token con rotación y chain revocation #3
@@ -12,6 +12,22 @@ export interface LoginResponseDto {
|
||||
}
|
||||
}
|
||||
|
||||
export interface RefreshRequest {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
export interface RefreshResponse {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expiresIn: number
|
||||
}
|
||||
|
||||
export interface LogoutResponse {
|
||||
success: boolean
|
||||
mensaje: string
|
||||
}
|
||||
|
||||
export async function login(username: string, password: string): Promise<LoginResponseDto> {
|
||||
const response = await axiosClient.post<LoginResponseDto>('/api/v1/auth/login', {
|
||||
username,
|
||||
@@ -19,3 +35,13 @@ export async function login(username: string, password: string): Promise<LoginRe
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function refresh(payload: RefreshRequest): Promise<RefreshResponse> {
|
||||
const response = await axiosClient.post<RefreshResponse>('/api/v1/auth/refresh', payload)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function logout(): Promise<LogoutResponse> {
|
||||
const response = await axiosClient.post<LogoutResponse>('/api/v1/auth/logout', {})
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { login } from '../../../features/auth/api/authApi'
|
||||
import { login, refresh, logout } from '../../../features/auth/api/authApi'
|
||||
|
||||
const API_URL = 'http://localhost:5000'
|
||||
|
||||
@@ -17,6 +17,17 @@ const mockLoginResponse = {
|
||||
},
|
||||
}
|
||||
|
||||
const mockRefreshResponse = {
|
||||
accessToken: 'eyJhbGciOiJSUzI1NiJ9.new-payload.new-sig',
|
||||
refreshToken: 'new-opaque-refresh-token-xyz',
|
||||
expiresIn: 3600,
|
||||
}
|
||||
|
||||
const mockLogoutResponse = {
|
||||
success: true,
|
||||
mensaje: 'Sesion cerrada correctamente',
|
||||
}
|
||||
|
||||
const server = setupServer(
|
||||
http.post(`${API_URL}/api/v1/auth/login`, async ({ request }) => {
|
||||
const body = await request.json() as { username: string; password: string }
|
||||
@@ -25,6 +36,18 @@ const server = setupServer(
|
||||
}
|
||||
return HttpResponse.json({ error: 'Credenciales inválidas' }, { status: 401 })
|
||||
}),
|
||||
|
||||
http.post(`${API_URL}/api/v1/auth/refresh`, async ({ request }) => {
|
||||
const body = await request.json() as { accessToken: string; refreshToken: string }
|
||||
if (body.accessToken && body.refreshToken) {
|
||||
return HttpResponse.json(mockRefreshResponse, { status: 200 })
|
||||
}
|
||||
return HttpResponse.json({ error: 'invalid_token' }, { status: 401 })
|
||||
}),
|
||||
|
||||
http.post(`${API_URL}/api/v1/auth/logout`, async () => {
|
||||
return HttpResponse.json(mockLogoutResponse, { status: 200 })
|
||||
}),
|
||||
)
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
@@ -45,3 +68,60 @@ describe('login()', () => {
|
||||
await expect(login('admin', 'wrongpassword')).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('refresh()', () => {
|
||||
it('refresh_callsCorrectEndpoint_withPayload', async () => {
|
||||
let capturedBody: unknown = null
|
||||
server.use(
|
||||
http.post(`${API_URL}/api/v1/auth/refresh`, async ({ request }) => {
|
||||
capturedBody = await request.json()
|
||||
return HttpResponse.json(mockRefreshResponse, { status: 200 })
|
||||
}),
|
||||
)
|
||||
|
||||
const payload = {
|
||||
accessToken: 'old-access-token',
|
||||
refreshToken: 'old-refresh-token',
|
||||
}
|
||||
const result = await refresh(payload)
|
||||
|
||||
expect(result.accessToken).toBe(mockRefreshResponse.accessToken)
|
||||
expect(result.refreshToken).toBe(mockRefreshResponse.refreshToken)
|
||||
expect(result.expiresIn).toBe(3600)
|
||||
expect(capturedBody).toEqual(payload)
|
||||
})
|
||||
|
||||
it('throws on invalid refresh token (401)', async () => {
|
||||
server.use(
|
||||
http.post(`${API_URL}/api/v1/auth/refresh`, () => {
|
||||
return HttpResponse.json({ error: 'invalid_token' }, { status: 401 })
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(
|
||||
refresh({ accessToken: 'bad-access', refreshToken: 'bad-refresh' }),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('logout()', () => {
|
||||
it('logout_callsCorrectEndpoint', async () => {
|
||||
let requestUrl: string | null = null
|
||||
let requestMethod: string | null = null
|
||||
|
||||
server.use(
|
||||
http.post(`${API_URL}/api/v1/auth/logout`, ({ request }) => {
|
||||
requestUrl = request.url
|
||||
requestMethod = request.method
|
||||
return HttpResponse.json(mockLogoutResponse, { status: 200 })
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await logout()
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.mensaje).toBe('Sesion cerrada correctamente')
|
||||
expect(requestUrl).toContain('/api/v1/auth/logout')
|
||||
expect(requestMethod).toBe('POST')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user