UDT-009: Overrides de PermisosJson por usuario — cierre módulo Auth #12
87
src/web/package-lock.json
generated
87
src/web/package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@tanstack/react-query": "^5.99.0",
|
||||
"axios": "1.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -2636,6 +2637,92 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
|
||||
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-roving-focus": "1.1.11",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@tanstack/react-query": "^5.99.0",
|
||||
"axios": "1.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
53
src/web/src/components/ui/tabs.tsx
Normal file
53
src/web/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react'
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-9 items-center justify-start rounded-lg bg-muted p-1 text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
197
src/web/src/features/users/components/PermisosEditor.tsx
Normal file
197
src/web/src/features/users/components/PermisosEditor.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { isAxiosError } from 'axios'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useUserPermisos } from '../hooks/useUserPermisos'
|
||||
import { useUpdateUserPermisosOverrides } from '../hooks/useUpdateUserPermisosOverrides'
|
||||
import { usePermisos } from '@/features/permisos/hooks/usePermisos'
|
||||
import type { PermisoOverrideState } from '../types'
|
||||
import type { PermisoDto } from '@/features/permisos/api/types'
|
||||
|
||||
interface PermisosEditorProps {
|
||||
userId: number
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function resolveErrorMessage(err: unknown): string {
|
||||
if (isAxiosError(err) && err.response?.data) {
|
||||
const data = err.response.data as { title?: string; invalidCodes?: string[]; overlap?: string[] }
|
||||
if (data.title === 'invalid-permiso-codes') {
|
||||
const codes = data.invalidCodes?.join(', ') ?? ''
|
||||
return `Códigos de permiso inválidos: ${codes}`
|
||||
}
|
||||
if (data.title === 'grant-deny-overlap') {
|
||||
const codes = data.overlap?.join(', ') ?? ''
|
||||
return `Los siguientes permisos están en grant y deny al mismo tiempo: ${codes}`
|
||||
}
|
||||
return 'No se pudieron guardar los cambios.'
|
||||
}
|
||||
return 'No se pudieron guardar los cambios.'
|
||||
}
|
||||
|
||||
export function PermisosEditor({ userId }: PermisosEditorProps) {
|
||||
const { data: permisoData, isLoading: loadingPermisos } = useUserPermisos(userId)
|
||||
const { data: catalogo, isLoading: loadingCatalogo } = usePermisos()
|
||||
const mutation = useUpdateUserPermisosOverrides(userId)
|
||||
|
||||
// Map<codigopermiso, PermisoOverrideState>
|
||||
const [states, setStates] = useState<Map<string, PermisoOverrideState>>(new Map())
|
||||
const [saveError, setSaveError] = useState<string | null>(null)
|
||||
|
||||
// Initialize state from loaded data
|
||||
useEffect(() => {
|
||||
if (!permisoData) return
|
||||
const map = new Map<string, PermisoOverrideState>()
|
||||
// Start all known codes as 'heredado'
|
||||
for (const c of permisoData.rolPermisos) map.set(c, 'heredado')
|
||||
// Apply grant overrides
|
||||
for (const c of permisoData.grant) map.set(c, 'concedido')
|
||||
// Apply deny overrides
|
||||
for (const c of permisoData.deny) map.set(c, 'denegado')
|
||||
setStates(map)
|
||||
setSaveError(null)
|
||||
}, [permisoData])
|
||||
|
||||
if (loadingPermisos || loadingCatalogo) {
|
||||
return <p className="text-sm text-muted-foreground">Cargando permisos...</p>
|
||||
}
|
||||
|
||||
if (!permisoData || !catalogo) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>No se pudieron cargar los permisos.</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
// Build complete set of all relevant permission codes from catalog
|
||||
// Filter catalog to only show permisos that appear in rolPermisos, grant, deny, or all catalog
|
||||
const allCodes = new Set([
|
||||
...permisoData.rolPermisos,
|
||||
...permisoData.grant,
|
||||
...permisoData.deny,
|
||||
])
|
||||
|
||||
// Use catalog for grouping and names, showing all permisos known plus any from overrides
|
||||
const relevantPermisos = catalogo.filter(
|
||||
(p) => allCodes.has(p.codigo) || catalogo.length <= 30,
|
||||
)
|
||||
|
||||
const grupos = groupByModulo(relevantPermisos)
|
||||
|
||||
function getState(codigo: string): PermisoOverrideState {
|
||||
return states.get(codigo) ?? 'heredado'
|
||||
}
|
||||
|
||||
function setState(codigo: string, state: PermisoOverrideState) {
|
||||
setSaveError(null)
|
||||
setStates((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(codigo, state)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
const grant: string[] = []
|
||||
const deny: string[] = []
|
||||
|
||||
for (const [codigo, state] of states.entries()) {
|
||||
if (state === 'concedido') grant.push(codigo)
|
||||
else if (state === 'denegado') deny.push(codigo)
|
||||
}
|
||||
|
||||
mutation.mutate(
|
||||
{ grant, deny },
|
||||
{
|
||||
onError: (err) => {
|
||||
setSaveError(resolveErrorMessage(err))
|
||||
},
|
||||
onSuccess: () => {
|
||||
setSaveError(null)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{saveError && (
|
||||
<Alert variant="destructive" role="alert">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{saveError}</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="space-y-2">
|
||||
{permisos.map((p) => {
|
||||
const currentState = getState(p.codigo)
|
||||
return (
|
||||
<div
|
||||
key={p.codigo}
|
||||
data-testid={`permiso-row-${p.codigo}`}
|
||||
className="flex items-center justify-between rounded-md border border-border px-3 py-2"
|
||||
>
|
||||
<div className="flex flex-col min-w-0 mr-4">
|
||||
<span className="text-sm font-medium">{p.nombre}</span>
|
||||
<span className="font-mono text-xs text-muted-foreground/70">{p.codigo}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{(['heredado', 'concedido', 'denegado'] as PermisoOverrideState[]).map(
|
||||
(state) => (
|
||||
<button
|
||||
key={state}
|
||||
type="button"
|
||||
role="button"
|
||||
aria-label={state.charAt(0).toUpperCase() + state.slice(1)}
|
||||
aria-pressed={currentState === state}
|
||||
onClick={() => setState(p.codigo, state)}
|
||||
className={`
|
||||
px-2 py-1 rounded text-xs font-medium transition-colors capitalize
|
||||
${
|
||||
currentState === state
|
||||
? state === 'heredado'
|
||||
? 'bg-secondary text-secondary-foreground'
|
||||
: state === 'concedido'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100'
|
||||
: 'bg-transparent text-muted-foreground hover:bg-accent'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{state}
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button onClick={handleSave} disabled={mutation.isPending}>
|
||||
{mutation.isPending ? 'Guardando...' : 'Guardar cambios'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
197
src/web/src/tests/features/users/PermisosEditor.test.tsx
Normal file
197
src/web/src/tests/features/users/PermisosEditor.test.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'
|
||||
import { render, screen, waitFor, within } 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 { PermisosEditor } from '../../../features/users/components/PermisosEditor'
|
||||
|
||||
const API_URL = 'http://localhost:5000'
|
||||
|
||||
// Catalog of ALL known permissions (from /api/v1/permisos)
|
||||
const catalogoPermisos = [
|
||||
{ id: 1, codigo: 'ventas:contado:crear', nombre: 'Crear venta contado', descripcion: null, modulo: 'ventas' },
|
||||
{ id: 2, codigo: 'ventas:contado:cobrar', nombre: 'Cobrar venta contado', descripcion: null, modulo: 'ventas' },
|
||||
{ id: 3, codigo: 'textos:editar', nombre: 'Editar textos', descripcion: null, modulo: 'textos' },
|
||||
]
|
||||
|
||||
// User permisos — from /api/v1/users/42/permisos
|
||||
const mockUsuarioPermisos = {
|
||||
usuarioId: 42,
|
||||
rol: 'cajero',
|
||||
rolPermisos: ['ventas:contado:crear', 'ventas:contado:cobrar'],
|
||||
grant: ['textos:editar'],
|
||||
deny: ['ventas:contado:cobrar'],
|
||||
effective: ['ventas:contado:crear', 'textos:editar'],
|
||||
}
|
||||
|
||||
const server = setupServer()
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
afterEach(() => server.resetHandlers())
|
||||
afterAll(() => server.close())
|
||||
|
||||
function renderEditor(userId = 42) {
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
})
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<MemoryRouter>
|
||||
<PermisosEditor userId={userId} />
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
function setupHandlers() {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/permisos`, () => HttpResponse.json(catalogoPermisos)),
|
||||
http.get(`${API_URL}/api/v1/users/42/permisos`, () => HttpResponse.json(mockUsuarioPermisos)),
|
||||
)
|
||||
}
|
||||
|
||||
describe('PermisosEditor', () => {
|
||||
it('calls GET /api/v1/users/:id/permisos on mount', async () => {
|
||||
let getPermisosCallCount = 0
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/permisos`, () => HttpResponse.json(catalogoPermisos)),
|
||||
http.get(`${API_URL}/api/v1/users/42/permisos`, () => {
|
||||
getPermisosCallCount++
|
||||
return HttpResponse.json(mockUsuarioPermisos)
|
||||
}),
|
||||
)
|
||||
|
||||
renderEditor(42)
|
||||
|
||||
await waitFor(() => expect(getPermisosCallCount).toBe(1))
|
||||
})
|
||||
|
||||
it('renders permissions grouped by module', async () => {
|
||||
setupHandlers()
|
||||
renderEditor()
|
||||
|
||||
await waitFor(() => expect(screen.getByText('ventas')).toBeInTheDocument())
|
||||
|
||||
expect(screen.getByText('textos')).toBeInTheDocument()
|
||||
expect(screen.getByText('Crear venta contado')).toBeInTheDocument()
|
||||
expect(screen.getByText('Cobrar venta contado')).toBeInTheDocument()
|
||||
expect(screen.getByText('Editar textos')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows Heredado state for permissions in role but not in grant or deny', async () => {
|
||||
setupHandlers()
|
||||
renderEditor()
|
||||
|
||||
// ventas:contado:crear is in rolPermisos, not in grant, not in deny
|
||||
// expect a button/element indicating "Heredado" is active for that permission
|
||||
await waitFor(() => expect(screen.getByText('Crear venta contado')).toBeInTheDocument())
|
||||
|
||||
// Should have a "Heredado" indicator active for ventas:contado:crear
|
||||
// We look for the specific row container and check the selected state
|
||||
const crearRow = screen.getByTestId('permiso-row-ventas:contado:crear')
|
||||
expect(within(crearRow).getByRole('button', { name: /heredado/i })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'true',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows Concedido state for permissions in grant', async () => {
|
||||
setupHandlers()
|
||||
renderEditor()
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Editar textos')).toBeInTheDocument())
|
||||
|
||||
const editarRow = screen.getByTestId('permiso-row-textos:editar')
|
||||
expect(within(editarRow).getByRole('button', { name: /concedido/i })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'true',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows Denegado state for permissions in deny', async () => {
|
||||
setupHandlers()
|
||||
renderEditor()
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Cobrar venta contado')).toBeInTheDocument())
|
||||
|
||||
const cobrarRow = screen.getByTestId('permiso-row-ventas:contado:cobrar')
|
||||
expect(within(cobrarRow).getByRole('button', { name: /denegado/i })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'true',
|
||||
)
|
||||
})
|
||||
|
||||
it('Guardar button calls PUT with correct { grant, deny } body', async () => {
|
||||
let capturedBody: unknown = null
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/permisos`, () => HttpResponse.json(catalogoPermisos)),
|
||||
http.get(`${API_URL}/api/v1/users/42/permisos`, () => HttpResponse.json(mockUsuarioPermisos)),
|
||||
http.put(`${API_URL}/api/v1/users/42/permisos/overrides`, async ({ request }) => {
|
||||
capturedBody = await request.json()
|
||||
return HttpResponse.json(mockUsuarioPermisos)
|
||||
}),
|
||||
)
|
||||
|
||||
const u = userEvent.setup()
|
||||
renderEditor()
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Guardar cambios')).toBeInTheDocument())
|
||||
|
||||
await u.click(screen.getByRole('button', { name: /guardar cambios/i }))
|
||||
|
||||
await waitFor(() => expect(capturedBody).not.toBeNull())
|
||||
// Initial state: grant=['textos:editar'], deny=['ventas:contado:cobrar']
|
||||
expect(capturedBody).toMatchObject({
|
||||
grant: expect.arrayContaining(['textos:editar']),
|
||||
deny: expect.arrayContaining(['ventas:contado:cobrar']),
|
||||
})
|
||||
})
|
||||
|
||||
it('shows alert on 400 invalid-permiso-codes error', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/permisos`, () => HttpResponse.json(catalogoPermisos)),
|
||||
http.get(`${API_URL}/api/v1/users/42/permisos`, () => HttpResponse.json(mockUsuarioPermisos)),
|
||||
http.put(`${API_URL}/api/v1/users/42/permisos/overrides`, () =>
|
||||
HttpResponse.json(
|
||||
{ title: 'invalid-permiso-codes', status: 400, invalidCodes: ['fake:codigo'] },
|
||||
{ status: 400 },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const u = userEvent.setup()
|
||||
renderEditor()
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Guardar cambios')).toBeInTheDocument())
|
||||
await u.click(screen.getByRole('button', { name: /guardar cambios/i }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument(),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows alert on 400 grant-deny-overlap error', async () => {
|
||||
server.use(
|
||||
http.get(`${API_URL}/api/v1/permisos`, () => HttpResponse.json(catalogoPermisos)),
|
||||
http.get(`${API_URL}/api/v1/users/42/permisos`, () => HttpResponse.json(mockUsuarioPermisos)),
|
||||
http.put(`${API_URL}/api/v1/users/42/permisos/overrides`, () =>
|
||||
HttpResponse.json(
|
||||
{ title: 'grant-deny-overlap', status: 400, overlap: ['textos:editar'] },
|
||||
{ status: 400 },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
const u = userEvent.setup()
|
||||
renderEditor()
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Guardar cambios')).toBeInTheDocument())
|
||||
await u.click(screen.getByRole('button', { name: /guardar cambios/i }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument(),
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user