Ctrl K
ComunidadGuías…

web/src/stores/auth.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { flushPromises } from '@vue/test-utils'

// Mock axios before importing the store
vi.mock('@/utils/axios', () => ({
  default: {
    get: vi.fn(),
    post: vi.fn(),
    put: vi.fn(),
    delete: vi.fn(),
    defaults: { headers: { common: {} } },
  },
}))

// Mock url utilities (used for profile_image URL resolution)
vi.mock('@/utils/url', () => ({
  getProfileImageUrl: vi.fn((path) => `http://localhost${path}`),
}))

import axios from '@/utils/axios'
import { useAuthStore } from './auth'

const mockUser = { id: 1, name: 'Test User', email: 'test@example.com', role: 'athlete' }
const mockToken = 'eyJhbGciOiJIUzI1NiJ9.test'
const mockRefreshToken = 'refresh-token-value'

describe('auth store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    localStorage.clear()
    delete axios.defaults.headers.common['Authorization']
    vi.clearAllMocks()

    // Suppress init() and fetchSchedulingEnabled() side effects that run on store creation
    axios.get.mockResolvedValue({ data: { scheduling_enabled: false, logo_variant: 'logo' } })
    axios.post.mockResolvedValue({})
  })

  afterEach(() => {
    localStorage.clear()
  })

  // =========================================================================
  // isAuthenticated computed
  // =========================================================================

  describe('isAuthenticated', () => {
    it('is false when no token or user is stored', async () => {
      const store = useAuthStore()
      await flushPromises()

      expect(store.isAuthenticated).toBe(false)
    })

    it('is true when both token and user are set', async () => {
      localStorage.setItem('token', mockToken)
      localStorage.setItem('user', JSON.stringify(mockUser))

      // fetchProfile is called by init()
      axios.get.mockImplementation((url) => {
        if (url === '/api/users/profile')
          return Promise.resolve({ data: { user: mockUser, is_coach: false } })
        return Promise.resolve({ data: { scheduling_enabled: false, logo_variant: 'logo' } })
      })

      const store = useAuthStore()
      await flushPromises()

      expect(store.isAuthenticated).toBe(true)
    })
  })

  // =========================================================================
  // isAdmin / isCoachRole
  // =========================================================================

  describe('role computed properties', () => {
    it('isAdmin is true when user role is admin', async () => {
      const store = useAuthStore()
      await flushPromises()

      store.user = { ...mockUser, role: 'admin' }
      expect(store.isAdmin).toBe(true)
    })

    it('isCoachRole is true when user role is coach', async () => {
      const store = useAuthStore()
      await flushPromises()

      store.user = { ...mockUser, role: 'coach' }
      expect(store.isCoachRole).toBe(true)
    })

    it('isCoachOrAdmin is true for both coach and admin', async () => {
      const store = useAuthStore()
      await flushPromises()

      store.user = { ...mockUser, role: 'admin' }
      expect(store.isCoachOrAdmin).toBe(true)

      store.user = { ...mockUser, role: 'coach' }
      expect(store.isCoachOrAdmin).toBe(true)

      store.user = { ...mockUser, role: 'athlete' }
      expect(store.isCoachOrAdmin).toBe(false)
    })
  })

  // =========================================================================
  // login()
  // =========================================================================

  describe('login()', () => {
    it('sets token and user on success, returns true', async () => {
      axios.get.mockResolvedValue({ data: { scheduling_enabled: false, logo_variant: 'logo' } })
      axios.post.mockImplementation((url) => {
        if (url === '/api/auth/login')
          return Promise.resolve({ data: { token: mockToken, user: mockUser } })
        return Promise.resolve({})
      })

      const store = useAuthStore()
      await flushPromises()

      // Reset after store init side effects
      axios.get.mockResolvedValue({ data: { user: mockUser, is_coach: false } })

      const result = await store.login('test@example.com', 'password')

      expect(result).toBe(true)
      expect(store.token).toBe(mockToken)
      expect(store.user).toEqual(mockUser)
    })

    it('stores token and user in localStorage on success', async () => {
      axios.post.mockImplementation((url) => {
        if (url === '/api/auth/login')
          return Promise.resolve({ data: { token: mockToken, user: mockUser } })
        return Promise.resolve({})
      })
      axios.get.mockResolvedValue({ data: { user: mockUser, is_coach: false } })

      const store = useAuthStore()
      await flushPromises()

      await store.login('test@example.com', 'password')

      expect(localStorage.getItem('token')).toBe(mockToken)
      expect(JSON.parse(localStorage.getItem('user'))).toEqual(mockUser)
    })

    it('stores refresh token in localStorage when provided', async () => {
      axios.post.mockImplementation((url) => {
        if (url === '/api/auth/login')
          return Promise.resolve({
            data: { token: mockToken, user: mockUser, refresh_token: mockRefreshToken }
          })
        return Promise.resolve({})
      })
      axios.get.mockResolvedValue({ data: { user: mockUser, is_coach: false } })

      const store = useAuthStore()
      await flushPromises()

      await store.login('test@example.com', 'password', true)

      expect(localStorage.getItem('refreshToken')).toBe(mockRefreshToken)
    })

    it('sets error message and returns false on 401', async () => {
      axios.post.mockImplementation((url) => {
        if (url === '/api/auth/login')
          return Promise.reject({
            response: { status: 401, data: { message: 'Invalid credentials' } }
          })
        return Promise.resolve({})
      })

      const store = useAuthStore()
      await flushPromises()

      const result = await store.login('bad@example.com', 'wrong')

      expect(result).toBe(false)
      expect(store.error).toBe('Invalid credentials')
    })

    it('sets network error message when server is unreachable', async () => {
      axios.post.mockImplementation((url) => {
        if (url === '/api/auth/login')
          return Promise.reject({ request: {}, message: 'Network Error' })
        return Promise.resolve({})
      })

      const store = useAuthStore()
      await flushPromises()

      const result = await store.login('test@example.com', 'password')

      expect(result).toBe(false)
      expect(store.error).toContain('Cannot connect')
    })

    it('sets loading false after login completes', async () => {
      axios.post.mockImplementation((url) => {
        if (url === '/api/auth/login')
          return Promise.resolve({ data: { token: mockToken, user: mockUser } })
        return Promise.resolve({})
      })
      axios.get.mockResolvedValue({ data: { user: mockUser, is_coach: false } })

      const store = useAuthStore()
      await flushPromises()

      const loginPromise = store.login('test@example.com', 'password')
      expect(store.loading).toBe(true)
      await loginPromise
      await flushPromises()

      expect(store.loading).toBe(false)
    })
  })

  // =========================================================================
  // logout()
  // =========================================================================

  describe('logout()', () => {
    it('clears token, user and refreshToken state', async () => {
      const store = useAuthStore()
      await flushPromises()

      store.token = mockToken
      store.user = mockUser
      store.refreshToken = null // no refresh token so we skip revocation

      await store.logout()

      expect(store.token).toBeNull()
      expect(store.user).toBeNull()
    })

    it('removes auth items from localStorage', async () => {
      localStorage.setItem('token', mockToken)
      localStorage.setItem('user', JSON.stringify(mockUser))
      localStorage.setItem('refreshToken', mockRefreshToken)

      const store = useAuthStore()
      await flushPromises()

      store.token = mockToken
      store.refreshToken = null

      await store.logout()

      expect(localStorage.getItem('token')).toBeNull()
      expect(localStorage.getItem('user')).toBeNull()
    })

    it('removes Authorization header from axios defaults', async () => {
      axios.defaults.headers.common['Authorization'] = `Bearer ${mockToken}`

      const store = useAuthStore()
      await flushPromises()

      store.token = mockToken
      store.refreshToken = null

      await store.logout()

      expect(axios.defaults.headers.common['Authorization']).toBeUndefined()
    })

    it('calls revoke endpoint when refresh token exists', async () => {
      const store = useAuthStore()
      await flushPromises()

      store.token = mockToken
      store.refreshToken = mockRefreshToken
      axios.post.mockResolvedValue({})

      await store.logout()

      expect(axios.post).toHaveBeenCalledWith('/api/auth/revoke', {
        refresh_token: mockRefreshToken,
      })
    })
  })

  // =========================================================================
  // refreshAccessToken()
  // =========================================================================

  describe('refreshAccessToken()', () => {
    it('returns false when no refresh token', async () => {
      const store = useAuthStore()
      await flushPromises()

      store.refreshToken = null

      const result = await store.refreshAccessToken()

      expect(result).toBe(false)
    })

    it('updates token and returns true on success', async () => {
      const newToken = 'new-access-token'
      axios.post.mockImplementation((url) => {
        if (url === '/api/auth/refresh')
          return Promise.resolve({ data: { token: newToken, user: mockUser } })
        return Promise.resolve({})
      })
      axios.get.mockResolvedValue({ data: { scheduling_enabled: false, logo_variant: 'logo' } })

      const store = useAuthStore()
      await flushPromises()

      store.refreshToken = mockRefreshToken

      const result = await store.refreshAccessToken()

      expect(result).toBe(true)
      expect(store.token).toBe(newToken)
      expect(localStorage.getItem('token')).toBe(newToken)
    })

    it('calls logout and returns false when refresh fails', async () => {
      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
      axios.post.mockImplementation((url) => {
        if (url === '/api/auth/refresh')
          return Promise.reject(new Error('Refresh failed'))
        return Promise.resolve({})
      })
      axios.get.mockResolvedValue({ data: { scheduling_enabled: false, logo_variant: 'logo' } })

      const store = useAuthStore()
      await flushPromises()

      store.token = mockToken
      store.refreshToken = mockRefreshToken
      store.user = mockUser

      const result = await store.refreshAccessToken()

      expect(result).toBe(false)
      expect(store.token).toBeNull() // logout was called
      consoleSpy.mockRestore()
    })
  })

  // =========================================================================
  // logoPath computed
  // =========================================================================

  describe('logoPath', () => {
    it('returns /logo.svg for default variant', async () => {
      const store = useAuthStore()
      await flushPromises()

      store.logoVariant = 'logo'
      expect(store.logoPath).toBe('/logo.svg')
    })

    it('returns /beta/logo.svg for betalogo variant', async () => {
      const store = useAuthStore()
      await flushPromises()

      store.logoVariant = 'betalogo'
      expect(store.logoPath).toBe('/beta/logo.svg')
    })
  })
})