wraith/frontend/tests/stores/auth.store.spec.ts
Vantz Stockwell f01e357647 test: frontend test suite — Vitest infrastructure, auth/connection stores, vault composable, admin middleware
28 tests across 4 spec files. Vitest + happy-dom configured with Nuxt auto-import
shims ($$fetch, navigateTo, defineNuxtRouteMiddleware) so stores and composables
resolve cleanly outside the Nuxt runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 19:06:14 -04:00

166 lines
5.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useAuthStore } from '../../stores/auth.store'
// $fetch and navigateTo are shimmed in tests/setup.ts as globals.
// We re-cast them here so vi.mocked() provides typed mock utilities.
const mockFetch = vi.mocked($fetch as ReturnType<typeof vi.fn>)
const mockNavigateTo = vi.mocked(navigateTo as ReturnType<typeof vi.fn>)
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
// ---------------------------------------------------------------------------
// login()
// ---------------------------------------------------------------------------
describe('login()', () => {
it('stores the user and returns the response on success', async () => {
const user = { id: 1, email: 'alice@example.com', displayName: 'Alice', role: 'user' }
mockFetch.mockResolvedValueOnce({ user })
const auth = useAuthStore()
const result = await auth.login('alice@example.com', 'secret')
expect(mockFetch).toHaveBeenCalledWith('/api/auth/login', {
method: 'POST',
body: { email: 'alice@example.com', password: 'secret' },
})
expect(auth.user).toEqual(user)
expect(result).toEqual({ user })
})
it('includes totpCode in body when provided', async () => {
const user = { id: 1, email: 'alice@example.com', displayName: 'Alice', role: 'user' }
mockFetch.mockResolvedValueOnce({ user })
const auth = useAuthStore()
await auth.login('alice@example.com', 'secret', '123456')
expect(mockFetch).toHaveBeenCalledWith('/api/auth/login', {
method: 'POST',
body: { email: 'alice@example.com', password: 'secret', totpCode: '123456' },
})
})
it('returns requires_totp and does NOT set user when TOTP is required', async () => {
mockFetch.mockResolvedValueOnce({ requires_totp: true })
const auth = useAuthStore()
const result = await auth.login('alice@example.com', 'secret')
expect(result).toEqual({ requires_totp: true })
expect(auth.user).toBeNull()
})
it('does NOT include an Authorization header (cookie-only auth)', async () => {
mockFetch.mockResolvedValueOnce({ user: { id: 1, email: 'a@b.com', displayName: null, role: 'user' } })
const auth = useAuthStore()
await auth.login('a@b.com', 'pass')
const callArgs = mockFetch.mock.calls[0]
const options = callArgs[1] as Record<string, unknown>
expect(options).not.toHaveProperty('headers')
})
})
// ---------------------------------------------------------------------------
// logout()
// ---------------------------------------------------------------------------
describe('logout()', () => {
it('clears user state and calls navigateTo("/login")', async () => {
mockFetch.mockResolvedValueOnce({}) // logout POST succeeds
const auth = useAuthStore()
auth.user = { id: 1, email: 'alice@example.com', displayName: 'Alice', role: 'user' }
await auth.logout()
expect(auth.user).toBeNull()
expect(mockNavigateTo).toHaveBeenCalledWith('/login')
})
it('clears user state even when the logout request fails', async () => {
mockFetch.mockRejectedValueOnce(new Error('network error'))
const auth = useAuthStore()
auth.user = { id: 1, email: 'alice@example.com', displayName: 'Alice', role: 'user' }
await auth.logout()
expect(auth.user).toBeNull()
expect(mockNavigateTo).toHaveBeenCalledWith('/login')
})
})
// ---------------------------------------------------------------------------
// fetchProfile()
// ---------------------------------------------------------------------------
describe('fetchProfile()', () => {
it('sets user on success', async () => {
const user = { id: 2, email: 'bob@example.com', displayName: 'Bob', role: 'admin' }
mockFetch.mockResolvedValueOnce(user)
const auth = useAuthStore()
await auth.fetchProfile()
expect(auth.user).toEqual(user)
expect(mockFetch).toHaveBeenCalledWith('/api/auth/profile')
})
it('sets user to null on failure', async () => {
mockFetch.mockRejectedValueOnce(new Error('401'))
const auth = useAuthStore()
auth.user = { id: 1, email: 'a@b.com', displayName: null, role: 'user' }
await auth.fetchProfile()
expect(auth.user).toBeNull()
})
})
// ---------------------------------------------------------------------------
// getWsTicket()
// ---------------------------------------------------------------------------
describe('getWsTicket()', () => {
it('returns the ticket string from the API', async () => {
mockFetch.mockResolvedValueOnce({ ticket: 'abc-xyz-ticket' })
const auth = useAuthStore()
const ticket = await auth.getWsTicket()
expect(ticket).toBe('abc-xyz-ticket')
expect(mockFetch).toHaveBeenCalledWith('/api/auth/ws-ticket', { method: 'POST' })
})
})
// ---------------------------------------------------------------------------
// getters
// ---------------------------------------------------------------------------
describe('isAuthenticated getter', () => {
it('returns false when user is null', () => {
const auth = useAuthStore()
expect(auth.isAuthenticated).toBe(false)
})
it('returns true when user is set', () => {
const auth = useAuthStore()
auth.user = { id: 1, email: 'a@b.com', displayName: null, role: 'user' }
expect(auth.isAuthenticated).toBe(true)
})
})
describe('isAdmin getter', () => {
it('returns false for non-admin role', () => {
const auth = useAuthStore()
auth.user = { id: 1, email: 'a@b.com', displayName: null, role: 'user' }
expect(auth.isAdmin).toBe(false)
})
it('returns true for admin role', () => {
const auth = useAuthStore()
auth.user = { id: 1, email: 'a@b.com', displayName: null, role: 'admin' }
expect(auth.isAdmin).toBe(true)
})
})