| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [x] Pas de régression - [x] TU/TI/TF rédigée - [x] TU/TI/TF OK - [ ] CHANGELOG modifié Co-authored-by: Matthieu <mtholot19@gmail.com> Reviewed-on: MALIO-DEV/Coltura#8 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #8.
This commit is contained in:
71
frontend/shared/composables/__tests__/useModules.test.ts
Normal file
71
frontend/shared/composables/__tests__/useModules.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { useModules } from '../useModules'
|
||||
|
||||
// Mock de useApi : on peut scripter la reponse de /api/modules.
|
||||
const mockApiGet = vi.hoisted(() => vi.fn())
|
||||
|
||||
// useApi est auto-importe par Nuxt en prod. En Vitest isole, on expose le
|
||||
// mock comme global pour que l'appel sans import dans useModules.ts
|
||||
// (pattern aligne sur useSidebar) fonctionne.
|
||||
vi.stubGlobal('useApi', () => ({ get: mockApiGet }))
|
||||
|
||||
describe('useModules', () => {
|
||||
beforeEach(() => {
|
||||
mockApiGet.mockReset()
|
||||
// Reset l'etat singleton entre tests.
|
||||
const { resetModules } = useModules()
|
||||
resetModules()
|
||||
})
|
||||
|
||||
it('charge la liste des modules actifs depuis /api/modules', async () => {
|
||||
mockApiGet.mockResolvedValueOnce({ modules: ['core', 'sites'] })
|
||||
const { loadModules, activeModuleIds, loaded } = useModules()
|
||||
|
||||
await loadModules()
|
||||
|
||||
expect(mockApiGet).toHaveBeenCalledWith('/modules', {}, { toast: false })
|
||||
expect(activeModuleIds.value).toEqual(['core', 'sites'])
|
||||
expect(loaded.value).toBe(true)
|
||||
})
|
||||
|
||||
it('isModuleActive retourne true pour un id present', async () => {
|
||||
mockApiGet.mockResolvedValueOnce({ modules: ['core', 'sites'] })
|
||||
const { loadModules, isModuleActive } = useModules()
|
||||
await loadModules()
|
||||
|
||||
expect(isModuleActive('sites')).toBe(true)
|
||||
expect(isModuleActive('core')).toBe(true)
|
||||
})
|
||||
|
||||
it('isModuleActive retourne false pour un id absent', async () => {
|
||||
mockApiGet.mockResolvedValueOnce({ modules: ['core'] })
|
||||
const { loadModules, isModuleActive } = useModules()
|
||||
await loadModules()
|
||||
|
||||
expect(isModuleActive('sites')).toBe(false)
|
||||
expect(isModuleActive('inexistant')).toBe(false)
|
||||
})
|
||||
|
||||
it('swallow les erreurs reseau et laisse la liste vide', async () => {
|
||||
mockApiGet.mockRejectedValueOnce(new Error('boom'))
|
||||
const { loadModules, activeModuleIds, loaded, isModuleActive } = useModules()
|
||||
|
||||
await loadModules()
|
||||
|
||||
expect(activeModuleIds.value).toEqual([])
|
||||
expect(loaded.value).toBe(true)
|
||||
expect(isModuleActive('sites')).toBe(false)
|
||||
})
|
||||
|
||||
it('resetModules vide l\'etat', async () => {
|
||||
mockApiGet.mockResolvedValueOnce({ modules: ['core', 'sites'] })
|
||||
const { loadModules, resetModules, activeModuleIds, loaded } = useModules()
|
||||
await loadModules()
|
||||
expect(activeModuleIds.value.length).toBeGreaterThan(0)
|
||||
|
||||
resetModules()
|
||||
|
||||
expect(activeModuleIds.value).toEqual([])
|
||||
expect(loaded.value).toBe(false)
|
||||
})
|
||||
})
|
||||
71
frontend/shared/composables/__tests__/useSidebar.test.ts
Normal file
71
frontend/shared/composables/__tests__/useSidebar.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { useSidebar } from '../useSidebar'
|
||||
|
||||
const mockApiGet = vi.hoisted(() => vi.fn())
|
||||
vi.stubGlobal('useApi', () => ({ get: mockApiGet }))
|
||||
|
||||
/**
|
||||
* Tests de l'invariant "loadSidebar ne reject jamais".
|
||||
*
|
||||
* Garantie utilisee par le middleware auth.global.ts qui fait un
|
||||
* Promise.all([loadSidebar(), loadModules()]) — si l'un throw, le
|
||||
* middleware echoue et toute l'app avec. Le swallow interne est donc
|
||||
* load-bearing et ce test le verrouille.
|
||||
*/
|
||||
describe('useSidebar', () => {
|
||||
beforeEach(() => {
|
||||
mockApiGet.mockReset()
|
||||
const { resetSidebar } = useSidebar()
|
||||
resetSidebar()
|
||||
})
|
||||
|
||||
it('charge sections et disabledRoutes depuis /api/sidebar', async () => {
|
||||
mockApiGet.mockResolvedValueOnce({
|
||||
sections: [{ label: 's', icon: 'i', items: [] }],
|
||||
disabledRoutes: ['/foo'],
|
||||
})
|
||||
const { loadSidebar, sections, disabledRoutes, loaded } = useSidebar()
|
||||
|
||||
await loadSidebar()
|
||||
|
||||
expect(sections.value).toHaveLength(1)
|
||||
expect(disabledRoutes.value).toEqual(['/foo'])
|
||||
expect(loaded.value).toBe(true)
|
||||
})
|
||||
|
||||
it('swallow les erreurs reseau sans rejeter (invariant middleware)', async () => {
|
||||
mockApiGet.mockRejectedValueOnce(new Error('boom'))
|
||||
const { loadSidebar, sections, disabledRoutes, loaded } = useSidebar()
|
||||
|
||||
// Assertion principale : la promise resout normalement meme sur erreur.
|
||||
await expect(loadSidebar()).resolves.toBeUndefined()
|
||||
expect(sections.value).toEqual([])
|
||||
expect(disabledRoutes.value).toEqual([])
|
||||
expect(loaded.value).toBe(true)
|
||||
})
|
||||
|
||||
it('isRouteDisabled matche exactement un chemin', async () => {
|
||||
mockApiGet.mockResolvedValueOnce({ sections: [], disabledRoutes: ['/foo'] })
|
||||
const { loadSidebar, isRouteDisabled } = useSidebar()
|
||||
await loadSidebar()
|
||||
|
||||
expect(isRouteDisabled('/foo')).toBe(true)
|
||||
expect(isRouteDisabled('/foo/bar')).toBe(true)
|
||||
expect(isRouteDisabled('/other')).toBe(false)
|
||||
})
|
||||
|
||||
it('resetSidebar vide l\'etat', async () => {
|
||||
mockApiGet.mockResolvedValueOnce({
|
||||
sections: [{ label: 's', icon: 'i', items: [] }],
|
||||
disabledRoutes: ['/foo'],
|
||||
})
|
||||
const { loadSidebar, resetSidebar, sections, loaded } = useSidebar()
|
||||
await loadSidebar()
|
||||
expect(loaded.value).toBe(true)
|
||||
|
||||
resetSidebar()
|
||||
|
||||
expect(sections.value).toEqual([])
|
||||
expect(loaded.value).toBe(false)
|
||||
})
|
||||
})
|
||||
49
frontend/shared/composables/useModules.ts
Normal file
49
frontend/shared/composables/useModules.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Composable de lecture des modules actifs (source : `/api/modules`).
|
||||
*
|
||||
* State singleton au niveau module : `useSidebar` suit la meme convention.
|
||||
* Chargement idempotent via le flag `loaded`, reset explicite au logout
|
||||
* (voir pages/logout.vue).
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
|
||||
const activeModuleIds = ref<string[]>([])
|
||||
const loaded = ref(false)
|
||||
|
||||
export function useModules() {
|
||||
async function loadModules() {
|
||||
try {
|
||||
const api = useApi()
|
||||
const data = await api.get<{ modules: string[] }>(
|
||||
'/modules',
|
||||
{},
|
||||
{ toast: false },
|
||||
)
|
||||
activeModuleIds.value = data.modules ?? []
|
||||
loaded.value = true
|
||||
} catch {
|
||||
// Swallow volontaire aligne sur useSidebar : un echec reseau ne
|
||||
// doit pas bloquer le rendu, l'app affichera juste sans la
|
||||
// granularite module (selector masque par defaut).
|
||||
activeModuleIds.value = []
|
||||
loaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function isModuleActive(id: string): boolean {
|
||||
return activeModuleIds.value.includes(id)
|
||||
}
|
||||
|
||||
function resetModules() {
|
||||
activeModuleIds.value = []
|
||||
loaded.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
activeModuleIds,
|
||||
loaded,
|
||||
loadModules,
|
||||
isModuleActive,
|
||||
resetModules,
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ref } from 'vue'
|
||||
import type { SidebarSection } from '~/shared/types'
|
||||
|
||||
const sections = ref<SidebarSection[]>([])
|
||||
|
||||
Reference in New Issue
Block a user