Initial commit
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
async function loadPlugin() {
|
||||
vi.resetModules()
|
||||
vi.stubGlobal('defineNuxtPlugin', (setup: unknown) => setup)
|
||||
|
||||
return (await import('../app/plugins/api.client')).default
|
||||
}
|
||||
|
||||
describe('api plugin', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('creates API clients from runtime config values', async () => {
|
||||
vi.stubGlobal('useRuntimeConfig', () => ({
|
||||
public: {
|
||||
apiBaseUrl: 'https://api.example.test',
|
||||
apiTimeoutMs: 4321
|
||||
}
|
||||
}))
|
||||
|
||||
const plugin = await loadPlugin()
|
||||
const { provide } = plugin()
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue({ status: 'ok' })
|
||||
vi.stubGlobal('$fetch', fetchMock)
|
||||
|
||||
const response = await provide.api.example.getHealth()
|
||||
|
||||
expect(response).toEqual({ status: 'ok' })
|
||||
expect(fetchMock).toHaveBeenCalledWith('/health', {
|
||||
baseURL: 'https://api.example.test',
|
||||
signal: undefined,
|
||||
timeout: 4321
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,114 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed } from 'vue'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useAuthStore } from '../app/stores/auth'
|
||||
|
||||
async function loadMiddleware() {
|
||||
vi.resetModules()
|
||||
vi.stubGlobal('defineNuxtRouteMiddleware', (setup: unknown) => setup)
|
||||
|
||||
return (await import('../app/middleware/auth.global')).default
|
||||
}
|
||||
|
||||
function createAuthClient(overrides: Partial<ReturnType<typeof buildAuthClient>> = {}) {
|
||||
return {
|
||||
...buildAuthClient(),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function buildAuthClient() {
|
||||
return {
|
||||
isEnabled: true,
|
||||
isAuthenticated: computed(() => false),
|
||||
ensureInitialized: vi.fn().mockResolvedValue(undefined),
|
||||
login: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
describe('auth middleware', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
vi.stubGlobal('useAuthStore', useAuthStore)
|
||||
})
|
||||
|
||||
it('skips public routes and disabled auth', async () => {
|
||||
const publicAuth = createAuthClient()
|
||||
vi.stubGlobal('useAuth', () => publicAuth)
|
||||
|
||||
const middleware = await loadMiddleware()
|
||||
|
||||
await expect(middleware({ meta: { public: true } })).resolves.toBeUndefined()
|
||||
expect(publicAuth.ensureInitialized).not.toHaveBeenCalled()
|
||||
|
||||
const disabledAuth = createAuthClient({ isEnabled: false })
|
||||
vi.stubGlobal('useAuth', () => disabledAuth)
|
||||
|
||||
await expect(middleware({ meta: { requiresAuth: true } })).resolves.toBeUndefined()
|
||||
expect(disabledAuth.ensureInitialized).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('requests login for unauthenticated protected routes', async () => {
|
||||
const auth = createAuthClient({
|
||||
isAuthenticated: computed(() => false)
|
||||
})
|
||||
vi.stubGlobal('useAuth', () => auth)
|
||||
|
||||
const middleware = await loadMiddleware()
|
||||
|
||||
await expect(middleware({ meta: { requiresAuth: true } })).resolves.toBeUndefined()
|
||||
|
||||
expect(auth.ensureInitialized).toHaveBeenCalledOnce()
|
||||
expect(auth.login).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('allows authenticated users with a matching role', async () => {
|
||||
const authStore = useAuthStore()
|
||||
authStore.setUser({
|
||||
id: 'user-1',
|
||||
roles: ['ADMIN']
|
||||
})
|
||||
|
||||
const auth = createAuthClient({
|
||||
isAuthenticated: computed(() => true)
|
||||
})
|
||||
vi.stubGlobal('useAuth', () => auth)
|
||||
|
||||
const middleware = await loadMiddleware()
|
||||
|
||||
await expect(middleware({ meta: { roles: ['ADMIN'] } })).resolves.toBeUndefined()
|
||||
|
||||
expect(auth.ensureInitialized).toHaveBeenCalledOnce()
|
||||
expect(auth.login).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('throws a 403 error for authenticated users without a required role', async () => {
|
||||
const authStore = useAuthStore()
|
||||
authStore.setUser({
|
||||
id: 'user-2',
|
||||
roles: ['USER']
|
||||
})
|
||||
|
||||
const createErrorMock = vi.fn((error: { statusCode: number; statusMessage: string }) => error)
|
||||
vi.stubGlobal('createError', createErrorMock)
|
||||
|
||||
const auth = createAuthClient({
|
||||
isAuthenticated: computed(() => true)
|
||||
})
|
||||
vi.stubGlobal('useAuth', () => auth)
|
||||
|
||||
const middleware = await loadMiddleware()
|
||||
|
||||
await expect(middleware({ meta: { roles: ['ADMIN'] } })).rejects.toMatchObject({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Forbidden'
|
||||
})
|
||||
|
||||
expect(createErrorMock).toHaveBeenCalledWith({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Forbidden'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,152 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useAuthStore, type AuthUser } from '../app/stores/auth'
|
||||
|
||||
type AuthPlugin = typeof import('../app/plugins/auth').default
|
||||
|
||||
function createRuntimeConfig(authMode: string | undefined = undefined) {
|
||||
return {
|
||||
public: {
|
||||
authMode,
|
||||
authLoginUrl: '/login',
|
||||
authLogoutUrl: '/logout',
|
||||
authUserinfoUrl: '/api/auth/me'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPlugin() {
|
||||
vi.resetModules()
|
||||
vi.stubGlobal('defineNuxtPlugin', (setup: AuthPlugin) => setup)
|
||||
|
||||
return (await import('../app/plugins/auth')).default
|
||||
}
|
||||
|
||||
describe('auth plugin', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('initializes a disabled auth client and keeps auth turned off', async () => {
|
||||
vi.stubGlobal('useRuntimeConfig', () => createRuntimeConfig())
|
||||
|
||||
const plugin = await loadPlugin()
|
||||
const { provide } = plugin()
|
||||
const auth = provide.auth
|
||||
|
||||
expect(auth.mode).toBe('disabled')
|
||||
expect(auth.isEnabled).toBe(false)
|
||||
|
||||
await auth.ensureInitialized()
|
||||
|
||||
const store = useAuthStore()
|
||||
expect(auth.isReady.value).toBe(true)
|
||||
expect(auth.isAuthenticated.value).toBe(false)
|
||||
expect(store.isInitialized).toBe(true)
|
||||
expect(store.user).toBeNull()
|
||||
})
|
||||
|
||||
it('uses a mock user in mock mode and supports login and logout locally', async () => {
|
||||
vi.stubGlobal('useRuntimeConfig', () => createRuntimeConfig('mock'))
|
||||
|
||||
const plugin = await loadPlugin()
|
||||
const { provide } = plugin()
|
||||
const auth = provide.auth
|
||||
|
||||
await auth.ensureInitialized()
|
||||
|
||||
const store = useAuthStore()
|
||||
expect(auth.mode).toBe('mock')
|
||||
expect(auth.isEnabled).toBe(true)
|
||||
expect(auth.isAuthenticated.value).toBe(true)
|
||||
expect(store.user).toMatchObject({
|
||||
id: 'mock-user',
|
||||
email: 'mock@example.com',
|
||||
roles: ['USER']
|
||||
})
|
||||
|
||||
auth.logout()
|
||||
expect(store.user).toBeNull()
|
||||
|
||||
auth.login()
|
||||
expect(store.user).toMatchObject({
|
||||
id: 'mock-user',
|
||||
email: 'mock@example.com'
|
||||
})
|
||||
})
|
||||
|
||||
it('loads the current user from the userinfo endpoint in userinfo mode', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
id: 'user-1',
|
||||
email: 'john@example.com',
|
||||
roles: ['USER']
|
||||
} satisfies AuthUser)
|
||||
|
||||
vi.stubGlobal('useRuntimeConfig', () => createRuntimeConfig('userinfo'))
|
||||
vi.stubGlobal('$fetch', fetchMock)
|
||||
|
||||
const plugin = await loadPlugin()
|
||||
const { provide } = plugin()
|
||||
const auth = provide.auth
|
||||
|
||||
await auth.ensureInitialized()
|
||||
|
||||
const store = useAuthStore()
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/auth/me', {
|
||||
credentials: 'include'
|
||||
})
|
||||
expect(auth.isAuthenticated.value).toBe(true)
|
||||
expect(store.user).toMatchObject({
|
||||
id: 'user-1',
|
||||
email: 'john@example.com'
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to an anonymous session when userinfo loading fails', async () => {
|
||||
const fetchMock = vi.fn().mockRejectedValue(new Error('network error'))
|
||||
|
||||
vi.stubGlobal('useRuntimeConfig', () => createRuntimeConfig('userinfo'))
|
||||
vi.stubGlobal('$fetch', fetchMock)
|
||||
|
||||
const plugin = await loadPlugin()
|
||||
const { provide } = plugin()
|
||||
const auth = provide.auth
|
||||
|
||||
await auth.ensureInitialized()
|
||||
|
||||
const store = useAuthStore()
|
||||
expect(auth.isReady.value).toBe(true)
|
||||
expect(auth.isAuthenticated.value).toBe(false)
|
||||
expect(store.isInitialized).toBe(true)
|
||||
expect(store.user).toBeNull()
|
||||
})
|
||||
|
||||
it('redirects login and logout in userinfo mode', async () => {
|
||||
vi.stubGlobal('useRuntimeConfig', () => createRuntimeConfig('userinfo'))
|
||||
vi.stubGlobal('$fetch', vi.fn().mockResolvedValue(null))
|
||||
vi.stubGlobal('window', {
|
||||
location: {
|
||||
href: ''
|
||||
}
|
||||
})
|
||||
|
||||
const plugin = await loadPlugin()
|
||||
const { provide } = plugin()
|
||||
const auth = provide.auth
|
||||
|
||||
auth.login()
|
||||
expect(window.location.href).toBe('/login')
|
||||
|
||||
const store = useAuthStore()
|
||||
store.setUser({
|
||||
id: 'user-2',
|
||||
roles: ['USER']
|
||||
})
|
||||
|
||||
auth.logout()
|
||||
expect(window.location.href).toBe('/logout')
|
||||
expect(store.user).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useAuthStore, type AuthUser } from '../app/stores/auth'
|
||||
|
||||
describe('useAuthStore', () => {
|
||||
const baseUser: AuthUser = {
|
||||
id: 'user-1',
|
||||
email: 'john@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
roles: ['USER', 'ADMIN']
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('initializes authenticated session state from a user', () => {
|
||||
const store = useAuthStore()
|
||||
|
||||
store.initialize(baseUser)
|
||||
|
||||
expect(store.isInitialized).toBe(true)
|
||||
expect(store.isAuthenticated).toBe(true)
|
||||
expect(store.user).toEqual(baseUser)
|
||||
expect(store.roles).toEqual(['USER', 'ADMIN'])
|
||||
expect(store.displayName).toBe('John Doe')
|
||||
})
|
||||
|
||||
it('falls back to email or id when full name is not available', () => {
|
||||
const store = useAuthStore()
|
||||
|
||||
store.setUser({
|
||||
id: 'user-2',
|
||||
email: 'fallback@example.com',
|
||||
roles: ['USER']
|
||||
})
|
||||
|
||||
expect(store.displayName).toBe('fallback@example.com')
|
||||
|
||||
store.setUser({
|
||||
id: 'user-3',
|
||||
roles: ['USER']
|
||||
})
|
||||
|
||||
expect(store.displayName).toBe('user-3')
|
||||
})
|
||||
|
||||
it('checks roles and clears session safely', () => {
|
||||
const store = useAuthStore()
|
||||
|
||||
store.initialize(baseUser)
|
||||
|
||||
expect(store.hasRole('ADMIN')).toBe(true)
|
||||
expect(store.hasRole('GUEST')).toBe(false)
|
||||
expect(store.hasAnyRole(['GUEST', 'ADMIN'])).toBe(true)
|
||||
expect(store.hasAnyRole(['GUEST'])).toBe(false)
|
||||
|
||||
store.clearSession()
|
||||
|
||||
expect(store.isInitialized).toBe(true)
|
||||
expect(store.isAuthenticated).toBe(false)
|
||||
expect(store.user).toBeNull()
|
||||
expect(store.roles).toEqual([])
|
||||
expect(store.displayName).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createExampleApi } from '../api/wrappers/example'
|
||||
|
||||
describe('createExampleApi', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('calls the expected endpoint configuration for getHealth', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({ status: 'ok' })
|
||||
vi.stubGlobal('$fetch', fetchMock)
|
||||
|
||||
const api = createExampleApi({
|
||||
baseURL: '/api',
|
||||
timeoutMs: 10_000
|
||||
})
|
||||
const controller = new AbortController()
|
||||
|
||||
const response = await api.getHealth({ signal: controller.signal })
|
||||
|
||||
expect(response).toEqual({ status: 'ok' })
|
||||
expect(fetchMock).toHaveBeenCalledWith('/health', {
|
||||
baseURL: '/api',
|
||||
signal: controller.signal,
|
||||
timeout: 10_000
|
||||
})
|
||||
})
|
||||
|
||||
it('calls the expected endpoint configuration for getWelcome', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({ message: 'Welcome' })
|
||||
vi.stubGlobal('$fetch', fetchMock)
|
||||
|
||||
const api = createExampleApi({
|
||||
baseURL: 'https://example.test',
|
||||
timeoutMs: 5_000
|
||||
})
|
||||
|
||||
const response = await api.getWelcome()
|
||||
|
||||
expect(response).toEqual({ message: 'Welcome' })
|
||||
expect(fetchMock).toHaveBeenCalledWith('/welcome', {
|
||||
baseURL: 'https://example.test',
|
||||
signal: undefined,
|
||||
timeout: 5_000
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import en from '../i18n/locales/en.json'
|
||||
import sk from '../i18n/locales/sk.json'
|
||||
|
||||
type JsonValue = string | number | boolean | null | JsonObject | JsonValue[]
|
||||
type JsonObject = Record<string, JsonValue>
|
||||
|
||||
function collectLeafPaths(value: JsonValue, prefix = ''): string[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap((item, index) => collectLeafPaths(item, `${prefix}[${index}]`))
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return Object.entries(value as JsonObject).flatMap(([key, nestedValue]) =>
|
||||
collectLeafPaths(nestedValue, prefix ? `${prefix}.${key}` : key)
|
||||
)
|
||||
}
|
||||
|
||||
return [prefix]
|
||||
}
|
||||
|
||||
describe('i18n locale files', () => {
|
||||
it('keep the same translation key structure in slovak and english', () => {
|
||||
const skPaths = collectLeafPaths(sk as JsonObject).sort()
|
||||
const enPaths = collectLeafPaths(en as JsonObject).sort()
|
||||
|
||||
expect(skPaths).toEqual(enPaths)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
async function loadPlugin() {
|
||||
vi.resetModules()
|
||||
vi.stubGlobal('defineNuxtPlugin', (setup: unknown) => setup)
|
||||
|
||||
return (await import('../app/plugins/logger')).default
|
||||
}
|
||||
|
||||
describe('logger plugin', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('logs messages with the expected prefix and payload', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-03-29T12:00:00.000Z'))
|
||||
|
||||
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {})
|
||||
|
||||
const plugin = await loadPlugin()
|
||||
const { provide } = plugin()
|
||||
|
||||
provide.logger.info('Action completed', { section: 'home' })
|
||||
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
'[template:info] 2026-03-29T12:00:00.000Z Action completed',
|
||||
{ section: 'home' }
|
||||
)
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('logs messages without payload as a single console argument', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-03-29T12:00:00.000Z'))
|
||||
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
const plugin = await loadPlugin()
|
||||
const { provide } = plugin()
|
||||
|
||||
provide.logger.warn('Missing optional data')
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'[template:warn] 2026-03-29T12:00:00.000Z Missing optional data'
|
||||
)
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUiStore } from '../app/stores/ui'
|
||||
|
||||
describe('useUiStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('tracks page loading state across nested requests', () => {
|
||||
const store = useUiStore()
|
||||
|
||||
expect(store.isPageLoading).toBe(false)
|
||||
|
||||
store.startLoading()
|
||||
store.startLoading()
|
||||
|
||||
expect(store.isPageLoading).toBe(true)
|
||||
|
||||
store.stopLoading()
|
||||
expect(store.isPageLoading).toBe(true)
|
||||
|
||||
store.stopLoading()
|
||||
store.stopLoading()
|
||||
expect(store.isPageLoading).toBe(false)
|
||||
|
||||
store.setLoading(true)
|
||||
expect(store.isPageLoading).toBe(true)
|
||||
|
||||
store.setLoading(false)
|
||||
expect(store.isPageLoading).toBe(false)
|
||||
})
|
||||
|
||||
it('creates notifications with a default tone and removes them by id', () => {
|
||||
const store = useUiStore()
|
||||
|
||||
vi.spyOn(crypto, 'randomUUID').mockReturnValue('generated-id')
|
||||
|
||||
const id = store.pushNotification({
|
||||
title: 'Saved',
|
||||
message: 'Changes were stored.'
|
||||
})
|
||||
|
||||
expect(id).toBe('generated-id')
|
||||
expect(store.notifications).toEqual([
|
||||
{
|
||||
id: 'generated-id',
|
||||
title: 'Saved',
|
||||
message: 'Changes were stored.',
|
||||
tone: 'info'
|
||||
}
|
||||
])
|
||||
|
||||
store.pushNotification({
|
||||
id: 'custom-id',
|
||||
title: 'Warning',
|
||||
tone: 'warning'
|
||||
})
|
||||
|
||||
expect(store.notifications).toHaveLength(2)
|
||||
|
||||
store.removeNotification('generated-id')
|
||||
expect(store.notifications.map((notification) => notification.id)).toEqual(['custom-id'])
|
||||
|
||||
store.clearNotifications()
|
||||
expect(store.notifications).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useLogger } from '../app/composables/useLogger'
|
||||
|
||||
describe('useLogger', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('returns the logger injected into the Nuxt app', () => {
|
||||
const logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn()
|
||||
}
|
||||
|
||||
vi.stubGlobal('useNuxtApp', () => ({
|
||||
$logger: logger
|
||||
}))
|
||||
|
||||
expect(useLogger()).toBe(logger)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user