feat: roles SPA foundations
This commit is contained in:
1 parent
80f7037d3a
commit
907949aa12
180 files changed
+4308
-835
No files matched your search
@@ -0,0 +1,147 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user