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:
1 parent
5d6f378fce
commit
b39a41be88
124 files changed
+8816
-523
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { initializeLocale, setLocale } from '@/i18n'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, type VueWrapper } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
@@ -9,13 +10,15 @@ import { renderInApp } from './roleKit'
|
||||
import TransferOwnershipDialog from '../TransferOwnershipDialog.vue'
|
||||
|
||||
const transferOwnership = vi.fn()
|
||||
const listUsers = vi.fn()
|
||||
const toastRemove = vi.fn()
|
||||
const refresh = vi.fn()
|
||||
let toastAdd: ReturnType<typeof vi.fn>
|
||||
|
||||
vi.mock('@/composables/useUsers', () => ({ useUsers: () => ({ transferOwnership }) }))
|
||||
vi.mock('@/composables/useUsers', () => ({ useUsers: () => ({ transferOwnership, listUsers }) }))
|
||||
vi.mock('@nuxt/ui/composables', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@nuxt/ui/composables')>()
|
||||
return { ...actual, useToast: () => ({ add: toastAdd }) }
|
||||
return { ...actual, useToast: () => ({ add: toastAdd, remove: toastRemove }) }
|
||||
})
|
||||
|
||||
const JONAS_EMAIL = 'jonas@renewbuild.example'
|
||||
@@ -104,6 +107,8 @@ function emitted(name: string) {
|
||||
beforeEach(() => {
|
||||
toastAdd = vi.fn()
|
||||
refresh.mockResolvedValue(undefined)
|
||||
listUsers.mockResolvedValue(USERS)
|
||||
toastRemove.mockReset()
|
||||
transferOwnership.mockResolvedValue({
|
||||
owner: { id: 2, name: 'Jonas Weber' },
|
||||
former_owner: { id: 1, name: 'Marta Alves', role: roleRefFor('admin') },
|
||||
@@ -116,6 +121,7 @@ afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
transferOwnership.mockReset()
|
||||
refresh.mockReset()
|
||||
listUsers.mockReset()
|
||||
})
|
||||
|
||||
describe('Pick a member', () => {
|
||||
@@ -241,19 +247,6 @@ describe('Transferring', () => {
|
||||
expect(emitted('update:open')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('409 user_role_changed re-reads the session and the list, toasts and closes', async () => {
|
||||
transferOwnership.mockRejectedValue(
|
||||
new ApiError(409, undefined, 'user_role_changed', { code: 'user_role_changed' }),
|
||||
)
|
||||
await mountDialog()
|
||||
await submit()
|
||||
|
||||
expect(refresh).toHaveBeenCalledTimes(1)
|
||||
expect(emitted('stale')).toHaveLength(1)
|
||||
expect(toastAdd).toHaveBeenCalledTimes(1)
|
||||
expect(emitted('update:open')?.at(-1)).toEqual([false])
|
||||
})
|
||||
|
||||
it('403 missing_permission closes and leaves the toast to the API client', async () => {
|
||||
transferOwnership.mockRejectedValue(
|
||||
new ApiError(403, undefined, 'missing_permission', { code: 'missing_permission' }),
|
||||
@@ -288,3 +281,135 @@ describe('Transferring', () => {
|
||||
expect(emitted('update:open')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Roles sync: the target role changed first (409)', () => {
|
||||
const STALE = new ApiError(409, undefined, 'user_role_changed', { code: 'user_role_changed' })
|
||||
const JONAS_NOW_WRITER = accepted(2, 'Jonas Weber', JONAS_EMAIL, 'editor')
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
initializeLocale()
|
||||
})
|
||||
|
||||
function alertText() {
|
||||
return dialog().querySelector('[role="alert"]')?.textContent ?? ''
|
||||
}
|
||||
|
||||
function firstToast() {
|
||||
const argument = toastAdd.mock.calls[0]![0]
|
||||
return typeof argument === 'function' ? argument() : argument
|
||||
}
|
||||
|
||||
async function staleAfterSubmit() {
|
||||
transferOwnership.mockRejectedValueOnce(STALE)
|
||||
listUsers.mockResolvedValue(USERS.map((user) => (user.id === 2 ? JONAS_NOW_WRITER : user)))
|
||||
await mountDialog()
|
||||
await submit()
|
||||
}
|
||||
|
||||
it('shows the alert with Reload member, keeps the typed email and switches the primary action off', async () => {
|
||||
await staleAfterSubmit()
|
||||
|
||||
expect(alertText()).toContain('Role changed in the meantime')
|
||||
expect(alertText()).toContain(
|
||||
'Jonas Weber is now Writer. What you typed is kept. Check the change, then try again.',
|
||||
)
|
||||
expect(button('Reload member')).toBeDefined()
|
||||
expect(field().value).toBe(JONAS_EMAIL)
|
||||
expect(button('Transfer ownership')!.disabled).toBe(true)
|
||||
expect(emitted('update:open')).toBeUndefined()
|
||||
expect(toastAdd).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('re-reads the member on Reload member and turns the primary action back on', async () => {
|
||||
await staleAfterSubmit()
|
||||
listUsers.mockClear()
|
||||
|
||||
button('Reload member')!.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(listUsers).toHaveBeenCalledTimes(1)
|
||||
expect(dialog().querySelector('[role="alert"]')).toBeNull()
|
||||
expect(field().value).toBe(JONAS_EMAIL)
|
||||
expect(button('Transfer ownership')!.disabled).toBe(false)
|
||||
expect(emitted('stale')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('transfers once reloaded', async () => {
|
||||
await staleAfterSubmit()
|
||||
button('Reload member')!.click()
|
||||
await flushPromises()
|
||||
|
||||
button('Transfer ownership')!.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(transferOwnership).toHaveBeenLastCalledWith(2, JONAS_EMAIL)
|
||||
expect(emitted('transferred')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('goes back to the member list when the target is gone', async () => {
|
||||
transferOwnership.mockRejectedValueOnce(STALE)
|
||||
listUsers.mockResolvedValue(USERS.filter((user) => user.id !== 2))
|
||||
await mountDialog()
|
||||
await submit()
|
||||
|
||||
expect(dialog().textContent).toContain('Choose who becomes the owner')
|
||||
expect(emitted('stale')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('reads the alert in French', async () => {
|
||||
setLocale('fr')
|
||||
transferOwnership.mockRejectedValueOnce(STALE)
|
||||
listUsers.mockResolvedValue(USERS.map((user) => (user.id === 2 ? JONAS_NOW_WRITER : user)))
|
||||
await mountDialog()
|
||||
radio('Jonas Weber')!.click()
|
||||
await flushPromises()
|
||||
button('Continuer')!.click()
|
||||
await flushPromises()
|
||||
await type(JONAS_EMAIL)
|
||||
button('Transférer la propriété')!.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(alertText()).toContain('Rôle modifié entre-temps')
|
||||
expect(alertText()).toContain(
|
||||
'Jonas Weber a maintenant le rôle Writer. Ce que vous avez saisi est conservé. Vérifiez le changement, puis réessayez.',
|
||||
)
|
||||
expect(button('Recharger le membre')).toBeDefined()
|
||||
})
|
||||
|
||||
describe('when the dialog is gone before the answer arrives', () => {
|
||||
async function unmountedBefore409() {
|
||||
let reject!: (reason: unknown) => void
|
||||
transferOwnership.mockReturnValue(new Promise((_, fail) => (reject = fail)))
|
||||
await mountDialog()
|
||||
await submit()
|
||||
wrapper!.unmount()
|
||||
wrapper = null
|
||||
reject(STALE)
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
it('raises a warning toast that stays, with Reload', async () => {
|
||||
await unmountedBefore409()
|
||||
|
||||
const toast = firstToast()
|
||||
expect(toast.color).toBe('warning')
|
||||
expect(toast.duration).toBe(0)
|
||||
expect(toast.title).toBe('Ownership not transferred')
|
||||
expect(toast.description).toBe(
|
||||
"Jonas Weber's role changed in the meantime, so nothing was transferred. Check it and try again.",
|
||||
)
|
||||
expect(toast.actions[0].label).toBe('Reload')
|
||||
})
|
||||
|
||||
it('refetches the users and dismisses the toast on Reload', async () => {
|
||||
await unmountedBefore409()
|
||||
const toast = firstToast()
|
||||
|
||||
toast.actions[0].onClick()
|
||||
|
||||
expect(listUsers).toHaveBeenCalledTimes(1)
|
||||
expect(toastRemove).toHaveBeenCalledWith(toast.id)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user