tests/testthat/test-safe_output.R

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#
# - - - - - - - - - - - - - - - - - ERRORS - - - - - - - - - - - - - - - - - #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#
test_that("safe_output fails when calling with invalid object", {
  expect_error(
    dsROCrate::safe_output(structure(list(), class = "InvalidClass"))
  )
})

test_that("safe_output fails when attempt calling with invalid connection", {
  # setup
  ## create basic RO-Crate
  basic_rocrate <- rocrateR::rocrate_5s()

  expect_error(
    basic_rocrate |>
      dsROCrate::safe_output(connection = NULL)
  )
})

test_that("safe_output warns when calling with RO-Crate without Safe People details", {
  # setup
  ## open connection to OBiBa's Opal demo server
  opal_con <- opal_demo_con()
  ## create basic RO-Crate
  basic_rocrate <- rocrateR::rocrate_5s()

  expect_warning(
    basic_rocrate |>
      dsROCrate::safe_output(connection = opal_con)
  )

  # close connection to OBiBa's Opal demo server
  opalr::opal.logout(opal_con)
})

test_that("safe_output warns when attempt setting multiple users as the RO-Crate authors", {
  # setup
  ## open connection to OBiBa's Opal demo server
  opal_con <- opal_demo_con()
  ## create basic RO-Crate
  basic_rocrate <- rocrateR::rocrate_5s()

  expect_error(
    basic_rocrate |>
      dsROCrate::safe_project(
        connection = opal_con,
        project = attr(opal_con, "PROJECT")
      ) |>
      dsROCrate::safe_people(user = 1) |>
      dsROCrate::safe_people(user = 2) |>
      rocrateR::add_entity_value(
        id = "./",
        key = "author",
        value = lapply(
          c(
            "#person:6717f2823d3202449301145073ab8719",
            "#person:db8e490a925a60e62212cefc7674ca02"
          ),
          \(x) list(`@id` = x)
        )
      ) |>
      dsROCrate::safe_output(connection = opal_con, user = NULL)
  )

  # close connection to OBiBa's Opal demo server
  opalr::opal.logout(opal_con)
})

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#
# - - - - - - - - - - - - - - - - - VALID  - - - - - - - - - - - - - - - - - #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#
test_that("safe_output works", {
  # setup
  ## open connection to OBiBa's Opal demo server
  opal_con <- opal_demo_con()
  ## create basic RO-Crate
  basic_rocrate <- rocrateR::rocrate_5s()

  # add Safe People details
  ## add project entity to the RO-Crate
  basic_rocrate_2 <- basic_rocrate |>
    rocrateR::add_entity(rocrateR::entity("Fake Project", type = "Project"))

  ## add Safe People details for
  basic_rocrate_3 <- basic_rocrate_2 |>
    dsROCrate::safe_people(
      connection = opal_con,
      user = attr(opal_con, "PEOPLE")
    )

  # call the function with an RO-Crate with multiple users
  basic_rocrate_4 <- basic_rocrate_3 |>
    # add another username
    dsROCrate::safe_people(
      connection = opal_con,
      user = list(id = "extra_username", name = "Extra username"),
      set_author = FALSE
    ) |>
    # set multiple users as the author
    rocrateR::add_entity_value(
      id = "./",
      key = "author",
      value = list(
        list(`@id` = "extra_username"),
        list(`@id` = paste0("#person:", digest::digest("dsuser")))
      ),
      overwrite = TRUE
    )

  # test for warning about multiple authors in the root entity
  expect_warning(
    basic_rocrate_4 |>
      dsROCrate::safe_output(connection = opal_con)
  )

  # close connection to OBiBa's Opal demo server
  opalr::opal.logout(opal_con)

  # simulate DataSHIELD operations
  ## run some test commands with dsBaseClient
  ### needed to defined the OpalDriver class in the current environment
  DSOpal::Opal()
  ### create new login object, note that here we use the `dsuser`
  builder <- DSI::newDSLoginBuilder()
  builder$append(
    server = "study1",
    url = attr(opal_con, "SERVER"),
    user = "dsuser",
    password = attr(opal_con, "DSUSERPASS"),
    driver = "OpalDriver"
  )
  logindata <- builder$build()
  conns <- DSI::datashield.login(logins = logindata)

  ### assign data
  DSI::datashield.assign.table(
    conns["study1"],
    symbol = "dsROCrate_test",
    table = paste0(attr(opal_con, "PROJECT"), ".", attr(opal_con, "TABLES")[1]),
    errors.print = TRUE
  )

  dsBaseClient::ds.ls(datasources = conns["study1"])

  ## open connection to OBiBa's Opal demo server
  opal_con <- opal_demo_con()

  expect_warning(
    basic_rocrate_5 <- basic_rocrate_3 |>
      dsROCrate::safe_output(
        connection = opal_con,
        logs_from = Sys.time() - 60, # capture the last min
        logs_to = Sys.time(),
        user = "dsuser"
      )
  )

  # run function with logs for an invalid period (no logs)
  expect_warning(
    basic_rocrate_6 <- basic_rocrate_3 |>
      dsROCrate::safe_output(
        connection = opal_con,
        logs_from = Sys.time() - 60^2 * 24, # 1 day ago
        logs_to = Sys.time() - 60^2 * 23,
        user = "dsuser"
      )
  )

  # provide invalid path to write the logs
  expect_warning(
    basic_rocrate_7 <- basic_rocrate_3 |>
      dsROCrate::safe_output(
        connection = opal_con,
        logs_from = Sys.time() - 60^2 * 24, # 1 day ago
        logs_to = Sys.time(),
        user = "dsuser",
        path = "/invalid/path/rocrateR"
      )
  )

  # write logs in temporary file
  tempdir_name <- tempdir()
  on.exit(unlink(tempdir_name, force = TRUE, recursive = TRUE))
  expect_true(dir.exists(tempdir_name))
  ## ignore warnings about missing logs
  suppressWarnings(
    basic_rocrate_8 <- basic_rocrate_3 |>
      dsROCrate::safe_output(
        connection = opal_con,
        logs_from = Sys.time() - 60, # capture the last min
        logs_to = Sys.time(),
        user = "dsuser",
        path = tempdir_name
      )
  )
  unlink(tempdir_name, force = TRUE, recursive = TRUE)
  expect_false(dir.exists(tempdir_name))

  # close connection to OBiBa's Opal demo server
  opalr::opal.logout(opal_con)
})

