UDT-005: Gestión de Permisos (RBAC) — catálogo + asignación rol↔permisos #9
@@ -7,6 +7,7 @@ import {
|
||||
Settings,
|
||||
UserPlus,
|
||||
ShieldCheck,
|
||||
KeyRound,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -117,6 +118,18 @@ export function SidebarNav() {
|
||||
<ShieldCheck className="h-4 w-4 shrink-0" />
|
||||
<span>Roles</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/admin/permisos"
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground',
|
||||
pathname.startsWith('/admin/permisos')
|
||||
? 'bg-accent text-accent-foreground font-medium'
|
||||
: 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<KeyRound className="h-4 w-4 shrink-0" />
|
||||
<span>Permisos</span>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
12
src/web/src/features/permisos/api/assignPermisos.ts
Normal file
12
src/web/src/features/permisos/api/assignPermisos.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { axiosClient } from '../../../api/axiosClient'
|
||||
import type { AssignPermisosRequest } from './types'
|
||||
|
||||
export async function assignPermisos(
|
||||
rolCodigo: string,
|
||||
payload: AssignPermisosRequest,
|
||||
): Promise<void> {
|
||||
await axiosClient.put(
|
||||
`/api/v1/roles/${encodeURIComponent(rolCodigo)}/permisos`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
9
src/web/src/features/permisos/api/getRolPermisos.ts
Normal file
9
src/web/src/features/permisos/api/getRolPermisos.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { axiosClient } from '../../../api/axiosClient'
|
||||
import type { PermisoDto } from './types'
|
||||
|
||||
export async function getRolPermisos(rolCodigo: string): Promise<PermisoDto[]> {
|
||||
const response = await axiosClient.get<PermisoDto[]>(
|
||||
`/api/v1/roles/${encodeURIComponent(rolCodigo)}/permisos`,
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
7
src/web/src/features/permisos/api/listPermisos.ts
Normal file
7
src/web/src/features/permisos/api/listPermisos.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { axiosClient } from '../../../api/axiosClient'
|
||||
import type { PermisoDto } from './types'
|
||||
|
||||
export async function listPermisos(): Promise<PermisoDto[]> {
|
||||
const response = await axiosClient.get<PermisoDto[]>('/api/v1/permisos')
|
||||
return response.data
|
||||
}
|
||||
11
src/web/src/features/permisos/api/types.ts
Normal file
11
src/web/src/features/permisos/api/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface PermisoDto {
|
||||
id: number
|
||||
codigo: string
|
||||
nombre: string
|
||||
descripcion: string | null
|
||||
modulo: string
|
||||
}
|
||||
|
||||
export interface AssignPermisosRequest {
|
||||
codigos: string[]
|
||||
}
|
||||
140
src/web/src/features/permisos/components/RolPermisosEditor.tsx
Normal file
140
src/web/src/features/permisos/components/RolPermisosEditor.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { isAxiosError } from 'axios'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AlertCircle, CheckCircle2 } from 'lucide-react'
|
||||
import { usePermisos } from '../hooks/usePermisos'
|
||||
import { useRolPermisos } from '../hooks/useRolPermisos'
|
||||
import { useAssignPermisos } from '../hooks/useAssignPermisos'
|
||||
import type { PermisoDto } from '../api/types'
|
||||
|
||||
interface RolPermisosEditorProps {
|
||||
rolCodigo: string
|
||||
}
|
||||
|
||||
function groupByModulo(permisos: PermisoDto[]): Map<string, PermisoDto[]> {
|
||||
const map = new Map<string, PermisoDto[]>()
|
||||
for (const p of permisos) {
|
||||
const modulo = p.codigo.split(':')[0] ?? p.modulo
|
||||
if (!map.has(modulo)) map.set(modulo, [])
|
||||
map.get(modulo)!.push(p)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export function RolPermisosEditor({ rolCodigo }: RolPermisosEditorProps) {
|
||||
const { data: catalogo, isLoading: loadingCatalogo, isError: errorCatalogo } = usePermisos()
|
||||
const { data: asignados, isLoading: loadingAsignados, isError: errorAsignados } = useRolPermisos(rolCodigo)
|
||||
const assignMut = useAssignPermisos()
|
||||
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
// Prefill checkboxes cuando lleguen los permisos asignados al rol
|
||||
useEffect(() => {
|
||||
if (asignados) {
|
||||
setSelected(new Set(asignados.map((p) => p.codigo)))
|
||||
setSaved(false)
|
||||
}
|
||||
}, [asignados])
|
||||
|
||||
if (loadingCatalogo || loadingAsignados) {
|
||||
return <p className="text-sm text-muted-foreground">Cargando permisos...</p>
|
||||
}
|
||||
|
||||
if (errorCatalogo || errorAsignados) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>Error al cargar los permisos del rol.</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
if (!catalogo || catalogo.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">No hay permisos registrados en el sistema.</p>
|
||||
}
|
||||
|
||||
const grupos = groupByModulo(catalogo)
|
||||
|
||||
function toggle(codigo: string) {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(codigo)) next.delete(codigo)
|
||||
else next.add(codigo)
|
||||
return next
|
||||
})
|
||||
setSaved(false)
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
assignMut.mutate(
|
||||
{ rolCodigo, payload: { codigos: Array.from(selected) } },
|
||||
{
|
||||
onSuccess: () => setSaved(true),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const saveErrMsg = assignMut.error
|
||||
? isAxiosError(assignMut.error)
|
||||
? assignMut.error.response?.status === 400
|
||||
? 'El rol "admin" no puede quedar sin permisos.'
|
||||
: (assignMut.error.response?.data as { message?: string } | undefined)?.message ??
|
||||
'No se pudieron guardar los cambios.'
|
||||
: 'No se pudieron guardar los cambios.'
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{saveErrMsg && (
|
||||
<Alert variant="destructive" role="alert">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{saveErrMsg}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{saved && (
|
||||
<Alert role="status">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
<AlertDescription>Permisos guardados correctamente.</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{Array.from(grupos.entries()).map(([modulo, permisos]) => (
|
||||
<section key={modulo}>
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-3 capitalize">
|
||||
{modulo}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{permisos.map((p) => (
|
||||
<label
|
||||
key={p.codigo}
|
||||
title={p.descripcion ?? p.codigo}
|
||||
className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm cursor-pointer hover:bg-accent hover:text-accent-foreground transition-colors"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(p.codigo)}
|
||||
onChange={() => toggle(p.codigo)}
|
||||
className="h-4 w-4 accent-primary shrink-0"
|
||||
aria-label={p.nombre}
|
||||
/>
|
||||
<span className="flex-1 truncate">{p.nombre}</span>
|
||||
<span className="font-mono text-xs text-muted-foreground/70 hidden lg:block shrink-0">
|
||||
{p.codigo}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button onClick={handleSave} disabled={assignMut.isPending}>
|
||||
{assignMut.isPending ? 'Guardando...' : 'Guardar cambios'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
23
src/web/src/features/permisos/hooks/useAssignPermisos.ts
Normal file
23
src/web/src/features/permisos/hooks/useAssignPermisos.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { assignPermisos } from '../api/assignPermisos'
|
||||
import { permisosQueryKey } from './usePermisos'
|
||||
import { rolPermisosQueryKey } from './useRolPermisos'
|
||||
import type { AssignPermisosRequest } from '../api/types'
|
||||
|
||||
interface AssignPermisosVariables {
|
||||
rolCodigo: string
|
||||
payload: AssignPermisosRequest
|
||||
}
|
||||
|
||||
export function useAssignPermisos() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ rolCodigo, payload }: AssignPermisosVariables) =>
|
||||
assignPermisos(rolCodigo, payload),
|
||||
onSuccess: (_data, { rolCodigo }) => {
|
||||
void queryClient.invalidateQueries({ queryKey: permisosQueryKey })
|
||||
void queryClient.invalidateQueries({ queryKey: rolPermisosQueryKey(rolCodigo) })
|
||||
},
|
||||
})
|
||||
}
|
||||
12
src/web/src/features/permisos/hooks/usePermisos.ts
Normal file
12
src/web/src/features/permisos/hooks/usePermisos.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { listPermisos } from '../api/listPermisos'
|
||||
|
||||
export const permisosQueryKey = ['permisos'] as const
|
||||
|
||||
export function usePermisos() {
|
||||
return useQuery({
|
||||
queryKey: permisosQueryKey,
|
||||
queryFn: listPermisos,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
}
|
||||
15
src/web/src/features/permisos/hooks/useRolPermisos.ts
Normal file
15
src/web/src/features/permisos/hooks/useRolPermisos.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getRolPermisos } from '../api/getRolPermisos'
|
||||
|
||||
export function rolPermisosQueryKey(rolCodigo: string) {
|
||||
return ['permisos', 'rol', rolCodigo] as const
|
||||
}
|
||||
|
||||
export function useRolPermisos(rolCodigo: string | null) {
|
||||
return useQuery({
|
||||
queryKey: rolPermisosQueryKey(rolCodigo ?? ''),
|
||||
queryFn: () => getRolPermisos(rolCodigo!),
|
||||
enabled: rolCodigo !== null && rolCodigo.length > 0,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
77
src/web/src/features/permisos/pages/RolPermisosPage.tsx
Normal file
77
src/web/src/features/permisos/pages/RolPermisosPage.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { useRoles } from '../../roles/hooks/useRoles'
|
||||
import { RolPermisosEditor } from '../components/RolPermisosEditor'
|
||||
|
||||
export function RolPermisosPage() {
|
||||
const navigate = useNavigate()
|
||||
const user = useAuthStore((s) => s.user)
|
||||
const [selectedRol, setSelectedRol] = useState<string | null>(null)
|
||||
|
||||
const { data: roles, isLoading: loadingRoles } = useRoles()
|
||||
|
||||
if (!user || user.rol !== 'admin') {
|
||||
void navigate('/', { replace: true })
|
||||
return null
|
||||
}
|
||||
|
||||
const rolesActivos = roles?.filter((r) => r.activo) ?? []
|
||||
|
||||
return (
|
||||
<div className="flex justify-center py-8">
|
||||
<Card className="w-full max-w-5xl">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-xl">Permisos por rol</CardTitle>
|
||||
<CardDescription>
|
||||
Seleccioná un rol para ver y editar sus permisos. Los cambios se aplican inmediatamente al guardar.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Selector de rol */}
|
||||
<div className="flex flex-col gap-1 max-w-xs">
|
||||
<label
|
||||
htmlFor="rol-selector"
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
Rol
|
||||
</label>
|
||||
{loadingRoles ? (
|
||||
<p className="text-sm text-muted-foreground">Cargando roles...</p>
|
||||
) : (
|
||||
<select
|
||||
id="rol-selector"
|
||||
value={selectedRol ?? ''}
|
||||
onChange={(e) => setSelectedRol(e.target.value || null)}
|
||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<option value="">— Seleccioná un rol —</option>
|
||||
{rolesActivos.map((r) => (
|
||||
<option key={r.codigo} value={r.codigo}>
|
||||
{r.nombre} ({r.codigo})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Grid de permisos */}
|
||||
{selectedRol ? (
|
||||
<RolPermisosEditor rolCodigo={selectedRol} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Seleccioná un rol para ver sus permisos.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { CreateUserPage } from './features/users/pages/CreateUserPage'
|
||||
import { RolesPage } from './features/roles/pages/RolesPage'
|
||||
import { NewRolPage } from './features/roles/pages/NewRolPage'
|
||||
import { EditRolPage } from './features/roles/pages/EditRolPage'
|
||||
import { RolPermisosPage } from './features/permisos/pages/RolPermisosPage'
|
||||
import { HomePage } from './pages/HomePage'
|
||||
import { PublicLayout } from './layouts/PublicLayout'
|
||||
import { ProtectedLayout } from './layouts/ProtectedLayout'
|
||||
@@ -88,6 +89,16 @@ export function AppRoutes() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/permisos"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ProtectedLayout>
|
||||
<RolPermisosPage />
|
||||
</ProtectedLayout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
)
|
||||
|
||||
153
src/web/src/tests/features/permisos/RolPermisosEditor.test.tsx
Normal file
153
src/web/src/tests/features/permisos/RolPermisosEditor.test.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { RolPermisosEditor } from '../../../features/permisos/components/RolPermisosEditor'
|
||||
|
||||
const API_URL = 'http://localhost:5000'
|
||||
|
||||
const catalogoPermisos = [
|
||||
{ id: 1, codigo: 'ventas:contado:crear', nombre: 'Crear venta contado', descripcion: 'Permite crear una venta al contado', modulo: 'ventas' },
|
||||
{ id: 2, codigo: 'ventas:contado:anular', nombre: 'Anular venta contado', descripcion: 'Permite anular una venta al contado', modulo: 'ventas' },
|
||||
{ id: 3, codigo: 'reportes:ventas:ver', nombre: 'Ver reporte ventas', descripcion: 'Permite ver el reporte de ventas', modulo: 'reportes' },
|
||||
{ id: 4, codigo: 'admin:usuarios:gestionar', nombre: 'Gestionar usuarios', descripcion: 'Permite crear y editar usuarios', modulo: 'admin' },
|
||||
]
|
||||
|
||||
const rolPermisos = [
|
||||
catalogoPermisos[0]!, // ventas:contado:crear
|
||||
catalogoPermisos[2]!, // reportes:ventas:ver
|
||||
]
|
||||
|
||||
const server = setupServer()
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
afterEach(() => server.resetHandlers())
|
||||
afterAll(() => server.close())
|
||||
|
||||
function renderEditor(rolCodigo = 'cajero') {
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
})
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<MemoryRouter>
|
||||
<RolPermisosEditor rolCodigo={rolCodigo} />
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('RolPermisosEditor', () => {
|
||||
it('renders without crash and shows permission checkboxes grouped by module', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/permisos`, () => HttpResponse.json(catalogoPermisos)),
|
||||
http.get(`${API_URL}/api/v1/roles/cajero/permisos`, () => HttpResponse.json(rolPermisos)),
|
||||
)
|
||||
|
||||
renderEditor()
|
||||
|
||||
// Loading state should appear initially
|
||||
expect(screen.getByText(/cargando permisos/i)).toBeInTheDocument()
|
||||
|
||||
// After fetch resolves, show groups
|
||||
await waitFor(() => expect(screen.getByText('ventas')).toBeInTheDocument())
|
||||
|
||||
expect(screen.getByText('reportes')).toBeInTheDocument()
|
||||
expect(screen.getByText('admin')).toBeInTheDocument()
|
||||
|
||||
// Shows permission names
|
||||
expect(screen.getByLabelText('Crear venta contado')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('Anular venta contado')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('Ver reporte ventas')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('Gestionar usuarios')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('prefills checkboxes for already-assigned permissions', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/permisos`, () => HttpResponse.json(catalogoPermisos)),
|
||||
http.get(`${API_URL}/api/v1/roles/cajero/permisos`, () => HttpResponse.json(rolPermisos)),
|
||||
)
|
||||
|
||||
renderEditor()
|
||||
|
||||
await waitFor(() => expect(screen.getByLabelText('Crear venta contado')).toBeInTheDocument())
|
||||
|
||||
const crearCheckbox = screen.getByLabelText('Crear venta contado') as HTMLInputElement
|
||||
const anularCheckbox = screen.getByLabelText('Anular venta contado') as HTMLInputElement
|
||||
const reporteCheckbox = screen.getByLabelText('Ver reporte ventas') as HTMLInputElement
|
||||
const adminCheckbox = screen.getByLabelText('Gestionar usuarios') as HTMLInputElement
|
||||
|
||||
// cajero has ventas:contado:crear and reportes:ventas:ver assigned
|
||||
expect(crearCheckbox.checked).toBe(true)
|
||||
expect(reporteCheckbox.checked).toBe(true)
|
||||
// not assigned
|
||||
expect(anularCheckbox.checked).toBe(false)
|
||||
expect(adminCheckbox.checked).toBe(false)
|
||||
})
|
||||
|
||||
it('toggles a checkbox on click', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/permisos`, () => HttpResponse.json(catalogoPermisos)),
|
||||
http.get(`${API_URL}/api/v1/roles/cajero/permisos`, () => HttpResponse.json(rolPermisos)),
|
||||
)
|
||||
|
||||
const u = userEvent.setup()
|
||||
renderEditor()
|
||||
|
||||
await waitFor(() => expect(screen.getByLabelText('Anular venta contado')).toBeInTheDocument())
|
||||
|
||||
const anularCheckbox = screen.getByLabelText('Anular venta contado') as HTMLInputElement
|
||||
expect(anularCheckbox.checked).toBe(false)
|
||||
|
||||
// Toggle on
|
||||
await u.click(anularCheckbox)
|
||||
expect(anularCheckbox.checked).toBe(true)
|
||||
|
||||
// Toggle off
|
||||
await u.click(anularCheckbox)
|
||||
expect(anularCheckbox.checked).toBe(false)
|
||||
})
|
||||
|
||||
it('shows success alert after saving permissions', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/permisos`, () => HttpResponse.json(catalogoPermisos)),
|
||||
http.get(`${API_URL}/api/v1/roles/cajero/permisos`, () => HttpResponse.json(rolPermisos)),
|
||||
http.put(`${API_URL}/api/v1/roles/cajero/permisos`, () => new HttpResponse(null, { status: 200 })),
|
||||
)
|
||||
|
||||
const u = userEvent.setup()
|
||||
renderEditor()
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Guardar cambios')).toBeInTheDocument())
|
||||
|
||||
await u.click(screen.getByText('Guardar cambios'))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('status')).toHaveTextContent(/guardados correctamente/i),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows 400 error message when admin would be left without permissions', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/permisos`, () => HttpResponse.json(catalogoPermisos)),
|
||||
http.get(`${API_URL}/api/v1/roles/admin/permisos`, () => HttpResponse.json(catalogoPermisos)),
|
||||
http.put(`${API_URL}/api/v1/roles/admin/permisos`, () =>
|
||||
HttpResponse.json({ message: 'El rol admin no puede quedar sin permisos' }, { status: 400 }),
|
||||
),
|
||||
)
|
||||
|
||||
const u = userEvent.setup()
|
||||
renderEditor('admin')
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Guardar cambios')).toBeInTheDocument())
|
||||
|
||||
await u.click(screen.getByText('Guardar cambios'))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('alert')).toHaveTextContent(/admin.*sin permisos/i),
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user