import { afterEach, describe, expect, it } from 'vitest' import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { h } from 'vue' import uiPlugin from '@nuxt/ui/vue-plugin' import UApp from '@nuxt/ui/components/App.vue' import router from '@/router' import RouteAccessGate from '@/components/RouteAccessGate.vue' import { useSessionStore, type SessionStatus } from '@/stores/session' import { customAccess, noAccess } from '@/test/access' import { setLocale } from '@/i18n' let wrapper: VueWrapper | null = null interface Setup { grants: string role?: string path: string status?: SessionStatus verified?: boolean } async function open({ grants, role = 'Contributor', path, status = 'ready', verified = true, }: Setup) { setActivePinia(createPinia()) const session = useSessionStore() session.session = { id: 4, verified, displayName: '', access: customAccess(grants, role) } session.status = status await router.push(path).catch(() => undefined) wrapper = mount( () => h(UApp, null, () => h(RouteAccessGate, null, () => h('div', { 'data-testid': 'page' }, 'PAGE')), ), { attachTo: document.body, global: { plugins: [router, uiPlugin] } }, ) await flushPromises() return wrapper } afterEach(async () => { wrapper?.unmount() wrapper = null setLocale('en') document.body.innerHTML = '' await router.push('/login') }) const NO_ACCESS_TITLE = "You don't have access to this page" describe('A direct link to a page the role lacks', () => { it.each([ ['content.read=all', 'Contributor', '/schema', 'Go to Articles'], ['content.read=all', 'Contributor', '/schema/new', 'Go to Articles'], ['content.read=all', 'Contributor', '/users', 'Go to Articles'], ['content.read=all', 'Contributor', '/settings/sites', 'Go to Articles'], ['content.read=all', 'Contributor', '/integration/api-keys', 'Go to Articles'], [ 'content.read=all, content.read:events=none', 'Contributor', '/content/events', 'Go to Articles', ], ['media.view', 'Media', '/schema', 'Go to Home'], ])( '%s opening %s shows the page-level 403 and keeps the URL', async (grants, role, path, action) => { const view = await open({ grants, role, path }) expect(router.currentRoute.value.fullPath).toBe(path) expect(view.find('[data-testid="page"]').exists()).toBe(false) expect(view.text()).toContain(NO_ACCESS_TITLE) expect(view.text()).toContain( `Your role, ${role}, doesn't include it. Ask an owner or admin if you need it.`, ) const buttons = view .findAll('[data-slot="root"] button') .filter((button) => !button.element.closest('nav')) expect(buttons.map((button) => button.text()).filter((label) => label !== '')).toEqual([ action, ]) }, ) }) describe('A page the role can use', () => { it.each([ ['schema.view', '/schema'], ['schema.edit', '/schema/new'], ['users.view', '/users'], ['settings.manage_sites', '/settings/sites'], ['content.read:events=own', '/content/events'], ['content.create:events=all', '/content/events/new'], ['apikeys.view=own', '/integration/api-keys'], ['settings.view', '/settings/media/cache'], ])('%s opens %s without a 403', async (grants, path) => { const view = await open({ grants, path }) expect(view.find('[data-testid="page"]').exists()).toBe(true) expect(view.text()).not.toContain(NO_ACCESS_TITLE) }) }) describe('The guard', () => { it('shows neither the page nor the 403 while the session is checking', async () => { const view = await open({ grants: 'media.view', path: '/schema', status: 'checking' }) expect(view.find('[data-testid="page"]').exists()).toBe(false) expect(view.text()).not.toContain(NO_ACCESS_TITLE) useSessionStore().status = 'ready' await flushPromises() expect(view.text()).toContain(NO_ACCESS_TITLE) }) it('leaves an unverified session to the pending redirect', async () => { const view = await open({ grants: '', path: '/schema', verified: false }) expect(view.text()).not.toContain(NO_ACCESS_TITLE) }) it('switches to the 403 state when grants are lowered while the page is open', async () => { const view = await open({ grants: 'schema.view', path: '/schema' }) expect(view.find('[data-testid="page"]').exists()).toBe(true) const session = useSessionStore() session.session = { ...session.session!, access: noAccess('Contributor') } await flushPromises() expect(view.find('[data-testid="page"]').exists()).toBe(false) expect(view.text()).toContain(NO_ACCESS_TITLE) expect(router.currentRoute.value.fullPath).toBe('/schema') }) it('shows the gate copy in French', async () => { setLocale('fr') const view = await open({ grants: 'content.read=all', path: '/users' }) expect(view.text()).toContain('Vous n’avez pas accès à cette page') }) })