package services import ( "strings" "testing" ) // El caso real que lo destapó: un script de migración bien documentado, donde // cada sentencia viene debajo de su encabezado en comentarios y uno de esos // comentarios contiene un punto y coma. const scriptReal = `-- ============================================================ -- PARAMETROS WEBMASTER -- ============================================================ -- ---------------------------------------------------------- -- 1. EL SUBMODULO -- ---------------------------------------------------------- INSERT INTO submodulo (nombre, url) VALUES ('parametroswebmaster', 'p.php'); -- 2. ACCESO INSERT INTO usuario_has_submodulo (submodulo_id, usuario_id) SELECT S.id, 1 FROM submodulo S WHERE S.nombre = 'parametroswebmaster'; -- 4. COLUMNAS DE IA ALTER TABLE bot_tipos_config ADD COLUMN IF NOT EXISTS ia_activo TINYINT(1) NOT NULL DEFAULT 0; -- 5. VERIFICACION -- (cargar_menu() excluye el modulo 6 en duro; si sale 6, no aparecera) SELECT S.id, S.nombre FROM submodulo S WHERE S.nombre = 'parametroswebmaster'; SHOW COLUMNS FROM bot_tipos_config; ` func TestDividirSentenciasScriptReal(t *testing.T) { sentencias := DividirSentenciasSQL(scriptReal) if len(sentencias) != 5 { t.Fatalf("salieron %d sentencias, esperaba 5:\n%s", len(sentencias), strings.Join(sentencias, "\n---\n")) } // Cada una tiene que terminar conteniendo su SQL, aunque venga precedida // de comentarios. El parser viejo descartaba todas estas. esperados := []string{"INSERT INTO submodulo", "INSERT INTO usuario_has_submodulo", "ALTER TABLE bot_tipos_config", "SELECT S.id", "SHOW COLUMNS"} for i, esperado := range esperados { if !strings.Contains(sentencias[i], esperado) { t.Errorf("sentencia %d no contiene %q:\n%s", i, esperado, sentencias[i]) } } // El punto y coma dentro del comentario no puede haber partido nada: si // partiera, quedaría un fragmento que empieza con el resto del comentario. for _, s := range sentencias { if strings.HasPrefix(strings.TrimSpace(s), "si sale 6") { t.Error("se partió dentro de un comentario: quedó un fragmento corrupto") } } } func TestDividirSentenciasCasosBorde(t *testing.T) { casos := []struct { nombre string entrada string cuantas int }{ {"punto y coma dentro de una cadena", `INSERT INTO t VALUES ('hola; chau'); SELECT 1;`, 2}, {"comilla escapada con barra", `INSERT INTO t VALUES ('a\'; b'); SELECT 1;`, 2}, {"comilla duplicada", `INSERT INTO t VALUES ('a''; b'); SELECT 1;`, 2}, {"comentario de bloque con punto y coma", `/* uno; dos */ SELECT 1;`, 1}, {"solo comentarios no es una sentencia", "-- nada que ver\n-- tampoco esto\n", 0}, {"sin punto y coma final", "SELECT 1", 1}, {"vacío", "", 0}, {"puntos y coma de más", "SELECT 1;;; SELECT 2;", 2}, {"backticks con punto y coma", "SELECT `col;raro` FROM t; SELECT 2;", 2}, {"almohadilla como comentario", "# comentario; con punto y coma\nSELECT 1;", 1}, } for _, c := range casos { got := DividirSentenciasSQL(c.entrada) if len(got) != c.cuantas { t.Errorf("%s: salieron %d, esperaba %d → %q", c.nombre, len(got), c.cuantas, got) } } } // La regresión concreta: el filtro viejo tiraba toda sentencia que empezara // con un comentario, que en un script documentado son casi todas. func TestSentenciaConEncabezadoNoSeDescarta(t *testing.T) { s := "-- Crea la tabla\n-- Es re-ejecutable\nCREATE TABLE t (id INT)" if !TieneSQLReal(s) { t.Error("una sentencia precedida por comentarios se está descartando") } if TieneSQLReal("-- solo un comentario\n") { t.Error("un comentario suelto no es una sentencia") } // Un "--" adentro de una cadena no convierte la sentencia en comentario. if !TieneSQLReal(`SELECT '-- esto no es comentario'`) { t.Error("un guión doble dentro de una cadena no debe ocultar la sentencia") } }