test_that("SQLite DB inicializa correctamente", { skip_on_cran() con <- get_cie10_db() expect_s4_class(con, "SQLiteConnection") expect_true(DBI::dbExistsTable(con, "cie10")) }) test_that("cie10_sql ejecuta queries SELECT", { skip_on_cran() resultado <- cie10_sql("SELECT COUNT(*) AS n FROM cie10") expect_s3_class(resultado, "tbl_df") expect_gt(resultado$n, 5000) # Minimo 5k codigos }) test_that("cie10_sql bloquea queries peligrosas", { skip_on_cran() expect_error( cie10_sql("DROP TABLE cie10"), "Solo queries SELECT" ) }) test_that("cie10_sql emite deprecation warning para argumento close", { skip_on_cran() expect_warning( cie10_sql("SELECT COUNT(*) AS n FROM cie10", close = FALSE), "deprecado" ) }) # ============================================================ # PRUEBAS ADICIONALES cie10_sql() # ============================================================ test_that("cie10_sql ejecuta queries SQL con clausulas WHERE/LIKE/GROUP/ORDER", { skip_on_cran() # WHERE retorna codigo exacto con contenido correcto r_where <- cie10_sql("SELECT * FROM cie10 WHERE codigo = 'E11.0'") expect_equal(nrow(r_where), 1) expect_true(grepl("iabetes", r_where$descripcion, ignore.case = TRUE)) # LIKE filtra por prefijo r_like <- cie10_sql("SELECT * FROM cie10 WHERE codigo LIKE 'E11%' LIMIT 10") expect_true(all(grepl("^E11", r_like$codigo))) # GROUP BY retorna capitulos r_group <- cie10_sql("SELECT capitulo, COUNT(*) as n FROM cie10 GROUP BY capitulo") expect_gt(nrow(r_group), 10) # ORDER BY ordena correctamente r_order <- cie10_sql("SELECT codigo FROM cie10 ORDER BY codigo LIMIT 5") expect_equal(r_order$codigo, sort(r_order$codigo)) }) test_that("cie10_sql bloquea ALTER TABLE", { skip_on_cran() # Bloquea por keyword peligroso o por no ser SELECT expect_error( cie10_sql("ALTER TABLE cie10 ADD COLUMN test TEXT") ) }) test_that("cie10_sql bloquea CREATE TABLE", { skip_on_cran() expect_error( cie10_sql("CREATE TABLE test (id INTEGER)") ) }) test_that("cie10_sql bloquea TRUNCATE", { skip_on_cran() expect_error( cie10_sql("TRUNCATE TABLE cie10") ) }) test_that("cie10_sql bloquea ATTACH DATABASE", { skip_on_cran() expect_error( cie10_sql("ATTACH DATABASE 'test.db' AS test") ) }) test_that("cie10_sql bloquea PRAGMA", { skip_on_cran() expect_error( cie10_sql("PRAGMA table_info(cie10)") ) }) test_that("cie10_sql bloquea keywords peligrosos case-insensitive", { skip_on_cran() expect_error(cie10_sql("SELECT * FROM cie10; dRoP TABLE cie10")) expect_error(cie10_sql("SELECT * FROM cie10; Pragma table_info(cie10)")) expect_error(cie10_sql("SELECT * FROM cie10; aTTaCH DATABASE 'x' AS y")) }) test_that("cie10_sql permite DISTINCT", { skip_on_cran() resultado <- cie10_sql("SELECT DISTINCT capitulo FROM cie10") expect_s3_class(resultado, "tbl_df") expect_gt(nrow(resultado), 0) }) test_that("cie10_sql permite subqueries y UNION", { skip_on_cran() # Subquery en WHERE con IN (SELECT ...) r_sub <- cie10_sql("SELECT * FROM cie10 WHERE codigo IN (SELECT codigo FROM cie10 WHERE codigo = 'E11.0')") expect_equal(nrow(r_sub), 1) # UNION de dos SELECT validos r_union <- cie10_sql("SELECT codigo, descripcion FROM cie10 WHERE codigo = 'E11.0' UNION SELECT codigo, descripcion FROM cie10 WHERE codigo = 'I10'") expect_gte(nrow(r_union), 1) }) test_that("cie10_sql permite COUNT con condicion", { skip_on_cran() resultado <- cie10_sql("SELECT COUNT(*) as n FROM cie10 WHERE codigo LIKE 'E%'") expect_s3_class(resultado, "tbl_df") expect_gt(resultado$n, 100) }) test_that("cie10_sql maneja query con saltos de linea", { skip_on_cran() query <- " SELECT codigo, descripcion FROM cie10 WHERE codigo = 'E11.0' " resultado <- cie10_sql(query) expect_s3_class(resultado, "tbl_df") expect_equal(nrow(resultado), 1) }) # ============================================================ # PRUEBAS get_cie10_db() # ============================================================ test_that("get_cie10_db retorna conexion DBI valida", { skip_on_cran() con <- ciecl:::get_cie10_db() expect_true(DBI::dbIsValid(con)) expect_s4_class(con, "SQLiteConnection") }) test_that("get_cie10_db crea tabla cie10 si no existe", { skip_on_cran() con <- ciecl:::get_cie10_db() expect_true(DBI::dbExistsTable(con, "cie10")) }) test_that("get_cie10_db tabla tiene indices", { skip_on_cran() con <- ciecl:::get_cie10_db() indices <- DBI::dbGetQuery(con, "SELECT name FROM sqlite_master WHERE type='index'") expect_gt(nrow(indices), 0) }) test_that("get_cie10_db usa directorio cache correcto", { skip_on_cran() cache_dir <- tools::R_user_dir("ciecl", "data") db_path <- file.path(cache_dir, "cie10.db") con <- ciecl:::get_cie10_db() expect_true(file.exists(db_path)) }) test_that("get_cie10_db tabla tiene columnas esperadas", { skip_on_cran() con <- ciecl:::get_cie10_db() columnas <- DBI::dbListFields(con, "cie10") expect_true("codigo" %in% columnas) expect_true("descripcion" %in% columnas) }) # ============================================================ # PRUEBAS cie10_clear_cache() # ============================================================ test_that("cie10_clear_cache elimina archivo db", { skip_on_cran() cache_dir <- tools::R_user_dir("ciecl", "data") db_path <- file.path(cache_dir, "cie10.db") # Asegurar que existe ciecl:::get_cie10_db() expect_true(file.exists(db_path)) # Limpiar cache suppressMessages(cie10_clear_cache()) expect_false(file.exists(db_path)) }) test_that("cie10_clear_cache es idempotente", { skip_on_cran() expect_no_error({ suppressMessages(cie10_clear_cache()) suppressMessages(cie10_clear_cache()) }) }) test_that("cie10_clear_cache emite mensaje apropiado", { skip_on_cran() # Asegurar que existe cache ciecl:::get_cie10_db() expect_message(cie10_clear_cache(), "eliminado") expect_message(cie10_clear_cache(), "no existe") }) test_that("cie10_clear_cache retorna invisible NULL", { skip_on_cran() resultado <- suppressMessages(cie10_clear_cache()) expect_null(resultado) }) # ============================================================ # PRUEBAS ADICIONALES COBERTURA - Semicolon dentro de strings # ============================================================ test_that("cie10_sql permite semicolon dentro de strings", { skip_on_cran() # Semicolon dentro de comillas simples no debe bloquearse resultado <- cie10_sql("SELECT codigo, descripcion FROM cie10 WHERE descripcion LIKE '%tipo;%' LIMIT 1") expect_s3_class(resultado, "tbl_df") }) test_that("cie10_sql bloquea semicolon fuera de strings", { skip_on_cran() # Multiples statements separados por semicolon (sin keywords peligrosos) expect_error( cie10_sql("SELECT * FROM cie10; SELECT * FROM cie10"), "Multiples statements" ) }) test_that("cie10_sql maneja comentarios SQL sin falsos positivos", { skip_on_cran() # Comentario de linea no genera falso positivo resultado <- cie10_sql("SELECT codigo FROM cie10 WHERE codigo = 'E11.0' -- comentario") expect_equal(nrow(resultado), 1) # Comentario de bloque no genera falso positivo resultado2 <- cie10_sql("SELECT codigo /* columna */ FROM cie10 WHERE codigo = 'E11.0'") expect_equal(nrow(resultado2), 1) # Semicolon dentro de comentario no bloquea resultado3 <- cie10_sql("SELECT codigo FROM cie10 WHERE codigo = 'E11.0' -- ;test") expect_equal(nrow(resultado3), 1) }) test_that("cie10_sql bloquea INSERT", { skip_on_cran() expect_error( cie10_sql("INSERT INTO cie10 VALUES ('X99', 'Test')"), "Solo queries SELECT" ) }) test_that("cie10_sql bloquea UPDATE", { skip_on_cran() expect_error( cie10_sql("UPDATE cie10 SET descripcion = 'test' WHERE codigo = 'E11.0'"), "Solo queries SELECT" ) }) test_that("cie10_sql bloquea DELETE", { skip_on_cran() expect_error( cie10_sql("DELETE FROM cie10 WHERE codigo = 'E11.0'"), "Solo queries SELECT" ) }) test_that("cie10_sql bloquea DETACH", { skip_on_cran() expect_error( cie10_sql("DETACH DATABASE main"), "Solo queries SELECT" ) }) test_that("cie10_sql bloquea EXEC", { skip_on_cran() expect_error( cie10_sql("EXEC sp_help"), "Solo queries SELECT" ) }) # ============================================================ # PRUEBAS get_cie10_db() creacion directorio cache # ============================================================ test_that("get_cie10_db crea directorio cache si no existe", { skip_on_cran() # Limpiar cache para forzar recreacion suppressMessages(cie10_clear_cache()) cache_dir <- tools::R_user_dir("ciecl", "data") # Conectar - debe crear directorio si no existe ciecl:::get_cie10_db() # Verificar que directorio existe expect_true(dir.exists(cache_dir)) }) test_that("get_cie10_db inicializa indices en DB nueva", { skip_on_cran() # Limpiar cache suppressMessages(cie10_clear_cache()) # Conectar - debe inicializar DB (mensajes solo en interactive) con <- ciecl:::get_cie10_db() # Verificar indices indices <- DBI::dbGetQuery(con, "SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_%'") expect_true("idx_codigo" %in% indices$name) expect_true("idx_desc" %in% indices$name) }) # ============================================================ # PRUEBAS CONNECTION POOLING # ============================================================ test_that("get_cie10_db reutiliza conexion (pooling)", { skip_on_cran() con1 <- ciecl:::get_cie10_db() con2 <- ciecl:::get_cie10_db() # Misma referencia de objeto expect_identical(con1, con2) }) test_that("cie10_clear_cache invalida pool", { skip_on_cran() env <- getFromNamespace(".ciecl_env", "ciecl") con1 <- ciecl:::get_cie10_db() suppressMessages(cie10_clear_cache()) # Pool debe estar vacio expect_null(env$con) expect_null(env$db_path) # Nueva conexion debe ser diferente con2 <- ciecl:::get_cie10_db() expect_true(DBI::dbIsValid(con2)) }) test_that("cie10_disconnect cierra conexion pooled", { skip_on_cran() env <- getFromNamespace(".ciecl_env", "ciecl") # Asegurar conexion activa ciecl:::get_cie10_db() expect_false(is.null(env$con)) # Desconectar cie10_disconnect() expect_null(env$con) expect_null(env$db_path) }) test_that("cie10_disconnect es idempotente", { skip_on_cran() expect_no_error({ cie10_disconnect() cie10_disconnect() }) }) # ============================================================ # PRUEBAS CACHE VERSIONADO # ============================================================ test_that("cache incluye tabla cie10_meta con version", { skip_on_cran() con <- ciecl:::get_cie10_db() expect_true(DBI::dbExistsTable(con, "cie10_meta")) meta <- DBI::dbGetQuery(con, "SELECT * FROM cie10_meta WHERE key = 'cache_version'") expect_equal(nrow(meta), 1) expect_equal(meta$value, as.character(utils::packageVersion("ciecl"))) }) test_that("cache_is_current retorna TRUE para cache actual", { skip_on_cran() con <- ciecl:::get_cie10_db() expect_true(ciecl:::cache_is_current(con)) }) test_that("cache_is_current retorna FALSE si no hay tabla meta", { skip_on_cran() # Crear db temporal sin tabla meta tmp <- tempfile(fileext = ".db") on.exit(unlink(tmp), add = TRUE) con_tmp <- DBI::dbConnect(RSQLite::SQLite(), tmp) on.exit(DBI::dbDisconnect(con_tmp), add = TRUE) DBI::dbWriteTable(con_tmp, "cie10", data.frame(codigo = "E11.0")) expect_false(ciecl:::cache_is_current(con_tmp)) }) test_that("version mismatch fuerza rebuild", { skip_on_cran() # Obtener conexion valida con <- ciecl:::get_cie10_db() # Manipular version en meta DBI::dbExecute(con, "UPDATE cie10_meta SET value = '0.0.0' WHERE key = 'cache_version'") # Desconectar correctamente antes de forzar rebuild # (necesario en Windows para liberar lock del archivo) cie10_disconnect() # get_cie10_db debe detectar version mismatch y reconstruir con_new <- ciecl:::get_cie10_db() expect_true(DBI::dbIsValid(con_new)) # Version debe ser correcta ahora meta <- DBI::dbGetQuery(con_new, "SELECT value FROM cie10_meta WHERE key = 'cache_version'") expect_equal(meta$value, as.character(utils::packageVersion("ciecl"))) }) # ============================================================ # PRUEBAS BUILD ATOMICO # ============================================================ test_that("build_cache_atomic crea cache completo", { skip_on_cran() # Limpiar suppressMessages(cie10_clear_cache()) cache_dir <- tools::R_user_dir("ciecl", "data") db_path <- file.path(cache_dir, "cie10.db") # No debe existir .tmp residual despues de build exitoso expect_false(file.exists(paste0(db_path, ".tmp"))) con <- ciecl:::get_cie10_db() expect_true(DBI::dbExistsTable(con, "cie10")) expect_true(DBI::dbExistsTable(con, "cie10_fts")) expect_true(DBI::dbExistsTable(con, "cie10_meta")) }) # ============================================================ # PRUEBAS .ciecl_env y .onUnload (zzz.R) # ============================================================ test_that(".ciecl_env tiene estructura correcta", { env <- ciecl:::.ciecl_env expect_true(is.environment(env)) expect_true("con" %in% ls(env, all.names = TRUE)) expect_true("db_path" %in% ls(env, all.names = TRUE)) }) test_that(".onUnload cierra conexion y limpia env", { skip_on_cran() ciecl:::get_cie10_db() env <- ciecl:::.ciecl_env expect_false(is.null(env$con)) onUnload <- getFromNamespace(".onUnload", "ciecl") onUnload(libpath = .libPaths()[1]) expect_null(env$con) expect_null(env$db_path) }) test_that(".onUnload no falla sin conexion activa", { ciecl:::cie10_disconnect() onUnload <- getFromNamespace(".onUnload", "ciecl") expect_no_error(onUnload(libpath = .libPaths()[1])) }) # ============================================================ # PRUEBAS BRANCHES ADICIONALES cie-sql.R # ============================================================ test_that("get_cie10_db reconstruye FTS5 si falta en pooled", { skip_on_cran() con <- ciecl:::get_cie10_db() DBI::dbExecute(con, "DROP TABLE IF EXISTS cie10_fts") # Forzar re-check via pooling (misma conexion valida, mismo path) con2 <- ciecl:::get_cie10_db() tablas <- DBI::dbListTables(con2) expect_true(any(grepl("cie10_fts", tablas))) ciecl::cie10_disconnect() }) test_that("get_cie10_db reconstruye si version no coincide en pooled", { skip_on_cran() con <- ciecl:::get_cie10_db() # Modificar version en pooled connection -> trigger rebuild en pooling path DBI::dbExecute(con, "UPDATE cie10_meta SET value = '0.0.0' WHERE key = 'cache_version'") con2 <- ciecl:::get_cie10_db() ver <- DBI::dbGetQuery(con2, "SELECT value FROM cie10_meta WHERE key = 'cache_version'") expect_equal(ver$value, as.character(utils::packageVersion("ciecl"))) ciecl::cie10_disconnect() }) test_that("get_cie10_db limpia conexion invalida", { skip_on_cran() on.exit(ciecl::cie10_disconnect(), add = TRUE) con <- ciecl:::get_cie10_db() DBI::dbDisconnect(con) # .ciecl_env aun tiene referencia invalida con2 <- ciecl:::get_cie10_db() expect_true(DBI::dbIsValid(con2)) ciecl::cie10_disconnect() }) test_that("get_cie10_db reconstruye si tabla cie10 falta", { skip_on_cran() ciecl::cie10_disconnect() con_direct <- DBI::dbConnect(RSQLite::SQLite(), file.path(tools::R_user_dir("ciecl", "data"), "cie10.db")) on.exit(try(DBI::dbDisconnect(con_direct), silent = TRUE), add = TRUE) DBI::dbExecute(con_direct, "DROP TABLE IF EXISTS cie10") DBI::dbDisconnect(con_direct) con2 <- ciecl:::get_cie10_db() expect_true(DBI::dbExistsTable(con2, "cie10")) ciecl::cie10_disconnect() }) test_that("cie10_sql maneja error de ejecucion SQL", { skip_on_cran() expect_error(ciecl::cie10_sql("SELECT * FROM tabla_inexistente")) }) test_that("cache_is_current retorna FALSE cuando query falla", { skip_on_cran() tmp_db <- tempfile(fileext = ".db") on.exit(unlink(tmp_db), add = TRUE) con <- DBI::dbConnect(RSQLite::SQLite(), tmp_db) on.exit(try(DBI::dbDisconnect(con), silent = TRUE), add = TRUE) # Crear tabla cie10_meta con schema incorrecto para que el SELECT falle DBI::dbExecute(con, "CREATE TABLE cie10_meta (x INTEGER)") expect_false(ciecl:::cache_is_current(con)) }) test_that("build_cache_atomic limpia .tmp residual", { skip_on_cran() on.exit(ciecl::cie10_disconnect(), add = TRUE) ciecl::cie10_disconnect() cache_dir <- tools::R_user_dir("ciecl", "data") tmp_file <- file.path(cache_dir, "cie10.db.tmp") if (!dir.exists(cache_dir)) dir.create(cache_dir, recursive = TRUE) file.create(tmp_file) expect_true(file.exists(tmp_file)) suppressMessages(ciecl:::cie10_clear_cache()) con <- ciecl:::get_cie10_db() expect_false(file.exists(tmp_file)) ciecl::cie10_disconnect() })