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> {
|
export async function login(username: string, password: string): Promise<LoginResponseDto> {
|
||||||
const response = await axiosClient.post<LoginResponseDto>('/api/v1/auth/login', {
|
const response = await axiosClient.post<LoginResponseDto>('/api/v1/auth/login', {
|
||||||
username,
|
username,
|
||||||
@@ -19,3 +35,13 @@ export async function login(username: string, password: string): Promise<LoginRe
|
|||||||
})
|
})
|
||||||
return response.data
|
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 { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'
|
||||||
import { http, HttpResponse } from 'msw'
|
import { http, HttpResponse } from 'msw'
|
||||||
import { setupServer } from 'msw/node'
|
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'
|
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(
|
const server = setupServer(
|
||||||
http.post(`${API_URL}/api/v1/auth/login`, async ({ request }) => {
|
http.post(`${API_URL}/api/v1/auth/login`, async ({ request }) => {
|
||||||
const body = await request.json() as { username: string; password: string }
|
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 })
|
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' }))
|
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||||
@@ -45,3 +68,60 @@ describe('login()', () => {
|
|||||||
await expect(login('admin', 'wrongpassword')).rejects.toThrow()
|
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