• Fri, Mar 2026

Write unit/integration tests for auth flows (mock tokens & API responses)

Write unit/integration tests for auth flows (mock tokens & API responses)

Authentication flows are the backbone of many single-page applications. When they break, users get locked out and panic spreads. Testing these flows—login, token refresh, logout, and edge cases like token reuse—keeps your app resilient.

This tutorial teaches you how to write unit and integration tests for auth flows in Vue apps using Jest, Vue Test Utils, and mocking strategies (mock tokens, axios mocks, fake backend handlers). I’ll give you copy-pasteable tests and explain design decisions so you can adapt to your stack (Pinia/Vuex, axios, fetch, Express).

What you will learn

  • How to structure tests for auth flows (unit vs integration).
  • How to mock axios/HTTP calls and JWT tokens safely.
  • Testing access token expiry + refresh flow with axios interceptor.
  • Testing logout and refresh token revocation behavior.
  • Simulating token reuse and ensuring your app reacts properly.
  • Example code for a real-world auth store and test suite.

Assumptions & prerequisites

  • Vue 3 project (Vite or Vue CLI) with Jest set up.
  • Jest, @vue/test-utils, and axios in your devDependencies.
  • Familiarity with Pinia or Vuex (examples use a simple store pattern—adaptable).

High-level testing strategy

Break testing into two levels:

  • Unit tests — test isolated pieces (auth store actions, helper utilities, token parsing). Use mocks for HTTP calls. Fast and focused.
  • Integration tests — mount components or a small app and test end-to-end behavior including axios interceptor, refresh flow, and UI updates. Use HTTP-level mocking (axios mock adapter or MSW) to simulate server responses.

Project skeleton & minimal auth code

To keep examples concrete, here’s a minimal auth store and axios client that our tests will exercise. If you already use Pinia/Vuex, adapt the logic accordingly.

File: src/services/api.js (axios client with refresh interceptor)

// src/services/api.js
import axios from 'axios'
import { authStore } from '../stores/auth' // adjust to your store system

const api = axios.create({ baseURL: '/api', withCredentials: true })

let isRefreshing = false
let failedQueue = []

function processQueue(error, token = null) {
  failedQueue.forEach(p => {
    if (error) p.reject(error)
    else p.resolve(token)
  })
  failedQueue = []
}

api.interceptors.response.use(
  r => r,
  async err => {
    const originalRequest = err.config
    if (err.response && err.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true
      const store = authStore()
      if (isRefreshing) {
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject })
        }).then(token => {
          originalRequest.headers.Authorization = `Bearer ${token}`
          return api(originalRequest)
        })
      }
      isRefreshing = true
      try {
        const { data } = await axios.post('/api/auth/refresh', {}, { withCredentials: true })
        store.setAccessToken(data.accessToken)
        processQueue(null, data.accessToken)
        originalRequest.headers.Authorization = `Bearer ${data.accessToken}`
        return api(originalRequest)
      } catch (e) {
        processQueue(e, null)
        store.logout()
        return Promise.reject(e)
      } finally {
        isRefreshing = false
      }
    }
    return Promise.reject(err)
  }
)

export default api

File: src/stores/auth.js (simple store-ish module)

// src/stores/auth.js
import { ref } from 'vue'

export function authStore() {
  // in-memory access token for security in examples
  const accessToken = ref(null)
  const user = ref(null)
  const setAccessToken = token => { accessToken.value = token }
  const setUser = u => { user.value = u }
  const logout = () => { accessToken.value = null; user.value = null }
  return { accessToken, user, setAccessToken, setUser, logout }
}

The tests will mock axios network responses and validate that the store responds accordingly (tokens set, logout called, etc.). You can replace authStore() with a Pinia store and the same tests apply with small adaptions.

Tools & libraries for mocking

  • jest.mock() — quick way to mock axios module behavior.
  • axios-mock-adapter — useful for mocking axios requests at HTTP-level.
  • msw (Mock Service Worker) — excellent for integration tests that simulate server behavior at network layer; works both on Node and the browser.
  • vue-test-utils — for mounting components and integration tests in DOM.

Unit tests: testing store logic with mocked axios

We will write unit tests that:

  1. Test successful login sets access token and user.
  2. Test failed login yields error and doesn’t set token.
  3. Test refresh flow upon 401: when access token is expired, interceptor calls refresh and original request is retried.
  4. Test logout clears store when refresh fails.

Install test deps

npm install --save-dev jest @vue/test-utils axios-mock-adapter

Unit test file: tests/unit/auth.store.spec.js

// tests/unit/auth.store.spec.js
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
import api from '@/services/api'
import { authStore } from '@/stores/auth'

jest.mock('@/stores/auth', () => {
  // return a fresh store per import to isolate tests
  return {
    authStore: jest.fn(() => {
      const { ref } = require('vue')
      const accessToken = ref(null)
      const user = ref(null)
      const setAccessToken = token => { accessToken.value = token }
      const setUser = u => { user.value = u }
      const logout = jest.fn(() => { accessToken.value = null; user.value = null })
      return { accessToken, user, setAccessToken, setUser, logout }
    })
  }
})

