package controllers import ( "os" "path/filepath" "regexp" "strings" "testing" ) // Los mismos handlers de uMind se montan bajo /app (staff) y bajo /portal // (clientes). El middleware de scope deja la lista de tenants permitidos en el // contexto, pero NO mira el :id que llega en la URL — eso lo tiene que hacer // cada handler. Un handler nuevo que se olvide del chequeo es una fuga entre // clientes, y compila igual. // // Este test no prueba comportamiento: prueba que nadie agregue el próximo // handler sin el chequeo. Es lo único que escala a 40 handlers. var helpersDeAlcance = []string{ "accesoTenant(", "accesoAgente(", "accesoDocumento(", "accesoConexion(", "accesoAiConfigPropia(", "accesoCanal(", "accesoHerramienta(", "ConAcceso(c)", "UmindScopeDe(c)", } // sinAlcanceJustificado son los handlers que legítimamente no filtran por // tenant, cada uno con el motivo por el que no lo necesita. var sinAlcanceJustificado = map[string]string{ "UmindIndex": "renderiza la página del SPA, sin datos", "UmindPlanesPage": "renderiza la página de planes, sin datos", "GetPlantillasRubroHandler": "catálogo de plantillas de ejemplo, igual para todos", "UmindOAuthCallbackHandler": "el alcance viene en el state firmado, no en la URL", "CreateUmindTenantHandler": "crea el tenant: no hay uno previo contra el cual validar; solo staff", "GetUmindPlanesHandler": "planes del sistema, solo staff", "CreateUmindPlanHandler": "planes del sistema, solo staff", "UpdateUmindPlanHandler": "planes del sistema, solo staff", "DeleteUmindPlanHandler": "planes del sistema, solo staff", } func TestTodoHandlerUmindValidaAlcance(t *testing.T) { archivos, err := filepath.Glob("umind_*.go") if err != nil { t.Fatal(err) } re := regexp.MustCompile(`func ([A-Z]\w*)\(c \*fiber\.Ctx\) error \{`) revisados := 0 for _, f := range archivos { if strings.HasSuffix(f, "_test.go") || strings.Contains(f, "scope_helpers") { continue } b, err := os.ReadFile(f) if err != nil { t.Fatal(err) } s := string(b) for _, m := range re.FindAllStringSubmatchIndex(s, -1) { nombre := s[m[2]:m[3]] ini := m[1] fin := strings.Index(s[ini:], "\nfunc ") cuerpo := s[ini:] if fin > 0 { cuerpo = s[ini : ini+fin] } revisados++ tiene := false for _, h := range helpersDeAlcance { if strings.Contains(cuerpo, h) { tiene = true break } } if tiene { if _, exento := sinAlcanceJustificado[nombre]; exento { t.Errorf("%s (%s) ya valida alcance: sacalo de sinAlcanceJustificado", nombre, f) } continue } if _, exento := sinAlcanceJustificado[nombre]; exento { continue } t.Errorf("%s (%s) no valida el alcance del tenant.\n"+ " Usá accesoTenant/accesoAgente al entrar, o agregalo a sinAlcanceJustificado con el motivo.\n"+ " Sin esto, un cliente puede leer los datos de otro pasando el id en la URL.", nombre, f) } } if revisados < 30 { t.Fatalf("solo se revisaron %d handlers: el detector dejó de encontrarlos y el test no está protegiendo nada", revisados) } t.Logf("%d handlers uMind revisados", revisados) }