feat: roles SPA foundations

This commit is contained in:
Florian Sylvain committed 2026-10-27 22:21:12 +01:00
1 parent 80f7037d3a
commit 907949aa12
180 files changed
+4308 -835

No files matched your search

+45 -18
View File
@@ -3,6 +3,7 @@ import { flushPromises } from '@vue/test-utils'
import router from '@/router'
import { useApi } from '@/composables/useApi'
import { DESKTOP_WIDTH, MOBILE_WIDTH } from '@/components/__tests__/viewport'
import { customAccess } from '@/test/access'
import {
callsTo,
chooseMenuItem,
@@ -239,33 +240,28 @@ describe('Sites: set as default', () => {
)
})
it('closes and shows the owner-only toast on a 403', async () => {
it('closes and leaves the generic missing-permission toast to the API client on a 403', async () => {
const backend = createBackend()
backend.responders['PATCH /v1/site/2/default'] = failWith(
403,
'Only the owner can do this.',
)
backend.responders['PATCH /v1/site/2/default'] = failWith(403, 'missing_permission')
await openDialog(backend)
await click('Make default', dialog()!)
expect(dialog()).toBeNull()
expect(document.body.textContent).toContain('Only the owner can manage sites')
expect(document.body.textContent).toContain('Ask the owner of this site.')
expect(document.body.textContent).not.toContain('Only the owner can manage sites')
})
})
describe.each([
['admin', '/settings/sites'],
['member', '/settings/sites'],
['admin', '/settings/sites?edit=journal'],
['member', '/settings/sites?edit=journal'],
])('Sites: forbidden (403 page) for %s on %s', (role, path) => {
it('explains instead of redirecting, opens no panel and Back to Settings goes to /settings', async () => {
it('explains instead of redirecting, opens no panel and offers Go to Articles', async () => {
const backend = await mountDirectory('sites', path, { role })
const text = document.body.textContent ?? ''
expect(text).toContain("You don't have access to Sites")
expect(text).toContain("You don't have access to this page")
expect(text).toContain(
'Only the owner can manage sites. Ask the owner of this site if you need a change.',
"Your role, Writer, doesn't include it. Ask an owner or admin if you need it.",
)
expect(router.currentRoute.value.fullPath).toBe(path)
expect(dialog()).toBeNull()
@@ -275,9 +271,9 @@ describe.each([
expect(callsTo(backend, 'GET', '/v1/site/')).toHaveLength(0)
const push = vi.spyOn(router, 'push').mockResolvedValue(undefined)
await click('Back to Settings')
await click('Go to Articles')
expect(push).toHaveBeenCalledWith('/settings')
expect(push).toHaveBeenCalledWith('/article')
push.mockRestore()
})
})
@@ -288,7 +284,7 @@ describe('Sites: the API answers 403 where the SPA expected the owner', () => {
backend.responders['GET /v1/site/'] = failWith(403, 'Only the owner can do this.')
await mountDirectory('sites', '/settings/sites', { backend })
expect(document.body.textContent).toContain("You don't have access to Sites")
expect(document.body.textContent).toContain("You don't have access to this page")
expect(rows()).toHaveLength(0)
})
@@ -300,8 +296,7 @@ describe('Sites: the API answers 403 where the SPA expected the owner', () => {
await chooseMenuItem('Delete…')
await click('Delete site', dialog()!)
expect(document.body.textContent).toContain('Only the owner can manage sites')
expect(document.body.textContent).toContain('Ask the owner of this site.')
expect(document.body.textContent).not.toContain('Only the owner can manage sites')
})
})
@@ -316,13 +311,45 @@ describe('Sites: the sidebar for the owner and for an admin', () => {
expect(link.getAttribute('aria-current')).toBe('page')
})
it('has no Publishing group and no Sites entry for an admin', async () => {
it('shows the Publishing group and the Sites entry for an admin', async () => {
await mountDirectory('sites', '/settings/sites', { role: 'admin' })
const nav = document.body.querySelector('nav[aria-label="Main"]')!
expect(textOf(nav)).toContain('Publishing')
expect(nav.querySelector('a[href="/settings/sites"]')).not.toBeNull()
})
it('has no Publishing group and no Sites entry for a writer', async () => {
await mountDirectory('sites', '/settings/sites', { role: 'member' })
const nav = document.body.querySelector('nav[aria-label="Main"]')!
expect(textOf(nav)).not.toContain('Publishing')
expect(nav.querySelector('a[href="/settings/sites"]')).toBeNull()
})
it('disables Delete site with the Owner only tag for an admin', async () => {
await mountDirectory('sites', '/settings/sites', { role: 'admin' })
await openRowMenu('Archive')
const item = Array.from(document.body.querySelectorAll('[role="menuitem"]')).find(
(candidate) => candidate.textContent?.includes('Delete…'),
)!
expect(item.getAttribute('aria-disabled')).toBe('true')
expect(item.textContent).toContain('Owner only')
})
it('enables Delete site with settings.delete_site', async () => {
await mountDirectory('sites', '/settings/sites', {
access: customAccess('settings.view, settings.manage_sites, settings.delete_site'),
})
await openRowMenu('Archive')
const item = Array.from(document.body.querySelectorAll('[role="menuitem"]')).find(
(candidate) => candidate.textContent?.includes('Delete…'),
)!
expect(item.getAttribute('aria-disabled')).not.toBe('true')
expect(item.textContent).not.toContain('Owner only')
})
})
void useApi