describe('Auth flows (unit)', () => {
  let mock
  beforeEach(() => {
    mock = new MockAdapter(axios)
  })
  afterEach(() => {
    mock.restore()
    jest.resetModules()
    jest.clearAllMocks()
  })

  it('login sets token and user', async () => {
    // simulate login endpoint
    mock.onPost('/api/auth/login').reply(200, { accessToken: 'at-123', user: { id: 'u1', email: 'a@a' } })

    const store = authStore()
    // login implementation (example) calling axios
    const res = await axios.post('/api/auth/login', { email: 'a', password: 'p' })
    store.setAccessToken(res.data.accessToken)
    store.setUser(res.data.user)

    expect(store.accessToken.value).toBe('at-123')
    expect(store.user.value).toEqual({ id: 'u1', email: 'a@a' })
  })

  it('refresh flow retries failed request and sets new token', async () => {
    // Simulate: first request to /api/protected returns 401,
    // refresh returns new accessToken, and original endpoint succeeds when retried.

    // axios.post('/api/auth/refresh') -> returns new access token
    mock.onPost('/api/auth/refresh').reply(200, { accessToken: 'new-token' })

    // Protected endpoint: first call returns 401, second call returns 200
    let callCount = 0
    mock.onGet('/api/protected').reply(() => {
      callCount++
      if (callCount === 1) return [401]
      return [200, { data: 'ok' }]
    })

    const store = authStore()
    // initial token expired
    store.setAccessToken('expired-token')
    // perform request using api client which has interceptor
    const response = await api.get('/api/protected').then(r => r.data)
    // after interceptor, store accessToken should be updated via the refresh response
    expect(store.accessToken.value).toBe('new-token')
    expect(response).toEqual({ data: 'ok' })
  })

  it('failed refresh triggers logout', async () => {
    // Make refresh return 401 => logout called
    mock.onPost('/api/auth/refresh').reply(401)

    // Protected endpoint returns 401
    mock.onGet('/api/protected2').reply(401)

    const store = authStore()
    store.setAccessToken('expired-token')

    await expect(api.get('/api/protected2')).rejects.toBeTruthy()
    // logout should have been called by interceptor on refresh failure
    expect(store.logout).toHaveBeenCalled()
  })
})

Notes on unit tests:

  • We used axios-mock-adapter to intercept axios calls made by the client under test.
  • We mocked the store module to return isolated instances so tests don't share state across runs.
  • We exercise the refresh interceptor behavior by forcing a 401 then responding to the /auth/refresh call.

Integration tests: mount components & simulate user flows

Integration tests validate how components, store, and HTTP clients work together. We'll use @vue/test-utils to mount a login component and MSW (mock service worker) to simulate a backend that supports rotation behavior.

Why MSW?

MSW intercepts network calls at the networking layer. It's excellent for integration tests because handlers can maintain state (simulate DB/Redis) and you can test sequential scenarios like login & subsequent refresh attempts.

Install MSW and test utils

npm install --save-dev msw @vue/test-utils @testing-library/jest-dom

Example server handlers (tests/setup/msw.js)

// tests/setup/msw.js
const { setupServer } = require('msw/node')
const { rest } = require('msw')
const jwt = require('jsonwebtoken')

// in-memory "refresh store"
const refreshStore = new Map()

function signAccess(uid) {
  return jwt.sign({ uid }, 'access-secret', { expiresIn: '1m' })
}
function signRefresh(sid, uid) {
  return jwt.sign({ sid, uid }, 'refresh-secret', { expiresIn: '7d' })
}

const server = setupServer(
  rest.post('/api/auth/login', (req, res, ctx) => {
    const { email } = req.body
    const uid = email === 'admin@example.com' ? 'admin' : 'user'
    const access = signAccess(uid)
    const sid = Math.random().toString(36).slice(2)
    const refresh = signRefresh(sid, uid)
    refreshStore.set(sid, { uid, hash: refresh })
    // set cookie simulation by returning refresh in body (for test convenience)
    return res(ctx.status(200), ctx.json({ accessToken: access, refreshToken: refresh, user: { id: uid } }))
  }),

  rest.post('/api/auth/refresh', (req, res, ctx) => {
    const token = req.cookies?.refreshToken || req.body?.refreshToken || req.headers.get('authorization')?.split(' ')[1]
    if (!token) return res(ctx.status(401))
    try {
      const payload = jwt.verify(token, 'refresh-secret')
      const stored = refreshStore.get(payload.sid)
      if (!stored || stored.hash !== token) {
        // reuse detection
        refreshStore.clear() // revoke all
        return res(ctx.status(401), ctx.json({ error: 'reuse' }))
      }
      // rotate: create new sid/token
      const newSid = Math.random().toString(36).slice(2)
      const newRefresh = signRefresh(newSid, payload.uid)
      refreshStore.delete(payload.sid)
      refreshStore.set(newSid, { uid: payload.uid, hash: newRefresh })
      const newAccess = signAccess(payload.uid)
      return res(ctx.status(200), ctx.json({ accessToken: newAccess, refreshToken: newRefresh }))
    } catch (err) {
      return res(ctx.status(401))
    }
  }),

  rest.get('/api/protected', (req, res, ctx) => {
    const auth = req.headers.get('authorization')
    if (!auth) return res(ctx.status(401))
    const token = auth.replace('Bearer ', '')
    try {
      jwt.verify(token, 'access-secret')
      return res(ctx.status(200), ctx.json({ data: 'protected' }))
    } catch (e) {
      return res(ctx.status(401))
    }
  })
)

