148 lines
4.7 KiB
TypeScript
148 lines
4.7 KiB
TypeScript
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')
|
||
})
|
||
})
|