Files
soft_usite/rest/routes/submodulos_url_test.go
T
Lizandro GuarnizoandClaude Sonnet 5 105ab44759 fix(permisos): compara la ruta completa y avisa de los menús que dan 404
Auditoría del sistema de permisos a partir del 404 en /app/doc/paginas.

1. La comparación de permisos usaba solo el último segmento de la URL
   (lo que va después del último '/'). Rompía en las dos direcciones: con
   permiso sobre /app/doc/categorias se entraba a cualquier otra ruta
   terminada en "categorias", y a la vez el permiso parecía no aplicarse
   donde sí correspondía porque el segmento coincidía por casualidad.
   Ahora se compara la ruta completa, cortando en el separador para que
   /app/doc/paginas no habilite /app/doc/paginas-privadas.

2. El submódulo de Documentación nunca se sembró, aunque las rutas y las
   vistas existen desde mayo. Había que crear el ítem del menú a mano, y
   una URL mal tipeada ahí se ve exactamente igual que un permiso mal
   asignado: un 404. Ahora se siembra con la URL correcta.

3. El submódulo "Statuspage" apuntaba a /app/statuspage, que no existe en
   ninguna parte — la página real es la pública /status. Se corrige el
   seed y también el registro ya creado en la base.

4. Chequeo en el arranque que recorre los submódulos de la base y avisa
   cuáles apuntan a una URL sin ruta. Cubre los creados a mano, que es
   justo donde el compilador y los tests no llegan.

5. Test que cruza las URLs sembradas contra las rutas registradas: fue el
   que encontró lo de statuspage.

Queda pendiente y es más grande: de 454 rutas protegidas solo 54 pasan por
MenuMiddleware. El permiso gatea la página HTML pero no los endpoints de
datos — cualquier usuario autenticado puede llamar a
GET /app/doc/loadpaginas o DELETE /app/doc/paginas/:id sin tener el
submódulo asignado. Se reporta antes de tocarlo porque cerrarlo de golpe
puede dejar gente afuera.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 09:21:44 -05:00

91 lines
2.5 KiB
Go

package routes
import (
"os"
"regexp"
"sort"
"strings"
"testing"
"github.com/gofiber/fiber/v2"
)
// Un submódulo del menú es una URL escrita a mano en el seed. Si no coincide
// con ninguna ruta registrada, el ítem aparece en el menú, se le puede asignar
// permiso a un rol, y al hacer clic da 404 — que es exactamente el síntoma
// reportado en /app/doc/paginas. El compilador no ve nada de esto porque son
// cadenas sueltas en dos archivos distintos.
func TestUrlsDeSubmodulosTienenRuta(t *testing.T) {
rutas := rutasGETRegistradas(t)
urls, err := urlsSembradas()
if err != nil {
t.Skipf("no se pudo leer el seed: %v", err)
}
if len(urls) == 0 {
t.Fatal("no se encontró ninguna URL de submódulo en el seed")
}
var rotas []string
for _, u := range urls {
if !rutas[u] {
rotas = append(rotas, u)
}
}
sort.Strings(rotas)
for _, u := range rotas {
t.Errorf("el submódulo %q no tiene ruta registrada: el menú lo muestra y da 404 al entrar", u)
}
}
// rutasGETRegistradas devuelve el conjunto de rutas GET que sirven una página.
func rutasGETRegistradas(t *testing.T) map[string]bool {
t.Helper()
// LoadRoutes monta todos los grupos: un submódulo puede apuntar a una ruta
// pública (ej. /status) y no solo a /app/*.
app := fiber.New()
LoadRoutes(app)
out := map[string]bool{}
for _, capa := range app.Stack() {
for _, r := range capa {
if r.Method == "GET" {
out[r.Path] = true
}
}
}
return out
}
// Solo las tuplas {título, descripción, url} de los slices de entries y las
// asignaciones Url:. Buscar cualquier "/app/..." suelto daba falsos positivos
// con las líneas que corrigen una URL vieja en la base.
var reURLSubmodulo = regexp.MustCompile(`\{"[^"]*",\s*"[^"]*",\s*"(/[a-z0-9/_-]+)"\}|Url:\s*"(/[a-z0-9/_-]+)"`)
// urlsSembradas saca las URLs de submódulo del seed. Se lee el fuente porque
// los seeds son literales en el código, no datos que se puedan consultar.
func urlsSembradas() ([]string, error) {
b, err := os.ReadFile("../../migrations/migrate.go")
if err != nil {
return nil, err
}
texto := string(b)
vistas := map[string]bool{}
var out []string
for _, m := range reURLSubmodulo.FindAllStringSubmatch(texto, -1) {
u := m[1]
if u == "" {
u = m[2]
}
// Solo interesan las que se usan como Url de un submódulo; las de
// redirección o comparación quedan cubiertas igual y no molestan.
if vistas[u] || strings.Contains(u, "//") {
continue
}
vistas[u] = true
out = append(out, u)
}
return out, nil
}