module.exports = { server, rest }

Integration test: tests/integration/auth.flow.spec.js

// tests/integration/auth.flow.spec.js
import { mount } from '@vue/test-utils'
import { server } from './setup/msw'
import { rest } from 'msw'
import api from '@/services/api'
import { authStore } from '@/stores/auth'

// start/stop MSW server
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

jest.mock('@/stores/auth', () => {
  return {
    authStore: jest.fn(() => {
      const { ref } = require('vue')
      const accessToken = ref(null)
      const user = ref(null)
      const setAccessToken = token => { accessToken.value = token }
      const setUser = u => { user.value = u }
      const logout = jest.fn(() => { accessToken.value = null; user.value = null })
      return { accessToken, user, setAccessToken, setUser, logout }
    })
  }
})

describe('Integration: login -> access protected -> refresh rotation', () => {
  it('login returns tokens and protected endpoint is accessible, and refresh rotates tokens', async () => {
    // Simulate login by calling login endpoint
    const res = await api.post('/api/auth/login', { email: 'user@example.com', password: 'x' })
    const store = authStore()
    store.setAccessToken(res.data.accessToken)
    store.setUser(res.data.user)

    // Access protected endpoint -> should succeed with access token
    api.defaults.headers.common['Authorization'] = `Bearer ${store.accessToken.value}`
    const protectedResp = await api.get('/api/protected')
    expect(protectedResp.data).toEqual({ data: 'protected' })

    // Simulate server rotating refresh token when refresh endpoint called
    // Call refresh to get new access token
    const refreshResp = await api.post('/api/auth/refresh', { refreshToken: res.data.refreshToken })
    // store should update token
    store.setAccessToken(refreshResp.data.accessToken)
    expect(store.accessToken.value).toBe(refreshResp.data.accessToken)

    // If we try to use old refresh token again, server will detect reuse and revoke
    const oldRefresh = res.data.refreshToken
    const reuseCall = await api.post('/api/auth/refresh', { refreshToken: oldRefresh }).catch(e => e.response)
    expect(reuseCall.status).toBe(401)
  })
})

Integration notes:

  • MSW simulates server-side refresh token storage and rotation; tests call the real axios client so interceptors are exercised.
  • For convenience in tests we pass refreshToken in response bodies; production should use HttpOnly cookies—MSW can simulate cookies if needed.
  • This integration test validates happy path and a reuse detection case.

Testing edge cases & attack scenarios

Design tests that simulate attacks and failure modes so your app is resilient:

  • Replay attack: call refresh with an old/rotated token and expect global logout and revocation behaviors.
  • Concurrent refresh: simulate two simultaneous requests where interceptor queues requests until refresh completes—verify queue is processed and only one refresh call is made.
  • Network failures: simulate refresh endpoint returning 500 and check app either retries (if intended) or logs out safely.
  • Expired access token: simulate access token expiry and ensure interceptor tries refresh and retries original request.

Practical tips for writing reliable tests

  • Isolate state: Ensure each test uses fresh store instances or re-initializes state between tests.
  • Prefer integration tests for flows: Unit tests are fast, but real flow behaviors (interceptors, retries) should be covered by integration tests.
  • Keep tests readable: Use helper functions to perform login, set tokens, and perform protected requests.
  • Mock time where needed: If you test token expiry based on time, use jest.useFakeTimers() and advance timers.
  • Avoid coupling to implementation: Test behaviors and outcomes (store updated, logout invoked), not internal private functions.

Full example repository structure (recommended)


/src
  /services
    api.js
  /stores
    auth.js
/tests
  /unit
    auth.store.spec.js
  /integration
    auth.flow.spec.js
  /setup
    msw.js
jest.config.js
package.json
    

Summary & checklist

To effectively test auth flows in your Vue app:

  1. Write unit tests for store logic and helpers, mocking HTTP calls.
  2. Write integration tests for full flows: login, call protected resource, refresh, token rotation, and logout.
  3. Use axios-mock-adapter or MSW for realistic HTTP mocking; prefer MSW for integration scenarios.
  4. Simulate edge cases: concurrent refreshes, reuse detection, network failures, expiry.
  5. Keep tests isolated and assert behavior (store state, logout calls, UI changes) rather than implementation details.
This website uses cookies to enhance your browsing experience. By continuing to use this site, you consent to the use of cookies. Please review our Privacy Policy for more information on how we handle your data. Cookie Policy