feat: SPA Roles screens
This commit is contained in:
1 parent
907949aa12
commit
3db490a615
47 files changed
+6947
-9
No files matched your search
@@ -0,0 +1,99 @@
|
||||
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 } from '@/stores/session'
|
||||
import { customAccess } from '@/test/access'
|
||||
|
||||
let wrapper: VueWrapper | null = null
|
||||
|
||||
async function open(grants: string, path: string, role = 'Contributor') {
|
||||
setActivePinia(createPinia())
|
||||
const session = useSessionStore()
|
||||
session.session = { id: 4, verified: true, displayName: '', access: customAccess(grants, role) }
|
||||
session.status = 'ready'
|
||||
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
|
||||
document.body.innerHTML = ''
|
||||
await router.push('/login')
|
||||
})
|
||||
|
||||
describe('Route access', () => {
|
||||
it.each([
|
||||
['roles.view', '/settings/roles', true],
|
||||
['roles.view', '/settings/roles/new', false],
|
||||
['roles.view, roles.manage', '/settings/roles/new', true],
|
||||
['roles.view', '/settings/roles/4', true],
|
||||
['content.read=all', '/settings/roles', false],
|
||||
['content.read=all', '/settings/roles/4', false],
|
||||
])('%s opening %s renders the page: %s', async (grants, path, allowed) => {
|
||||
const view = await open(grants, path)
|
||||
|
||||
expect(router.currentRoute.value.fullPath).toBe(path)
|
||||
expect(view.find('[data-testid="page"]').exists()).toBe(allowed)
|
||||
if (!allowed) {
|
||||
expect(view.text()).toContain("You don't have access to this page")
|
||||
}
|
||||
})
|
||||
|
||||
it('shows the page-level 403 with Go to Articles to a Contributor', async () => {
|
||||
const view = await open('content.read=all', '/settings/roles')
|
||||
|
||||
expect(view.text()).toContain(
|
||||
"Your role, Contributor, doesn't include it. Ask an owner or admin if you need it.",
|
||||
)
|
||||
expect(view.text()).toContain('Go to Articles')
|
||||
})
|
||||
})
|
||||
|
||||
describe('/settings/permissions is retired', () => {
|
||||
it('replaces the URL with /settings/roles and flashes no 403', async () => {
|
||||
const view = await open('roles.view', '/settings/permissions')
|
||||
|
||||
expect(router.currentRoute.value.fullPath).toBe('/settings/roles')
|
||||
expect(view.find('[data-testid="page"]').exists()).toBe(true)
|
||||
expect(view.text()).not.toContain("You don't have access to this page")
|
||||
})
|
||||
|
||||
it('redirects even for someone who cannot see roles, to the same 403 as /settings/roles', async () => {
|
||||
await open('content.read=all', '/settings/permissions')
|
||||
|
||||
expect(router.currentRoute.value.fullPath).toBe('/settings/roles')
|
||||
})
|
||||
})
|
||||
|
||||
describe('the session is still being checked', () => {
|
||||
it('keeps the boot screen instead of the 403 state', async () => {
|
||||
setActivePinia(createPinia())
|
||||
const session = useSessionStore()
|
||||
session.status = 'checking'
|
||||
await router.push('/settings/roles').catch(() => undefined)
|
||||
wrapper = mount(
|
||||
() =>
|
||||
h(UApp, null, () =>
|
||||
h(RouteAccessGate, null, () => h('div', { 'data-testid': 'page' })),
|
||||
),
|
||||
{ attachTo: document.body, global: { plugins: [router, uiPlugin] } },
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).not.toContain("You don't have access to this page")
|
||||
})
|
||||
})
|
||||
+20
-5
@@ -9,14 +9,10 @@ import {
|
||||
} from 'vue-router'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { installDocumentTitle } from '@/composables/documentTitle'
|
||||
import { ARTICLE_TYPE_KEY, type RouteAccess } from '@/lib/routeAccess'
|
||||
import { ARTICLE_TYPE_KEY } from '@/lib/routeAccess'
|
||||
|
||||
export type AuthRequirement = 'public' | 'session' | 'verified' | 'any'
|
||||
|
||||
export function routeAccessOf(route: RouteLocationNormalized): RouteAccess | undefined {
|
||||
return route.meta.access as RouteAccess | undefined
|
||||
}
|
||||
|
||||
const gatedViews = new Map<string, Promise<Component>>()
|
||||
|
||||
function gated(key: string, load: () => Promise<{ default: Component }>) {
|
||||
@@ -226,6 +222,25 @@ const routes: RouteRecordRaw[] = [
|
||||
component: gated('@/views/SitesView.vue', () => import('@/views/SitesView.vue')),
|
||||
meta: { auth: 'verified', title: 'Settings', access: { key: 'settings.manage_sites' } },
|
||||
},
|
||||
{
|
||||
path: '/settings/permissions',
|
||||
redirect: '/settings/roles',
|
||||
},
|
||||
{
|
||||
path: '/settings/roles',
|
||||
component: gated('@/views/RolesListView.vue', () => import('@/views/RolesListView.vue')),
|
||||
meta: { auth: 'verified', title: 'Roles', access: { key: 'roles.view' } },
|
||||
},
|
||||
{
|
||||
path: '/settings/roles/new',
|
||||
component: gated('@/views/RoleEditorView.vue', () => import('@/views/RoleEditorView.vue')),
|
||||
meta: { auth: 'verified', title: 'Roles', access: { key: 'roles.manage' } },
|
||||
},
|
||||
{
|
||||
path: '/settings/roles/:id',
|
||||
component: gated('@/views/RoleEditorView.vue', () => import('@/views/RoleEditorView.vue')),
|
||||
meta: { auth: 'verified', title: 'Roles', access: { key: 'roles.view' } },
|
||||
},
|
||||
{
|
||||
path: '/settings/media/cache',
|
||||
component: gated('@/views/CachePage.vue', () => import('@/views/CachePage.vue')),
|
||||
|
||||
Reference in new issue
Block a user