Redirect to login/ when refresh token is invalid #6
@@ -3,14 +3,16 @@ import { setActivePinia, createPinia } from 'pinia'
|
||||
import { fetchWithAuth, apiClient } from '../client'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import * as authApi from '@/api/auth'
|
||||
import router from '@/router'
|
||||
|
||||
describe('api client (fetchWithAuth)', () => {
|
||||
const originalFetch = global.fetch
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
setActivePinia(createPinia())
|
||||
localStorage.clear()
|
||||
vi.restoreAllMocks()
|
||||
await router.push('/')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -105,6 +107,50 @@ describe('api client (fetchWithAuth)', () => {
|
||||
expect(res).toBe(secondResponse)
|
||||
})
|
||||
|
||||
it('redirects to login when a 401 occurs and no refresh token is available', async () => {
|
||||
const authStore = useAuthStore()
|
||||
authStore.setTokens({
|
||||
access_token: 'expired-token',
|
||||
refresh_token: 'valid-refresh-token',
|
||||
})
|
||||
authStore.clearTokens()
|
||||
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
await fetchWithAuth('/lists')
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
expect(router.currentRoute.value.name).toBe('login')
|
||||
})
|
||||
|
||||
it('redirects to login when refreshing the token fails', async () => {
|
||||
const authStore = useAuthStore()
|
||||
authStore.setTokens({
|
||||
access_token: 'expired-token',
|
||||
refresh_token: 'expired-refresh-token',
|
||||
})
|
||||
|
||||
vi.spyOn(authApi, 'refreshApi').mockRejectedValueOnce(new Error('Refresh token expired'))
|
||||
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
} as unknown as Response)
|
||||
global.fetch = fetchMock
|
||||
|
||||
await fetchWithAuth('/lists')
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
expect(authStore.accessToken).toBeNull()
|
||||
expect(router.currentRoute.value.name).toBe('login')
|
||||
})
|
||||
|
||||
it('calls apiClient helper methods correctly', async () => {
|
||||
const authStore = useAuthStore()
|
||||
authStore.setTokens({
|
||||
|
||||
+16
-1
@@ -1,8 +1,17 @@
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { API_BASE_URL } from '@/api/auth'
|
||||
import router from '@/router'
|
||||
|
||||
let refreshPromise: Promise<unknown> | null = null
|
||||
|
||||
async function redirectToLogin(): Promise<void> {
|
||||
const currentRoute = router.currentRoute.value
|
||||
if (currentRoute.name === 'login') {
|
||||
return
|
||||
}
|
||||
await router.push({ name: 'login', query: { redirect: currentRoute.fullPath } })
|
||||
}
|
||||
|
||||
export interface FetchOptions extends RequestInit {
|
||||
skipAuth?: boolean
|
||||
skipRefresh?: boolean
|
||||
@@ -43,7 +52,12 @@ export async function fetchWithAuth(
|
||||
headers: buildHeaders(customOptions, skipAuth ? null : authStore.accessToken),
|
||||
})
|
||||
|
||||
if (response.status === 401 && !skipRefresh && authStore.refreshToken) {
|
||||
if (response.status === 401 && !skipRefresh) {
|
||||
if (!authStore.refreshToken) {
|
||||
await redirectToLogin()
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
if (!refreshPromise) {
|
||||
refreshPromise = authStore.refreshTokens().finally(() => {
|
||||
@@ -57,6 +71,7 @@ export async function fetchWithAuth(
|
||||
headers: buildHeaders(customOptions, authStore.accessToken),
|
||||
})
|
||||
} catch {
|
||||
await redirectToLogin()
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,15 @@ import type { LocalList } from '@/database/db'
|
||||
|
||||
const pushMock = vi.fn<(to: string) => void>()
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: pushMock,
|
||||
}),
|
||||
}))
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('vue-router')>()
|
||||
return {
|
||||
...actual,
|
||||
useRouter: () => ({
|
||||
push: pushMock,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('ListDetailView', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -5,14 +5,18 @@ import LoginView from '../LoginView.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const mockPush = vi.fn<(to: string) => void>()
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
useRoute: () => ({
|
||||
query: {},
|
||||
}),
|
||||
}))
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('vue-router')>()
|
||||
return {
|
||||
...actual,
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
useRoute: () => ({
|
||||
query: {},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('LoginView', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user