Initial commit

This commit is contained in:
AI
2026-05-03 07:26:12 +00:00
commit 776d374b59
57 changed files with 15968 additions and 0 deletions
+39
View File
@@ -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
})
})
})
+114
View File
@@ -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'
})
})
})
+152
View File
@@ -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()
})
})
+67
View File
@@ -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('')
})
})
+47
View File
@@ -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
})
})
})
+29
View File
@@ -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)
})
})
+52
View File
@@ -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()
})
})
+69
View File
@@ -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([])
})
})
+24
View File
@@ -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)
})
})