test_that("safe_output captures RESOLVE and ASSIGN operations independently", {
  local_mocked_bindings(
    validate_backend = function(x, ...) invisible(TRUE),
    validate_backend_version = function(x, ..., minimum = "5.7.2") {
      invisible(TRUE)
    },
    backend_logs = function(x, ...) {
      tibble::tibble(
        `@timestamp` = as.POSIXct(
          c(
            "2026-09-24 10:00:00",
            "2026-09-24 10:00:01",
            "2026-09-24 10:00:02"
          ),
          tz = "UTC"
        ),
        logger_name = "datashield.user",
        level = "INFO",
        message = c(
          "Session opened",
          "Resolved CNSIM.CNSIM1 to D",
          "Assigned D from CNSIM.CNSIM1"
        ),
        ds_action = c("OPEN", "RESOLVE", "ASSIGN"),
        ds_id = "session-1",
        ds_profile = "default",
        username = "test_user",
        ds_symbol = c(NA_character_, "D", "D"),
        ds_table = c(NA_character_, "CNSIM.CNSIM1", "CNSIM.CNSIM1"),
        ds_resource = rep(NA_character_, 3),
        ds_eval = rep(NA_character_, 3)
      )
    },
    .package = "dsROCrate"
  )

  out <- suppressWarnings(
    safe_output(
      fake_opal_con(),
      user = "test_user",
      rocrate = rocrateR::rocrate_5s(),
      logs_from = as.POSIXct("2026-09-24 09:59:00", tz = "UTC"),
      logs_to = as.POSIXct("2026-09-24 10:01:00", tz = "UTC")
    )
  )

  files <- .get_entity(out, type = "File")
  mapping <- purrr::keep(files, \(x) grepl("_mappings\\.csv$", x$`@id`))[[1]]
  mappings <- mapping$content[[1]]

  expect_equal(mappings$action, c("OPEN", "RESOLVE", "ASSIGN"))

  expect_equal(
    mappings |>
      dplyr::filter(action == "RESOLVE") |>
      dplyr::select(symbol, kind, asset),
    tibble::tibble(
      symbol = "D",
      kind = "table",
      asset = "CNSIM.CNSIM1"
    )
  )

  expect_equal(
    mappings |>
      dplyr::filter(action == "ASSIGN") |>
      dplyr::select(symbol, kind, asset),
    tibble::tibble(
      symbol = "D",
      kind = "table",
      asset = "CNSIM.CNSIM1"
    )
  )
})

test_that("safe_output retains RESOLVE operations without ASSIGN", {
  local_mocked_bindings(
    validate_backend = function(x, ...) invisible(TRUE),
    validate_backend_version = function(x, ..., minimum = "5.7.2") {
      invisible(TRUE)
    },
    backend_logs = function(x, ...) {
      tibble::tibble(
        `@timestamp` = as.POSIXct(
          c(
            "2026-09-24 10:00:00",
            "2026-09-24 10:00:01"
          ),
          tz = "UTC"
        ),
        logger_name = "datashield.user",
        level = "INFO",
        message = c(
          "Session opened",
          "Unable to resolve CNSIM.MISSING to D"
        ),
        ds_action = c("OPEN", "RESOLVE"),
        ds_id = "session-1",
        ds_profile = "default",
        username = "test_user",
        ds_symbol = c(NA_character_, "D"),
        ds_table = c(NA_character_, "CNSIM.MISSING"),
        ds_resource = rep(NA_character_, 2),
        ds_eval = rep(NA_character_, 2)
      )
    },
    .package = "dsROCrate"
  )

  out <- suppressWarnings(
    safe_output(
      fake_opal_con(),
      user = "test_user",
      rocrate = rocrateR::rocrate_5s(),
      logs_from = as.POSIXct("2026-09-24 09:59:00", tz = "UTC"),
      logs_to = as.POSIXct("2026-09-24 10:01:00", tz = "UTC")
    )
  )

  files <- .get_entity(out, type = "File")
  mapping <- purrr::keep(files, \(x) grepl("_mappings\\.csv$", x$`@id`))[[1]]
  mappings <- mapping$content[[1]]

  resolve <- mappings |>
    dplyr::filter(action == "RESOLVE")

  expect_equal(nrow(resolve), 1)
  expect_equal(resolve$symbol, "D")
  expect_equal(resolve$asset, "CNSIM.MISSING")
  expect_equal(resolve$kind, "table")
  expect_false("ASSIGN" %in% mappings$action)
})

Try the dsROCrate package in your browser

Any scripts or data that you put into this service are public.

dsROCrate documentation built on Oct. 16, 2026, 5:09 p.m.