From 8ba16ed83b9ebbb1e1ffc364d49fc0eb354af3af Mon Sep 17 00:00:00 2001 From: davidpp09 Date: Tue, 11 Aug 2026 17:59:25 -0600 Subject: [PATCH] feat(productos): el ADMIN puede administrar la carta y el menu del dia Dar de alta, editar y borrar platillos era exclusivo de DEV. Se abre a ADMIN, que es el dueno y quien cambia precios de verdad. Sin esto, la pantalla de Platillos que el frontend le acaba de dar responderia 403. De paso se cierra un hueco que ya existia: el ADMIN veia Platillos del Dia en su menu, pero actualizarDia y desactivarDia no lo aceptaban, asi que la pantalla cargaba y fallaba al activar un platillo. El test compara conjuntos exactos, no contiene: avisa igual si a alguien se le va el permiso de mas hacia un MESERO. Se comprobo que sabe fallar metiendole MESERO al borrado a proposito. --- .../ordenes/ProductosController.java | 10 +- .../api/producto/PermisosProductosTest.java | 93 +++++++++++++++++++ 2 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 api/src/test/java/restaurante/api/producto/PermisosProductosTest.java diff --git a/api/src/main/java/restaurante/api/controller/ordenes/ProductosController.java b/api/src/main/java/restaurante/api/controller/ordenes/ProductosController.java index f80205f..b732286 100644 --- a/api/src/main/java/restaurante/api/controller/ordenes/ProductosController.java +++ b/api/src/main/java/restaurante/api/controller/ordenes/ProductosController.java @@ -38,7 +38,7 @@ public class ProductosController { @PostMapping @Transactional - @PreAuthorize("hasRole('DEV')") + @PreAuthorize("hasAnyRole('ADMIN', 'DEV')") public ResponseEntity registrar(@RequestBody @Valid DatosRegistroProducto datos, UriComponentsBuilder uriComponentsBuilder) { Categoria categoria = categoriaRepository.findById(datos.id_categoria()).orElseThrow(); // El nombre es único: si existe un producto con borrado suave se reutiliza ese registro @@ -63,7 +63,7 @@ public ResponseEntity> listar() { @PutMapping("/{id}") @Transactional - @PreAuthorize("hasRole('DEV')") + @PreAuthorize("hasAnyRole('ADMIN', 'DEV')") public ResponseEntity actualizar(@PathVariable Long id, @RequestBody @Valid DatosActualizacionProducto datos) { Producto producto = repository.findById(id).orElseThrow(); @@ -76,7 +76,7 @@ public ResponseEntity actualizar(@PathVariable Long id, @DeleteMapping("/{id}") @Transactional - @PreAuthorize("hasRole('DEV')") + @PreAuthorize("hasAnyRole('ADMIN', 'DEV')") public ResponseEntity eliminar(@PathVariable Long id) { Producto producto = repository.findById(id).orElseThrow(); if (ordenDetalleRepository.existePorProducto(id)) { @@ -164,7 +164,7 @@ public ResponseEntity archivarDelDia(@PathVariable Long id) { @PatchMapping("/{id}/dia") @Transactional - @PreAuthorize("hasAnyRole('DEV', 'REPARTIDOR')") + @PreAuthorize("hasAnyRole('ADMIN', 'DEV', 'REPARTIDOR')") public ResponseEntity actualizarDia(@PathVariable Long id, @RequestBody DatosActualizacionDia datos) { Producto producto = repository.findById(id).orElseThrow(); if (Boolean.TRUE.equals(datos.disponibilidad()) && !producto.getDisponibilidad()) { @@ -186,7 +186,7 @@ public ResponseEntity actualizarDia(@PathVariable Long id, @RequestBody Datos @PutMapping("/desactivar-dia/{categoriaId}") @Transactional - @PreAuthorize("hasAnyRole('DEV', 'REPARTIDOR')") + @PreAuthorize("hasAnyRole('ADMIN', 'DEV', 'REPARTIDOR')") public ResponseEntity desactivarDia(@PathVariable Long categoriaId) { repository.desactivarPorCategoria(categoriaId); return ResponseEntity.noContent().build(); diff --git a/api/src/test/java/restaurante/api/producto/PermisosProductosTest.java b/api/src/test/java/restaurante/api/producto/PermisosProductosTest.java new file mode 100644 index 0000000..dd0dd78 --- /dev/null +++ b/api/src/test/java/restaurante/api/producto/PermisosProductosTest.java @@ -0,0 +1,93 @@ +package restaurante.api.producto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.security.access.prepost.PreAuthorize; +import restaurante.api.controller.ordenes.ProductosController; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Quién puede tocar la carta. + * + * El 2026-08-11 se le dio al ADMIN la pantalla de Platillos, que hasta entonces + * era solo de DEV. El frontend por sí solo no alcanza: si el botón aparece pero + * el backend responde 403, la pantalla miente. Este test vigila el permiso real, + * que es el del servidor. + * + * Compara conjuntos EXACTOS a propósito, no "contiene". Así avisa en las dos + * direcciones: si alguien le quita el permiso al ADMIN y la pantalla se rompe, + * y —más importante— si alguien se lo regala de más a un MESERO o a COCINA y + * cualquiera desde una tablet puede borrar platillos de la carta. + * + * No levanta el contexto de Spring: lee las anotaciones de la clase, que es de + * donde Spring saca la regla. Si el método se renombra, también falla. + */ +class PermisosProductosTest { + + /** Los nombres de rol que aparecen entrecomillados dentro de hasRole/hasAnyRole. */ + private static final Pattern ROL = Pattern.compile("'([A-Z_]+)'"); + + private static Set rolesDe(String nombreMetodo) { + List metodos = Arrays.stream(ProductosController.class.getDeclaredMethods()) + .filter(m -> m.getName().equals(nombreMetodo)) + .toList(); + + assertEquals(1, metodos.size(), + "Se esperaba exactamente un método llamado '" + nombreMetodo + "' en ProductosController"); + + PreAuthorize anotacion = metodos.get(0).getAnnotation(PreAuthorize.class); + assertNotNull(anotacion, + nombreMetodo + " se quedó sin @PreAuthorize: quedaría abierto a cualquier usuario con sesión"); + + Set roles = new LinkedHashSet<>(); + Matcher m = ROL.matcher(anotacion.value()); + while (m.find()) { + roles.add(m.group(1)); + } + assertTrue(!roles.isEmpty(), + nombreMetodo + " tiene un @PreAuthorize que no nombra ningún rol: " + anotacion.value()); + return roles; + } + + @Test + @DisplayName("Dar de alta, editar y borrar platillos: solo el dueño (ADMIN) y DEV") + void administrarLaCarta_SoloAdminYDev() { + Set esperado = Set.of("ADMIN", "DEV"); + + assertEquals(esperado, rolesDe("registrar")); + assertEquals(esperado, rolesDe("actualizar")); + assertEquals(esperado, rolesDe("eliminar")); + } + + @Test + @DisplayName("Activar y apagar los platillos del día: ADMIN, DEV y el repartidor") + void menuDelDia_AdminDevYRepartidor() { + Set esperado = Set.of("ADMIN", "DEV", "REPARTIDOR"); + + // El ADMIN ya veía "Platillos del Día" en su menú, pero estos dos endpoints + // no lo aceptaban: la pantalla cargaba y fallaba al activar. Se corrigió el + // 2026-08-11 junto con lo de arriba. + assertEquals(esperado, rolesDe("actualizarDia")); + assertEquals(esperado, rolesDe("desactivarDia")); + + assertEquals(esperado, rolesDe("registrarDelDia")); + assertEquals(esperado, rolesDe("archivarDelDia")); + } + + @Test + @DisplayName("Leer la carta sí es de todos los que toman órdenes: el mesero no puede quedarse sin menú") + void listar_IncluyeAlMesero() { + assertEquals(Set.of("ADMIN", "DEV", "MESERO", "REPARTIDOR"), rolesDe("listar")); + } +}