Module sites (#8)
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s

| 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:
2026-04-20 15:31:58 +00:00
committed by Autin
parent 6b4868b261
commit 6cf5ef4cfc
77 changed files with 7739 additions and 80 deletions

View 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)
})
})

View 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)
})
})

View 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,
}
}

View File

@@ -1,3 +1,4 @@
import { ref } from 'vue'
import type { SidebarSection } from '~/shared/types'
const sections = ref<SidebarSection[]>([])