Тестирование защищенных маршрутов

В TanStack Router маршруты можно защищать с помощью guard-функций, которые проверяют авторизацию пользователя или выполнение других условий перед переходом на страницу. Тестирование таких маршрутов требует имитации состояния приложения и проверки корректного поведения роутера в различных сценариях.

Определение защищённых маршрутов

Защищённый маршрут создаётся с использованием свойства beforeLoad или loader, где выполняется проверка условий:

import { createRouter, createRouteConfig } from '@tanstack/router'

const isAuthenticated = () => Boolean(localStorage.getItem('token'))

const protectedRoute = createRouteConfig()
  .path('/dashboard')
  .beforeLoad(() => {
    if (!isAuthenticated()) {
      throw new Error('Unauthorized')
    }
  })

Ключевые моменты:

  • beforeLoad вызывается до загрузки компонента, что позволяет перенаправлять пользователя или блокировать доступ.
  • Для тестирования важно эмулировать различные состояния (например, авторизован/не авторизован).

Стратегии тестирования

  1. Юнит-тестирование guard-функций Guard-функции можно тестировать отдельно, без монтирования всего приложения:
test('should block access if user is not authenticated', () => {
  localStorage.removeItem('token')
  expect(() => protectedRoute.beforeLoad()).toThrow('Unauthorized')
})

test('should allow access if user is authenticated', () => {
  localStorage.setItem('token', 'valid-token')
  expect(() => protectedRoute.beforeLoad()).not.toThrow()
})
  1. Интеграционное тестирование маршрутов При интеграционном подходе проверяется, как роутер реагирует на переходы между защищёнными и публичными маршрутами. Для этого используют React Testing Library вместе с TanStack Router Provider:
import { render, screen } from '@testing-library/react'
import { RouterProvider } from '@tanstack/router'
import { router } from './router'

test('redirects unauthorized user from dashboard', async () => {
  localStorage.removeItem('token')
  render(<RouterProvider router={router} />)
  
  router.navigate('/dashboard')
  
  expect(await screen.findByText('Login Page')).toBeInTheDocument()
})

Особенности:

  • Важно эмулировать навигацию через router.navigate.
  • Проверка должна учитывать редирект, а не только выброс ошибки.

Мокаем состояния пользователя

Для упрощения тестирования защищённых маршрутов удобно создавать mock-сервисы авторизации:

export const authService = {
  isAuthenticated: () => Boolean(localStorage.getItem('token')),
  login: (token) => localStorage.setItem('token', token),
  logout: () => localStorage.removeItem('token'),
}

Использование мока позволяет легко проверять все сценарии:

  • пользователь авторизован и видит защищённую страницу
  • пользователь не авторизован и перенаправляется на страницу входа

Асинхронные проверки

Если beforeLoad выполняет асинхронную проверку (например, запрос к API), тесты должны работать с промисами:

const asyncProtectedRoute = createRouteConfig()
  .path('/profile')
  .beforeLoad(async () => {
    const auth = await fetch('/api/auth/status').then(res => res.json())
    if (!auth.loggedIn) throw new Error('Unauthorized')
  })

test('blocks access if API returns unauthorized', async () => {
  global.fetch = jest.fn(() =>
    Promise.resolve({ json: () => Promise.resolve({ loggedIn: false }) })
  )

  await expect(asyncProtectedRoute.beforeLoad()).rejects.toThrow('Unauthorized')
})

Важно: использовать await и rejects.toThrow для корректной проверки асинхронных guard-функций.

Тестирование редиректов

TanStack Router позволяет редиректить пользователя через router.navigate в beforeLoad:

.beforeLoad(() => {
  if (!isAuthenticated()) {
    router.navigate('/login')
  }
})

При тестировании редиректов нужно:

  • следить, что текущий путь меняется на ожидаемый;
  • проверять, что компонент целевой страницы рендерится корректно:
test('redirects to login if not authenticated', async () => {
  localStorage.removeItem('token')
  render(<RouterProvider router={router} />)

  router.navigate('/dashboard')
  
  expect(router.state.location.pathname).toBe('/login')
})

Использование вспомогательных функций

Для сокращения повторяющегося кода удобно создавать utility-функции для тестирования защищённых маршрутов:

export function testProtectedRoute(routePath, isAuthExpected) {
  localStorage.removeItem('token')
  if (isAuthExpected) localStorage.setItem('token', 'token')
  
  render(<RouterProvider router={router} />)
  router.navigate(routePath)
  
  return router.state.location.pathname
}

// Пример использования
test('dashboard route', () => {
  expect(testProtectedRoute('/dashboard', false)).toBe('/login')
  expect(testProtectedRoute('/dashboard', true)).toBe('/dashboard')
})

Такой подход упрощает поддержание большого числа защищённых маршрутов и уменьшает дублирование тестов.

Выводы по практике тестирования

  • Тесты защищённых маршрутов должны покрывать все сценарии авторизации: авторизован/не авторизован, ошибки API, асинхронные проверки.
  • Guard-функции тестируются отдельно и вместе с интеграционными тестами роутера.
  • Использование mock-сервисов и вспомогательных функций упрощает поддержание тестовой базы.
  • Асинхронные guard-функции требуют корректной обработки промисов с await и rejects.toThrow.
  • Проверка редиректов — обязательная часть тестирования, так как ошибки на этом уровне часто незаметны при юнит-тестах компонентов.

Эта методика обеспечивает надёжное тестирование защищённых маршрутов в TanStack Router, позволяя гарантировать корректное управление доступом и стабильное поведение приложения.