refac: DELETE /v1/user/{id}?expected_role_id=N, user.remove audit event, 409 target_role_not_found, GetAll returns its error, Own-scope visibility filter moved into repos, JSON tags removed from use-case types

This commit is contained in:
Florian Sylvain committed 2026-10-29 16:28:25 +01:00
1 parent 5d6f378fce
commit b39a41be88
124 files changed
+8816 -523

No files matched your search

@@ -0,0 +1,158 @@
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { initializeLocale, setLocale } from '@/i18n'
import { customAccess } from '@/test/access'
import {
customTypes,
eventEntries,
installSelectPolyfills,
listPage,
lookupNames,
mountList,
text,
unmountAll,
} from './contentTestKit'
import { failWithBody, mountEditor, main as editorMain } from './entryEditorKit'
vi.mock('@/composables/useApi', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/composables/useApi')>()
return { ...actual, useApi: vi.fn() }
})
beforeAll(installSelectPolyfills)
afterEach(async () => {
localStorage.clear()
initializeLocale()
await unmountAll()
})
const READ_OWN = 'content.read:events=own, content.create:events=all'
const READ_ALL = 'content.read:events=all, content.create:events=all'
function main() {
return document.body.querySelector('#dashboard-content')!
}
function accessWith(spec: string) {
return customAccess(spec, 'Junior writer')
}
function newEntryButtons() {
return [...main().querySelectorAll('button')].filter(
(candidate) => text(candidate) === 'New entry',
)
}
const three = (_key: string, query: URLSearchParams) => listPage(eventEntries.slice(0, 3), 3, query)
const none = (_key: string, query: URLSearchParams) => listPage([], 0, query)
describe('Entry list: Read = Own with entries of their own', () => {
it('reads the header, the banner and the count line from the total', async () => {
await mountList('/content/events', {
access: accessWith(READ_OWN),
list: three,
lookup: lookupNames,
})
expect(text(main())).toContain('Events')
expect(text(main())).toContain('Your role shows only entries you created.')
expect(text(main())).toContain("Entries by others aren't shown")
expect(text(main())).toContain(
'Your role reads only entries you created. Ask an owner or admin if you need to see more.',
)
expect(text(main())).toContain('3 entries')
expect(main().querySelector('nav[aria-label*="pagination" i]')).toBeNull()
expect(newEntryButtons().length).toBeGreaterThan(0)
expect(newEntryButtons().every((button) => !button.disabled)).toBe(true)
})
it('shows neither the banner nor the own-scope header under Read = All', async () => {
await mountList('/content/events', {
access: accessWith(READ_ALL),
list: three,
lookup: lookupNames,
})
expect(text(main())).not.toContain("Entries by others aren't shown")
expect(text(main())).not.toContain('Your role shows only entries you created.')
})
})
describe('Entry list: Read = Own with no entries of their own', () => {
it('says what the person has not created, without hinting at other entries', async () => {
await mountList('/content/events', { access: accessWith(READ_OWN), list: none })
expect(text(main())).toContain("You haven't created an event yet")
expect(text(main())).toContain(
"Your role shows only entries you created, so other people's events aren't listed here. Use New entry to add one.",
)
expect(text(main())).not.toContain('No entries yet')
expect(newEntryButtons().length).toBeGreaterThan(0)
})
it('hides the action when the role cannot create', async () => {
await mountList('/content/events', {
access: accessWith('content.read:events=own'),
list: none,
})
expect(text(main())).toContain("You haven't created an event yet")
expect(newEntryButtons()).toHaveLength(0)
})
it('keeps the existing empty state under Read = All', async () => {
await mountList('/content/events', { access: accessWith(READ_ALL), list: none })
expect(text(main())).toContain('No entries yet')
expect(text(main())).not.toContain("You haven't created an event yet")
})
it('reads in French', async () => {
setLocale('fr')
await mountList('/content/events', { access: accessWith(READ_OWN), list: none })
expect(text(main())).toContain('Vous n’avez pas encore créé d’entrée dans Events')
expect(text(main())).toContain('Les entrées des autres ne sont pas affichées')
})
})
describe('Entry list: the singular noun comes from the type name', () => {
it.each([
[
'Recipes',
"You haven't created a recipe yet",
"other people's recipes aren't listed here",
],
[
'Press mentions',
"You haven't created a press mention yet",
"other people's press mentions aren't listed here",
],
[
'Categories',
"You haven't created a category yet",
"other people's categories aren't listed here",
],
])('%s', async (name, title, fragment) => {
await mountList('/content/events', {
access: accessWith(READ_OWN),
list: none,
type: () => ({ ...customTypes[0]!, name }),
})
expect(text(main())).toContain(title)
expect(text(main())).toContain(fragment)
})
})
describe('Entry list: a direct link to a hidden entry', () => {
it('shows the existing Entry not found page and nothing about the entry', async () => {
await mountEditor('/content/events/1', {
access: accessWith(READ_OWN),
entry: failWithBody(404, 'entry_not_found'),
})
expect(text(editorMain())).toContain('Entry not found')
expect(text(editorMain())).not.toContain('created by')
})
})