From a59a7df914bc7e481a700a70bcebddd7efe7f166 Mon Sep 17 00:00:00 2001 From: roman-polak Date: Mon, 24 Nov 2025 08:05:57 +1100 Subject: [PATCH 01/14] RS-1154: Add document.token parameter to file functions. --- R/DataMart.R | 51 ++++++-- tests/testthat/test-datamart-secrets.R | 156 +++++++++++++++++++++++++ 2 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 tests/testthat/test-datamart-secrets.R diff --git a/R/DataMart.R b/R/DataMart.R index 6aed9f9..308fab4 100644 --- a/R/DataMart.R +++ b/R/DataMart.R @@ -15,13 +15,15 @@ MAX.FILENAME.LENGTH <- 100L #' @importFrom utils URLencode #' #' @export -QFileExists <- function(filename, show.warning = TRUE) +QFileExists <- function(filename, show.warning = TRUE, company.token = NA, document.token = NA) { - company.secret <- getCompanySecret() + company.secret <- if (missing(company.token)) getCompanySecret() else company.token + project.secret <- if (missing(document.token)) getProjectSecret() else document.token client.id <- getClientId() api.root <- getApiRoot("DataMartFileExists") res <- try(GET(paste0(api.root, "?filename=", URLencode(filename, TRUE)), config=add_headers("X-Q-Company-Secret" = company.secret, + "X-Q-Project-Secret" = project.secret, "X-Q-Project-ID" = client.id))) if (is.null(res$status_code) || res$status_code != 200) @@ -66,17 +68,19 @@ QFileExists <- function(filename, show.warning = TRUE) QFileOpen <- function(filename, open = "r", blocking = TRUE, encoding = getOption("encoding"), raw = FALSE, method = getOption("url.method", "default"), - mime.type = NA, company.token = NA) + mime.type = NA, company.token = NA, document.token = NA) { mode <- tolower(open) if (mode == "r" || mode == "rb") { company.secret <- if (missing(company.token)) getCompanySecret() else company.token + project.secret <- if (missing(document.token)) getProjectSecret() else document.token client.id <- getClientId() api.root <- getApiRoot() h <- new_handle() handle_setheaders(h, "X-Q-Company-Secret" = company.secret, + "X-Q-Project-Secret" = project.secret, "X-Q-Project-ID" = client.id ) uri <- paste0(api.root, "?filename=", URLencode(filename, TRUE)) @@ -149,11 +153,13 @@ close.qpostcon = function(con, ...) on.exit(if(file.exists(tmpfile)) file.remove(tmpfile)) company.secret <- getCompanySecret() + project.secret <- getProjectSecret() client.id <- getClientId() api.root <- getApiRoot() res <- try(POST(paste0(api.root, "?filename=", URLencode(filename, TRUE)), config = add_headers("Content-Type" = mimetype, "X-Q-Company-Secret" = company.secret, + "X-Q-Project-Secret" = project.secret, "X-Q-Project-ID" = client.id), encode = "raw", body = upload_file(tmpfile))) @@ -189,14 +195,16 @@ close.qpostcon = function(con, ...) #' @importFrom flipU StopForUserError #' #' @export -QLoadData <- function(filename, company.token = NA, ...) +QLoadData <- function(filename, company.token = NA, document.token = NA,...) { tmpfile <- tempfile() company.secret <- if (missing(company.token)) getCompanySecret() else company.token + project.secret <- if (missing(document.token)) getProjectSecret() else document.token client.id <- getClientId() api.root <- getApiRoot() res <- try(GET(paste0(api.root, "?filename=", URLencode(filename, TRUE)), config=add_headers("X-Q-Company-Secret" = company.secret, + "X-Q-Project-Secret" = project.secret, "X-Q-Project-ID" = client.id), write_disk(tmpfile, overwrite = TRUE))) @@ -317,11 +325,13 @@ QSaveData <- function(object, filename, compression.file.size.threshold = NULL, on.exit(if(file.exists(tmpfile)) file.remove(tmpfile)) company.secret <- getCompanySecret() + project.secret <- getProjectSecret() client.id <- getClientId() api.root <- getApiRoot() res <- try(POST(paste0(api.root, "?filename=", URLencode(filename, TRUE)), config = add_headers("Content-Type" = guess_type(filename), "X-Q-Company-Secret" = company.secret, + "X-Q-Project-Secret" = project.secret, "X-Q-Project-ID" = client.id), encode = "raw", body = upload_file(tmpfile))) @@ -368,7 +378,7 @@ QSaveData <- function(object, filename, compression.file.size.threshold = NULL, #' #' @param filenames collection of character strings. Names of the files to delete. #' To reference a file in a subdirectory, use double backslashes after each folder (e.g "subdir\\file.csv"). -#' @param company.token Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token. +#' @param company.token Use this if you need to delete files from a different company's Displayr Cloud Drive. You need to contact Support to get this token. #' #' @importFrom httr DELETE add_headers #' @importFrom utils URLencode @@ -377,14 +387,15 @@ QSaveData <- function(object, filename, compression.file.size.threshold = NULL, #' and assumed to succeed if no errors are thrown. #' #' @export -QDeleteFiles <- function(filenames, company.token = getCompanySecret()) +QDeleteFiles <- function(filenames, company.token = getCompanySecret(), document.token = getProjectSecret()) { company.secret <- company.token api.root <- getApiRoot("DataMartBatchDelete") url_param_filenames <- sprintf("filenames=%s", filenames) filenames.string <- paste(filenames, collapse = ", ") res <- try(DELETE(paste0(api.root, "?", URLencode(paste(url_param_filenames, collapse="&"))), - config=add_headers("X-Q-Company-Secret" = company.secret))) + config=add_headers("X-Q-Company-Secret" = company.secret, + "X-Q-Project-Secret" = document.token))) if (inherits(res, "try-error") || res$status_code != 200) { warning("Encountered an error deleting the following files: ", filenames.string) @@ -406,11 +417,13 @@ qSaveImage <- function(filename) on.exit(if(file.exists(tmpfile)) file.remove(tmpfile)) company.secret <- getCompanySecret() + project.secret <- getProjectSecret() client.id <- getClientId() api.root <- getApiRoot() res <- try(POST(paste0(api.root, "?filename=", URLencode(filename, TRUE)), config = add_headers("Content-Type" = guess_type(filename), "X-Q-Company-Secret" = company.secret, + "X-Q-Project-Secret" = project.secret, "X-Q-Project-ID" = client.id), encode = "raw", body = upload_file(tmpfile))) @@ -431,14 +444,16 @@ qSaveImage <- function(filename) invisible() } -qLoadImage <- function(filename, company.token = NA) +qLoadImage <- function(filename, company.token = NA, document.token = NA) { tmpfile <- tempfile() company.secret <- if (missing(company.token)) getCompanySecret() else company.token + project.secret <- if (missing(document.token)) getProjectSecret() else document.token client.id <- getClientId() api.root <- getApiRoot() res <- try(GET(paste0(api.root, "?filename=", URLencode(filename, TRUE)), config=add_headers("X-Q-Company-Secret" = company.secret, + "X-Q-Project-Secret" = project.secret, "X-Q-Project-ID" = client.id), write_disk(tmpfile, overwrite = TRUE))) @@ -484,6 +499,26 @@ getCompanySecret <- function() return (secret) } +#' Gets document secret from the environment. Throws an error if not found. +#' +#' @return Document secret token as a string. +#' +#' @noRd +getProjectSecret <- function() +{ + secret <- get0("projectSecret", ifnotfound = "") + # projectSecret might not have been copied into global projectSecret by a newer R Server, + # but it could have been stored by QServer in user secrets which are copied into userSecrets by older R servers. + if (secret == "") { + secret <- tryCatch({ + val <- userSecrets$projectSecret + if (is.character(val) && nzchar(val)) val else "" + }, + error = function(e) "") + } + return (secret) +} + #' Gets region from the environment and builds the api root. Throws an error if not found. #' #' @return Region-specific api root as a string. diff --git a/tests/testthat/test-datamart-secrets.R b/tests/testthat/test-datamart-secrets.R new file mode 100644 index 0000000..04c6f97 --- /dev/null +++ b/tests/testthat/test-datamart-secrets.R @@ -0,0 +1,156 @@ +library(testthat) +library(httr) + + +companySecret <- "test_company_secret" +assign("companySecret", companySecret, envir = .GlobalEnv) + +test_env = new.env() + +verifyHttpHeaders <- function(headers = list(), expect_header_to_be_equivalent_to_company_secret) +{ + companySecretHeader = headers["X-Q-Company-Secret"]; + companySecretHeader <- ifelse(is.null(companySecretHeader), "", companySecretHeader) + + if (expect_header_to_be_equivalent_to_company_secret && (companySecretHeader != companySecret)) { + test_env$headersVerificatoinResult <- list(asExpected = FALSE, + message = paste0("Expected 'companySecretHeader' ('",companySecretHeader,"') to equal 'companySecret' ('", companySecret,"')")) + } + + if (!expect_header_to_be_equivalent_to_company_secret && (companySecretHeader == companySecret)) { + test_env$headersVerificatoinResult <- list(asExpected = FALSE, + message = paste0("Expected 'companySecretHeader' ('",companySecretHeader,"') to not equal 'companySecret' ('", companySecret,"')")) + } + + test_env$headersVerificatoinResult <- list(asExpected = TRUE, message = "HTTP headers are as expected") + return(structure(list(status_code = 200, content = "mock"), class = "response")) +} + +params <- list( + list(company.token = NA, expect_header_to_be_equivalent_to_company_secret = TRUE, description = "company.token is not provided (NA)"), + list(company.token = "some_token", expect_header_to_be_equivalent_to_company_secret = FALSE, description = "company.token is some_token"), + list(company.token = companySecret, expect_header_to_be_equivalent_to_company_secret = TRUE, description = "company.token is the same as companySecret") +) + +for (p in params){ + companyTokenParameter = p$company.token + clientId <- "1" + + mockedHTTPRequest <- function(url = NULL, config = list(), ...) { + return(verifyHttpHeaders(config$headers, p$expect_header_to_be_equivalent_to_company_secret)) + } + + mockedCloseConnection <- function(con) { } + + mockedFileExists <- function(filename) { return(FALSE) } + + test_that(paste0("QFileExists correctly passes companySecret and company.token in HTTP header when ", p$description), + { + test_env$headersVerificatoinResult <- NULL + + with_mocked_bindings( + code = { + result <- ifelse(is.na(companyTokenParameter), + QFileExists("Test.dat", show.warning = FALSE), + QFileExists("Test.dat", show.warning = FALSE, company.token = companyTokenParameter)) + + expect_true(test_env$headersVerificatoinResult$asExpected, info = test_env$headersVerificatoinResult$message) + expect_true(result, info = "QFileExists should return TRUE with mocked GET.") + }, + GET = mockedHTTPRequest, + .package = "flipAPI" + ) + }) + + test_that(paste0("close.qpostcon correctly passes companySecret and company.token in HTTP header when ", p$description), + { + test_env$headersVerificatoinResult <- NULL + + orig_close_connection <- close.connection + orig_file_exists <- file.exists + on.exit({ + close.connection <- orig_close_connection + file.exists <- orig_file_exists + }, add = TRUE) + close.connection <- NULL + file.exists <- NULL + + with_mocked_bindings( + code = { + with_mocked_bindings( + code = { + con <- structure(list(url = "http://test/api/DataMart"), class = "connection") + result <- close.qpostcon(con) + + expect_true(test_env$headersVerificatoinResult$asExpected, info = test_env$headersVerificatoinResult$message) + }, + close.connection = mockedCloseConnection, + file.exists = mockedFileExists, + .package = "base" + ) + }, + POST = mockedHTTPRequest, + .package = "flipAPI" + ) + }) + + test_that(paste0("QDeleteFiles correctly passes companySecret and company.token in HTTP header when ", p$description), + { + test_env$headersVerificatoinResult <- NULL + + with_mocked_bindings( + code = { + result <- ifelse(is.na(companyTokenParameter), + QDeleteFiles(c("Test1.dat", "Test2.dat")), + QDeleteFiles(c("Test1.dat", "Test2.dat"), company.token = companyTokenParameter)) + + expect_true(test_env$headersVerificatoinResult$asExpected, info = test_env$headersVerificatoinResult$message) + }, + DELETE = mockedHTTPRequest, + .package = "flipAPI" + ) + }) +} + +test_that("getProjectSecret correctly extract project secret from environment", +{ + projectSecretValueName <- "projectSecret" + userSecretsValueName <- "userSecrets" + testProjectSecretValue <- "test_project_secret" + + clearProjectSecret <- function () { + if (exists(projectSecretValueName, envir = .GlobalEnv)) + rm(list = projectSecretValueName, envir = .GlobalEnv) + + if (exists(userSecretsValueName, envir = .GlobalEnv)) { + userSecrets <- get0(userSecretsValueName, envir = .GlobalEnv) + if (projectSecretValueName %in% names(userSecrets)) { + userSecrets[[projectSecretValueName]] <- NULL + assign(userSecretsValueName, userSecrets, envir = .GlobalEnv) + } + } + } + + # Case #1: projectSecret is set in environment in projectSecret + clearProjectSecret() + assign(projectSecretValueName, testProjectSecretValue, envir = .GlobalEnv) + expect_equal(getProjectSecret(), testProjectSecretValue) + + # Case #2: projectSecret is not set in environment in projectSecret, but is set in environment in userSecrets$projectSecret + clearProjectSecret() + userSecretsValue <- list(projectSecret = testProjectSecretValue) + assign(userSecretsValueName, userSecretsValue, envir = .GlobalEnv) + expect_equal(getProjectSecret(), testProjectSecretValue) + + # Case #3: projectSecret is set in both places, getProjectSecret() should prefer projectSecret + clearProjectSecret() + assign(projectSecretValueName, testProjectSecretValue, envir = .GlobalEnv) + userSecretsValue <- list(projectSecret = paste0(testProjectSecretValue, "_different")) + assign(userSecretsValueName, userSecretsValue, envir = .GlobalEnv) + expect_equal(getProjectSecret(), testProjectSecretValue) + + # Case #4: projectSecret is not set in either place + clearProjectSecret() + rm(list = userSecretsValueName, envir = .GlobalEnv) + expect_equal(getProjectSecret(), "") +}) From 8d9120389bdace3daa630ff87c1ccd38ebda9c6f Mon Sep 17 00:00:00 2001 From: roman-polak Date: Mon, 24 Nov 2025 08:07:02 +1100 Subject: [PATCH 02/14] RS-1154: Bump package version to 1.6.5. --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 8e08556..dee0697 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: flipAPI Type: Package Title: Web APIs tools -Version: 1.6.0 +Version: 1.6.5 Author: Displayr Maintainer: Displayr Description: Functions to extract data and interact with web APIs. From 0e335d9d3366b019c6210842ded465316bd935fd Mon Sep 17 00:00:00 2001 From: roman-polak Date: Tue, 25 Nov 2025 07:49:33 +1100 Subject: [PATCH 03/14] RS-1154: Add document.token parameter documentation. --- R/DataMart.R | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/R/DataMart.R b/R/DataMart.R index 308fab4..1c254ea 100644 --- a/R/DataMart.R +++ b/R/DataMart.R @@ -8,6 +8,8 @@ MAX.FILENAME.LENGTH <- 100L #' To reference a file in a subdirectory, use double backslashes after each folder (e.g "subdir\\file.csv"). #' @param show.warning logical scalar. Whether to show a warning when the file #' does not exist. +#' @param company.token Use this if you need to access a different company's Displayr Cloud Drive. You need to contact Support to get this token. +#' @param document.token Reserved #' #' @return TRUE if the file exists, otherwise FALSE. #' @@ -56,6 +58,7 @@ QFileExists <- function(filename, show.warning = TRUE, company.token = NA, docum #' @param method character string. See documentation for connections. #' @param mime.type character string. The mime-type of this file. If not provided, it will be interpreted from the file extension. #' @param company.token Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token. +#' @param document.token Reserved #' #' @return A curl connection (read) or a file connection (write) #' @@ -184,6 +187,7 @@ close.qpostcon = function(con, ...) #' @param filename character string. Name of the file to be opened from the Displayr Cloud Drive. #' To reference a file in a subdirectory, use double backslashes after each folder (e.g "subdir\\file.csv"). #' @param company.token Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token. +#' @param document.token Reserved #' @param ... Other parameters to pass to read.csv. #' #' @return An R object @@ -379,6 +383,7 @@ QSaveData <- function(object, filename, compression.file.size.threshold = NULL, #' @param filenames collection of character strings. Names of the files to delete. #' To reference a file in a subdirectory, use double backslashes after each folder (e.g "subdir\\file.csv"). #' @param company.token Use this if you need to delete files from a different company's Displayr Cloud Drive. You need to contact Support to get this token. +#' @param document.token Reserved #' #' @importFrom httr DELETE add_headers #' @importFrom utils URLencode From fb41face6e7176983b339a26c43b785f9fdb01e9 Mon Sep 17 00:00:00 2001 From: roman-polak Date: Tue, 25 Nov 2025 11:47:31 +1100 Subject: [PATCH 04/14] SS-1154: Update documentation. --- DESCRIPTION | 2 +- man/QDeleteFiles.Rd | 10 ++++++++-- man/QFileExists.Rd | 11 ++++++++++- man/QFileOpen.Rd | 5 ++++- man/QLoadData.Rd | 4 +++- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index dee0697..c09a2e3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -48,4 +48,4 @@ Suggests: httptest, officer, gifski -RoxygenNote: 7.3.2 +RoxygenNote: 7.3.3 diff --git a/man/QDeleteFiles.Rd b/man/QDeleteFiles.Rd index 5cb4a57..b4a1a05 100644 --- a/man/QDeleteFiles.Rd +++ b/man/QDeleteFiles.Rd @@ -4,13 +4,19 @@ \alias{QDeleteFiles} \title{Deletes a set of objects} \usage{ -QDeleteFiles(filenames, company.token = getCompanySecret()) +QDeleteFiles( + filenames, + company.token = getCompanySecret(), + document.token = getProjectSecret() +) } \arguments{ \item{filenames}{collection of character strings. Names of the files to delete. To reference a file in a subdirectory, use double backslashes after each folder (e.g "subdir\\file.csv").} -\item{company.token}{Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token.} +\item{company.token}{Use this if you need to delete files from a different company's Displayr Cloud Drive. You need to contact Support to get this token.} + +\item{document.token}{Reserved} } \value{ NULL invisibly. Called for the purpose of deleting data diff --git a/man/QFileExists.Rd b/man/QFileExists.Rd index f25fbca..431c1e3 100644 --- a/man/QFileExists.Rd +++ b/man/QFileExists.Rd @@ -4,7 +4,12 @@ \alias{QFileExists} \title{Check if a file exists} \usage{ -QFileExists(filename, show.warning = TRUE) +QFileExists( + filename, + show.warning = TRUE, + company.token = NA, + document.token = NA +) } \arguments{ \item{filename}{character string. Name of the file to search for. @@ -12,6 +17,10 @@ To reference a file in a subdirectory, use double backslashes after each folder \item{show.warning}{logical scalar. Whether to show a warning when the file does not exist.} + +\item{company.token}{Use this if you need to access a different company's Displayr Cloud Drive. You need to contact Support to get this token.} + +\item{document.token}{Reserved} } \value{ TRUE if the file exists, otherwise FALSE. diff --git a/man/QFileOpen.Rd b/man/QFileOpen.Rd index 2a7e204..c54a128 100644 --- a/man/QFileOpen.Rd +++ b/man/QFileOpen.Rd @@ -12,7 +12,8 @@ QFileOpen( raw = FALSE, method = getOption("url.method", "default"), mime.type = NA, - company.token = NA + company.token = NA, + document.token = NA ) } \arguments{ @@ -32,6 +33,8 @@ To reference a file in a subdirectory, use double backslashes after each folder \item{mime.type}{character string. The mime-type of this file. If not provided, it will be interpreted from the file extension.} \item{company.token}{Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token.} + +\item{document.token}{Reserved} } \value{ A curl connection (read) or a file connection (write) diff --git a/man/QLoadData.Rd b/man/QLoadData.Rd index de4a880..7bb4157 100644 --- a/man/QLoadData.Rd +++ b/man/QLoadData.Rd @@ -4,7 +4,7 @@ \alias{QLoadData} \title{Loads an object} \usage{ -QLoadData(filename, company.token = NA, ...) +QLoadData(filename, company.token = NA, document.token = NA, ...) } \arguments{ \item{filename}{character string. Name of the file to be opened from the Displayr Cloud Drive. @@ -12,6 +12,8 @@ To reference a file in a subdirectory, use double backslashes after each folder \item{company.token}{Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token.} +\item{document.token}{Reserved} + \item{...}{Other parameters to pass to read.csv.} } \value{ From cdb029323d7b2ce2f8893d2263f55ab939622da3 Mon Sep 17 00:00:00 2001 From: roman-polak Date: Tue, 25 Nov 2025 23:54:16 +1100 Subject: [PATCH 05/14] SS-1154: Update global variable reference. --- R/DataMart.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/DataMart.R b/R/DataMart.R index 1c254ea..5299feb 100644 --- a/R/DataMart.R +++ b/R/DataMart.R @@ -516,7 +516,7 @@ getProjectSecret <- function() # but it could have been stored by QServer in user secrets which are copied into userSecrets by older R servers. if (secret == "") { secret <- tryCatch({ - val <- userSecrets$projectSecret + val <- .GlobalEnv$userSecrets$projectSecret if (is.character(val) && nzchar(val)) val else "" }, error = function(e) "") From bd5ba14f6c56f41e575e91800118bda09631eac4 Mon Sep 17 00:00:00 2001 From: roman-polak Date: Wed, 26 Nov 2025 09:26:41 +1100 Subject: [PATCH 06/14] SS-1154: Remove import of unused package. Fix documentation reference to an imported function. --- DESCRIPTION | 1 - R/DataMart.R | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index c09a2e3..e4b5078 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -23,7 +23,6 @@ Imports: stringr, readxl, utils, - flipFormat, flipTime, flipTransformations, flipU (>= 1.6.1), diff --git a/R/DataMart.R b/R/DataMart.R index 5299feb..3ffe284 100644 --- a/R/DataMart.R +++ b/R/DataMart.R @@ -265,7 +265,7 @@ QLoadData <- function(filename, company.token = NA, document.token = NA,...) #' (in bytes) larger than this value will be compressed into a zip file. #' Defaults to NULL, in which case no compression occurs. #' @param ... Other parameters to pass to \code{\link{write.csv}}, \code{\link{saveRDS}}, -#' \code{\link{write.xlsx}}, or \code{\link{write_sav}}. +#' \code{\link[openxlsx]{write.xlsx}}, or \code{\link[haven]{write_sav}}. #' #' @importFrom haven write_sav #' @importFrom httr POST add_headers upload_file From 1f29f4e6d4132822333dd3decb16ae81865a3f94 Mon Sep 17 00:00:00 2001 From: roman-polak Date: Wed, 26 Nov 2025 10:03:29 +1100 Subject: [PATCH 07/14] SS-1154: Update *.rd. --- man/QSaveData.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/QSaveData.Rd b/man/QSaveData.Rd index 026dd36..92ab3be 100644 --- a/man/QSaveData.Rd +++ b/man/QSaveData.Rd @@ -17,7 +17,7 @@ To reference a file in a subdirectory, use double backslashes after each folder Defaults to NULL, in which case no compression occurs.} \item{...}{Other parameters to pass to \code{\link{write.csv}}, \code{\link{saveRDS}}, -\code{\link{write.xlsx}}, or \code{\link{write_sav}}.} +\code{\link[openxlsx]{write.xlsx}}, or \code{\link[haven]{write_sav}}.} } \value{ NULL invisibly. Called for the purpose of uploading data From b8853339f2256f67283b431f3f1ac0025b44f011 Mon Sep 17 00:00:00 2001 From: roman-polak Date: Wed, 26 Nov 2025 11:35:39 +1100 Subject: [PATCH 08/14] SS-1154: Restore global variable modified by tests. --- tests/testthat/test-datamart-secrets.R | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-datamart-secrets.R b/tests/testthat/test-datamart-secrets.R index 04c6f97..cefdf8e 100644 --- a/tests/testthat/test-datamart-secrets.R +++ b/tests/testthat/test-datamart-secrets.R @@ -1,9 +1,23 @@ library(testthat) library(httr) +companySecretValueName <- "companySecret" -companySecret <- "test_company_secret" -assign("companySecret", companySecret, envir = .GlobalEnv) +setup({ + old_companySecret <- get0(companySecretValueName, envir = .GlobalEnv, ifnotfound = NULL) + companySecret <- "test_company_secret" + assign(companySecretValueName, companySecret, envir = .GlobalEnv) +}) + +teardown({ + if (exists("old_companySecret", envir = parent.frame())) { + assign(companySecretValueName, old_companySecret, envir = .GlobalEnv) + } else { + if (exists(companySecretValueName, envir = .GlobalEnv)) { + rm(list = companySecretValueName, envir = .GlobalEnv) + } + } +}) test_env = new.env() From 0d5c15eb68c0ed5f00cef12fd67bc0e4a059a70b Mon Sep 17 00:00:00 2001 From: roman-polak Date: Wed, 26 Nov 2025 13:32:20 +1100 Subject: [PATCH 09/14] SS-1154: Replace deprecated setup/teardown with localGlobal. --- tests/testthat/helper-datamart.R | 10 ++++++++++ tests/testthat/test-datamart-secrets.R | 20 ++------------------ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/tests/testthat/helper-datamart.R b/tests/testthat/helper-datamart.R index 12e8844..485f198 100644 --- a/tests/testthat/helper-datamart.R +++ b/tests/testthat/helper-datamart.R @@ -4,3 +4,13 @@ clientId <- "-1027046" # This could be anything - we are just using this for met assign("clientId", clientId, envir = .GlobalEnv) region <- "app" assign("region", region, envir = .GlobalEnv) + +localGlobal <- function(name, value, envir = parent.frame()) { + if (exists(name, envir = .GlobalEnv)) { + old.value <- get(name, envir = .GlobalEnv) + withr::defer(assign(name, old.value, envir = .GlobalEnv), envir = envir) + } else { + withr::defer(rm(list = name, envir = .GlobalEnv), envir = envir) + } + assign(name, value, envir = .GlobalEnv) +} \ No newline at end of file diff --git a/tests/testthat/test-datamart-secrets.R b/tests/testthat/test-datamart-secrets.R index cefdf8e..856e406 100644 --- a/tests/testthat/test-datamart-secrets.R +++ b/tests/testthat/test-datamart-secrets.R @@ -1,25 +1,9 @@ library(testthat) library(httr) -companySecretValueName <- "companySecret" +localGlobal("companySecret", "test_company_secret") -setup({ - old_companySecret <- get0(companySecretValueName, envir = .GlobalEnv, ifnotfound = NULL) - companySecret <- "test_company_secret" - assign(companySecretValueName, companySecret, envir = .GlobalEnv) -}) - -teardown({ - if (exists("old_companySecret", envir = parent.frame())) { - assign(companySecretValueName, old_companySecret, envir = .GlobalEnv) - } else { - if (exists(companySecretValueName, envir = .GlobalEnv)) { - rm(list = companySecretValueName, envir = .GlobalEnv) - } - } -}) - -test_env = new.env() +test_env = new.env() # holds HTTP header verification result that is put there by mocked HTTP function and is used by testthat tests verifyHttpHeaders <- function(headers = list(), expect_header_to_be_equivalent_to_company_secret) { From cebe32734d60f8489d58bf5ed60d43013a76a124 Mon Sep 17 00:00:00 2001 From: roman-polak Date: Fri, 28 Nov 2025 09:59:43 +1100 Subject: [PATCH 10/14] SS-1154: Address most review comemnts. Fix implementatin and tests. --- R/DataMart.R | 33 +++--- tests/testthat/helper-datamart.R | 2 + tests/testthat/test-datamart-secrets.R | 143 +++++++++++++++++-------- 3 files changed, 118 insertions(+), 60 deletions(-) diff --git a/R/DataMart.R b/R/DataMart.R index 3ffe284..b93def9 100644 --- a/R/DataMart.R +++ b/R/DataMart.R @@ -58,7 +58,7 @@ QFileExists <- function(filename, show.warning = TRUE, company.token = NA, docum #' @param method character string. See documentation for connections. #' @param mime.type character string. The mime-type of this file. If not provided, it will be interpreted from the file extension. #' @param company.token Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token. -#' @param document.token Reserved +#' @inheritParams QFileExists params = c("document.token") #' #' @return A curl connection (read) or a file connection (write) #' @@ -74,10 +74,11 @@ QFileOpen <- function(filename, open = "r", blocking = TRUE, mime.type = NA, company.token = NA, document.token = NA) { mode <- tolower(open) + company.secret <- if (missing(company.token)) getCompanySecret() else company.token + project.secret <- if (missing(document.token)) getProjectSecret() else document.token + if (mode == "r" || mode == "rb") { - company.secret <- if (missing(company.token)) getCompanySecret() else company.token - project.secret <- if (missing(document.token)) getProjectSecret() else document.token client.id <- getClientId() api.root <- getApiRoot() h <- new_handle() @@ -123,6 +124,8 @@ QFileOpen <- function(filename, open = "r", blocking = TRUE, attr(con, "filename") <- filename if (missing(mime.type)) mime.type <- guess_type(filename) attr(con, "mimetype") <- mime.type + attr(con, "company.secret") <- company.secret + attr(con, "project.secret") <- project.secret return (con) } @@ -153,10 +156,10 @@ close.qpostcon = function(con, ...) filename <- attr(con, "filename") tmpfile <- attr(con, "tmpfile") mimetype <- attr(con, "mimetype") + company.secret <- attr(con, "company.secret") + project.secret <- attr(con, "project.secret") on.exit(if(file.exists(tmpfile)) file.remove(tmpfile)) - company.secret <- getCompanySecret() - project.secret <- getProjectSecret() client.id <- getClientId() api.root <- getApiRoot() res <- try(POST(paste0(api.root, "?filename=", URLencode(filename, TRUE)), @@ -187,7 +190,7 @@ close.qpostcon = function(con, ...) #' @param filename character string. Name of the file to be opened from the Displayr Cloud Drive. #' To reference a file in a subdirectory, use double backslashes after each folder (e.g "subdir\\file.csv"). #' @param company.token Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token. -#' @param document.token Reserved +#' @inheritParams QFileExists params = c("document.token") #' @param ... Other parameters to pass to read.csv. #' #' @return An R object @@ -383,7 +386,7 @@ QSaveData <- function(object, filename, compression.file.size.threshold = NULL, #' @param filenames collection of character strings. Names of the files to delete. #' To reference a file in a subdirectory, use double backslashes after each folder (e.g "subdir\\file.csv"). #' @param company.token Use this if you need to delete files from a different company's Displayr Cloud Drive. You need to contact Support to get this token. -#' @param document.token Reserved +#' @inheritParams QFileExists params = c("document.token") #' #' @importFrom httr DELETE add_headers #' @importFrom utils URLencode @@ -511,17 +514,11 @@ getCompanySecret <- function() #' @noRd getProjectSecret <- function() { - secret <- get0("projectSecret", ifnotfound = "") - # projectSecret might not have been copied into global projectSecret by a newer R Server, - # but it could have been stored by QServer in user secrets which are copied into userSecrets by older R servers. - if (secret == "") { - secret <- tryCatch({ - val <- .GlobalEnv$userSecrets$projectSecret - if (is.character(val) && nzchar(val)) val else "" - }, - error = function(e) "") - } - return (secret) + get0("projectSecret", ifnotfound = NULL) %||% + # projectSecret might not have been copied into global projectSecret by an older R Server, + # but it could have been stored by QServer in user secrets which are copied into userSecrets by older R servers. + get0("userSecrets", mode = "list", ifnotfound = NULL)[["projectSecret"]] %||% + "" } #' Gets region from the environment and builds the api root. Throws an error if not found. diff --git a/tests/testthat/helper-datamart.R b/tests/testthat/helper-datamart.R index 485f198..1b84c0f 100644 --- a/tests/testthat/helper-datamart.R +++ b/tests/testthat/helper-datamart.R @@ -1,5 +1,7 @@ companySecret <- get0("companySecret", ifnotfound = Sys.getenv("companySecret")) assign("companySecret", companySecret, envir = .GlobalEnv) +projectSecret <- "" +assign("projectSecret", projectSecret, envir = .GlobalEnv) clientId <- "-1027046" # This could be anything - we are just using this for metadata assign("clientId", clientId, envir = .GlobalEnv) region <- "app" diff --git a/tests/testthat/test-datamart-secrets.R b/tests/testthat/test-datamart-secrets.R index 856e406..2f55b64 100644 --- a/tests/testthat/test-datamart-secrets.R +++ b/tests/testthat/test-datamart-secrets.R @@ -1,68 +1,119 @@ -library(testthat) -library(httr) - localGlobal("companySecret", "test_company_secret") +localGlobal("projectSecret", "test_project_secret") -test_env = new.env() # holds HTTP header verification result that is put there by mocked HTTP function and is used by testthat tests +test_env = new.env() # holds HTTP header verification result that is put there by mocked HTTP function and is used by the tests below -verifyHttpHeaders <- function(headers = list(), expect_header_to_be_equivalent_to_company_secret) +verifyHttpHeaders <- function(headers = list(), expect_company_secret_header_to_be_equivalent_to_company_secret, expect_project_secret_header_to_be_equivalent_to_project_secret) { companySecretHeader = headers["X-Q-Company-Secret"]; companySecretHeader <- ifelse(is.null(companySecretHeader), "", companySecretHeader) + projectSecretHeader = headers["X-Q-Project-Secret"]; + projectSecretHeader <- ifelse(is.null(projectSecretHeader), "", projectSecretHeader) + httpResponse <- structure(list(status_code = 200, content = "mock"), class = "response") + companySecret <- getCompanySecret() + projectSecret <- getProjectSecret() + + test_env$company_secret_header <- companySecretHeader + test_env$company_secret <- companySecret + test_env$project_secret_header <- projectSecretHeader + test_env$project_secret <- projectSecret + + companySecretHeaderIsValid <- TRUE + projectSecretHeaderIsValid <- TRUE + errors <- character() - if (expect_header_to_be_equivalent_to_company_secret && (companySecretHeader != companySecret)) { - test_env$headersVerificatoinResult <- list(asExpected = FALSE, - message = paste0("Expected 'companySecretHeader' ('",companySecretHeader,"') to equal 'companySecret' ('", companySecret,"')")) + if (expect_company_secret_header_to_be_equivalent_to_company_secret && !identical(companySecretHeader, companySecret)) { + companySecretHeaderIsValid <- FALSE + errors <- append(errors, paste0("Expected 'companySecretHeader' ('", companySecretHeader, "') to equal 'companySecret' ('", companySecret, "')")) } - if (!expect_header_to_be_equivalent_to_company_secret && (companySecretHeader == companySecret)) { - test_env$headersVerificatoinResult <- list(asExpected = FALSE, - message = paste0("Expected 'companySecretHeader' ('",companySecretHeader,"') to not equal 'companySecret' ('", companySecret,"')")) + if (!expect_company_secret_header_to_be_equivalent_to_company_secret && identical(companySecretHeader, companySecret)) { + companySecretHeaderIsValid <- FALSE + errors <- append(errors, paste0("Expected 'companySecretHeader' ('", companySecretHeader, "') to not equal 'companySecret' ('", companySecret, "')")) } - test_env$headersVerificatoinResult <- list(asExpected = TRUE, message = "HTTP headers are as expected") - return(structure(list(status_code = 200, content = "mock"), class = "response")) + if (expect_project_secret_header_to_be_equivalent_to_project_secret && !identical(projectSecretHeader, projectSecret)) { + projectSecretHeaderIsValid <- FALSE + errors <- append(errors, paste0("Expected 'projectSecretHeader' ('", projectSecretHeader, "') to equal 'projectSecret' ('", projectSecret, "')")) + } + + if (!expect_project_secret_header_to_be_equivalent_to_project_secret && identical(projectSecretHeader, projectSecret)) { + projectSecretHeaderIsValid <- FALSE + errors <- append(errors, paste0("Expected 'projectSecretHeader' ('", projectSecretHeader, "') to not equal 'projectSecret' ('", projectSecret, "')")) + } + + test_env$headersVerificationResult <- list(asExpected = companySecretHeaderIsValid && projectSecretHeaderIsValid, messages = errors) + return(httpResponse) +} + +expect_successful_headers_verification <- function() { + if (!test_env$headersVerificationResult$asExpected) { + fail(test_env$headersVerificationResult$messages) + } else { + pass() + } } params <- list( - list(company.token = NA, expect_header_to_be_equivalent_to_company_secret = TRUE, description = "company.token is not provided (NA)"), - list(company.token = "some_token", expect_header_to_be_equivalent_to_company_secret = FALSE, description = "company.token is some_token"), - list(company.token = companySecret, expect_header_to_be_equivalent_to_company_secret = TRUE, description = "company.token is the same as companySecret") + list(company.token = NA, document.token = NA, + expect_company_secret_header_to_be_equivalent_to_company_secret = TRUE, + expect_project_secret_header_to_be_equivalent_to_project_secret = TRUE, + description = "neither company.token no document.token are provided (NA)"), + + list(company.token = "some_token", document.token = "some_project_token", + expect_company_secret_header_to_be_equivalent_to_company_secret = FALSE, + expect_project_secret_header_to_be_equivalent_to_project_secret = FALSE, + description = "company.token is 'some_token'; document.token is 'some_project_token'"), + + list(company.token = "test_company_secret", document.token = "test_project_secret", + expect_company_secret_header_to_be_equivalent_to_company_secret = TRUE, + expect_project_secret_header_to_be_equivalent_to_project_secret = TRUE, + description = "company.token is the same as companySecret; document.token is the same as projectSecret") ) for (p in params){ - companyTokenParameter = p$company.token + companyTokenParameter = ifelse(is.na(p$company.token), getCompanySecret(), p$company.token) # p$company.token == NA simulates missing company.token parameter + documentTokenParameter = ifelse(is.na(p$document.token), getProjectSecret(), p$document.token) # p$document.token == NA simulates missing document.token parameter clientId <- "1" mockedHTTPRequest <- function(url = NULL, config = list(), ...) { - return(verifyHttpHeaders(config$headers, p$expect_header_to_be_equivalent_to_company_secret)) + verifyHttpHeaders(config$headers, p$expect_company_secret_header_to_be_equivalent_to_company_secret, p$expect_project_secret_header_to_be_equivalent_to_project_secret) } mockedCloseConnection <- function(con) { } mockedFileExists <- function(filename) { return(FALSE) } + + mockedCurl <- function(url, open, handle) { + structure(list(url = "http://test/api/DataMart"), class = "connection") + } test_that(paste0("QFileExists correctly passes companySecret and company.token in HTTP header when ", p$description), { - test_env$headersVerificatoinResult <- NULL + test_env$headersVerificationResult <- NULL with_mocked_bindings( code = { - result <- ifelse(is.na(companyTokenParameter), - QFileExists("Test.dat", show.warning = FALSE), - QFileExists("Test.dat", show.warning = FALSE, company.token = companyTokenParameter)) - - expect_true(test_env$headersVerificatoinResult$asExpected, info = test_env$headersVerificatoinResult$message) + args <- list(filename = "Test.dat", show.warning = FALSE) + if (!is.na(companyTokenParameter)) { + args$company.token <- companyTokenParameter + } + if (!is.na(documentTokenParameter)) { + args$document.token <- documentTokenParameter + } + result <- do.call(QFileExists, args) + + expect_successful_headers_verification() expect_true(result, info = "QFileExists should return TRUE with mocked GET.") }, GET = mockedHTTPRequest, - .package = "flipAPI" + ##.package = "flipAPI" ) }) test_that(paste0("close.qpostcon correctly passes companySecret and company.token in HTTP header when ", p$description), { - test_env$headersVerificatoinResult <- NULL + test_env$headersVerificationResult <- NULL orig_close_connection <- close.connection orig_file_exists <- file.exists @@ -70,7 +121,7 @@ for (p in params){ close.connection <- orig_close_connection file.exists <- orig_file_exists }, add = TRUE) - close.connection <- NULL + close.connection <- NULL # this is needed to ensure a binding for the function exists in this package's namespace before mocking file.exists <- NULL with_mocked_bindings( @@ -78,31 +129,36 @@ for (p in params){ with_mocked_bindings( code = { con <- structure(list(url = "http://test/api/DataMart"), class = "connection") - result <- close.qpostcon(con) + # close.qpostcon does not directly accept token parameters, but uses values that were stored with the connection by QFileOpen + attr(con, "company.secret") <- companyTokenParameter + attr(con, "project.secret") <- documentTokenParameter - expect_true(test_env$headersVerificatoinResult$asExpected, info = test_env$headersVerificatoinResult$message) + close.qpostcon(con) + + expect_successful_headers_verification() }, close.connection = mockedCloseConnection, file.exists = mockedFileExists, .package = "base" ) }, - POST = mockedHTTPRequest, - .package = "flipAPI" + POST = mockedHTTPRequest ) }) test_that(paste0("QDeleteFiles correctly passes companySecret and company.token in HTTP header when ", p$description), - { - test_env$headersVerificatoinResult <- NULL + { + test_env$headersVerificationResult <- NULL with_mocked_bindings( code = { - result <- ifelse(is.na(companyTokenParameter), - QDeleteFiles(c("Test1.dat", "Test2.dat")), - QDeleteFiles(c("Test1.dat", "Test2.dat"), company.token = companyTokenParameter)) + args <- list(c("Test1.dat", "Test2.dat")) + if (!is.na(companyTokenParameter)) args$company.token <- companyTokenParameter + if (!is.na(documentTokenParameter)) args$document.token <- documentTokenParameter + + do.call(QDeleteFiles, args) - expect_true(test_env$headersVerificatoinResult$asExpected, info = test_env$headersVerificatoinResult$message) + expect_successful_headers_verification() }, DELETE = mockedHTTPRequest, .package = "flipAPI" @@ -110,15 +166,18 @@ for (p in params){ }) } -test_that("getProjectSecret correctly extract project secret from environment", -{ +test_that("getProjectSecret correctly extracts project secret from environment", +{ projectSecretValueName <- "projectSecret" userSecretsValueName <- "userSecrets" testProjectSecretValue <- "test_project_secret" + projectSecretValueInUserSecrets <- paste0(testProjectSecretValue, "_from_user_secrets") clearProjectSecret <- function () { - if (exists(projectSecretValueName, envir = .GlobalEnv)) - rm(list = projectSecretValueName, envir = .GlobalEnv) + envs <- find(projectSecretValueName) + for (env in envs) { + remove(list = projectSecretValueName, envir = as.environment(env)) + } if (exists(userSecretsValueName, envir = .GlobalEnv)) { userSecrets <- get0(userSecretsValueName, envir = .GlobalEnv) @@ -128,7 +187,7 @@ test_that("getProjectSecret correctly extract project secret from environment", } } } - + # Case #1: projectSecret is set in environment in projectSecret clearProjectSecret() assign(projectSecretValueName, testProjectSecretValue, envir = .GlobalEnv) From 6c1b7148203e52d0c57bbe46c78992e4f455eb27 Mon Sep 17 00:00:00 2001 From: roman-polak Date: Fri, 28 Nov 2025 16:02:17 +1100 Subject: [PATCH 11/14] SS-1154: Simplify documentation and tests for getProjectSecret(). --- R/DataMart.R | 8 ++-- man/QDeleteFiles.Rd | 2 +- man/QFileExists.Rd | 2 +- man/QFileOpen.Rd | 2 +- man/QLoadData.Rd | 2 +- tests/testthat/test-datamart-secrets.R | 52 +++++++++++--------------- 6 files changed, 29 insertions(+), 39 deletions(-) diff --git a/R/DataMart.R b/R/DataMart.R index b93def9..82e5242 100644 --- a/R/DataMart.R +++ b/R/DataMart.R @@ -9,7 +9,7 @@ MAX.FILENAME.LENGTH <- 100L #' @param show.warning logical scalar. Whether to show a warning when the file #' does not exist. #' @param company.token Use this if you need to access a different company's Displayr Cloud Drive. You need to contact Support to get this token. -#' @param document.token Reserved +#' @param document.token Reserved. #' #' @return TRUE if the file exists, otherwise FALSE. #' @@ -58,7 +58,7 @@ QFileExists <- function(filename, show.warning = TRUE, company.token = NA, docum #' @param method character string. See documentation for connections. #' @param mime.type character string. The mime-type of this file. If not provided, it will be interpreted from the file extension. #' @param company.token Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token. -#' @inheritParams QFileExists params = c("document.token") +#' @inheritParams QFileExists #' #' @return A curl connection (read) or a file connection (write) #' @@ -190,7 +190,7 @@ close.qpostcon = function(con, ...) #' @param filename character string. Name of the file to be opened from the Displayr Cloud Drive. #' To reference a file in a subdirectory, use double backslashes after each folder (e.g "subdir\\file.csv"). #' @param company.token Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token. -#' @inheritParams QFileExists params = c("document.token") +#' @inheritParams QFileExists #' @param ... Other parameters to pass to read.csv. #' #' @return An R object @@ -386,7 +386,7 @@ QSaveData <- function(object, filename, compression.file.size.threshold = NULL, #' @param filenames collection of character strings. Names of the files to delete. #' To reference a file in a subdirectory, use double backslashes after each folder (e.g "subdir\\file.csv"). #' @param company.token Use this if you need to delete files from a different company's Displayr Cloud Drive. You need to contact Support to get this token. -#' @inheritParams QFileExists params = c("document.token") +#' @inheritParams QFileExists #' #' @importFrom httr DELETE add_headers #' @importFrom utils URLencode diff --git a/man/QDeleteFiles.Rd b/man/QDeleteFiles.Rd index b4a1a05..2e0f725 100644 --- a/man/QDeleteFiles.Rd +++ b/man/QDeleteFiles.Rd @@ -16,7 +16,7 @@ To reference a file in a subdirectory, use double backslashes after each folder \item{company.token}{Use this if you need to delete files from a different company's Displayr Cloud Drive. You need to contact Support to get this token.} -\item{document.token}{Reserved} +\item{document.token}{Reserved.} } \value{ NULL invisibly. Called for the purpose of deleting data diff --git a/man/QFileExists.Rd b/man/QFileExists.Rd index 431c1e3..a87aab2 100644 --- a/man/QFileExists.Rd +++ b/man/QFileExists.Rd @@ -20,7 +20,7 @@ does not exist.} \item{company.token}{Use this if you need to access a different company's Displayr Cloud Drive. You need to contact Support to get this token.} -\item{document.token}{Reserved} +\item{document.token}{Reserved.} } \value{ TRUE if the file exists, otherwise FALSE. diff --git a/man/QFileOpen.Rd b/man/QFileOpen.Rd index c54a128..9f5c49e 100644 --- a/man/QFileOpen.Rd +++ b/man/QFileOpen.Rd @@ -34,7 +34,7 @@ To reference a file in a subdirectory, use double backslashes after each folder \item{company.token}{Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token.} -\item{document.token}{Reserved} +\item{document.token}{Reserved.} } \value{ A curl connection (read) or a file connection (write) diff --git a/man/QLoadData.Rd b/man/QLoadData.Rd index 7bb4157..2803509 100644 --- a/man/QLoadData.Rd +++ b/man/QLoadData.Rd @@ -12,7 +12,7 @@ To reference a file in a subdirectory, use double backslashes after each folder \item{company.token}{Use this if you need to read from a different company's Displayr Cloud Drive. You need to contact Support to get this token.} -\item{document.token}{Reserved} +\item{document.token}{Reserved.} \item{...}{Other parameters to pass to read.csv.} } diff --git a/tests/testthat/test-datamart-secrets.R b/tests/testthat/test-datamart-secrets.R index 2f55b64..930b37f 100644 --- a/tests/testthat/test-datamart-secrets.R +++ b/tests/testthat/test-datamart-secrets.R @@ -172,42 +172,32 @@ test_that("getProjectSecret correctly extracts project secret from environment", userSecretsValueName <- "userSecrets" testProjectSecretValue <- "test_project_secret" projectSecretValueInUserSecrets <- paste0(testProjectSecretValue, "_from_user_secrets") - - clearProjectSecret <- function () { - envs <- find(projectSecretValueName) - for (env in envs) { - remove(list = projectSecretValueName, envir = as.environment(env)) - } - - if (exists(userSecretsValueName, envir = .GlobalEnv)) { - userSecrets <- get0(userSecretsValueName, envir = .GlobalEnv) - if (projectSecretValueName %in% names(userSecrets)) { - userSecrets[[projectSecretValueName]] <- NULL - assign(userSecretsValueName, userSecrets, envir = .GlobalEnv) - } - } - } # Case #1: projectSecret is set in environment in projectSecret - clearProjectSecret() - assign(projectSecretValueName, testProjectSecretValue, envir = .GlobalEnv) - expect_equal(getProjectSecret(), testProjectSecretValue) + { + localGlobal(projectSecretValueName, testProjectSecretValue) + expect_equal(getProjectSecret(), testProjectSecretValue) + } # Case #2: projectSecret is not set in environment in projectSecret, but is set in environment in userSecrets$projectSecret - clearProjectSecret() - userSecretsValue <- list(projectSecret = testProjectSecretValue) - assign(userSecretsValueName, userSecretsValue, envir = .GlobalEnv) - expect_equal(getProjectSecret(), testProjectSecretValue) - + { + userSecretsValue <- list(projectSecret = testProjectSecretValue) + localGlobal(userSecretsValueName, userSecretsValue) + expect_equal(getProjectSecret(), testProjectSecretValue) + } + # Case #3: projectSecret is set in both places, getProjectSecret() should prefer projectSecret - clearProjectSecret() - assign(projectSecretValueName, testProjectSecretValue, envir = .GlobalEnv) - userSecretsValue <- list(projectSecret = paste0(testProjectSecretValue, "_different")) - assign(userSecretsValueName, userSecretsValue, envir = .GlobalEnv) - expect_equal(getProjectSecret(), testProjectSecretValue) + { + localGlobal(projectSecretValueName, testProjectSecretValue) + userSecretsValue <- list(projectSecret = testProjectSecretValue) + localGlobal(userSecretsValueName, userSecretsValue) + expect_equal(getProjectSecret(), testProjectSecretValue) + } # Case #4: projectSecret is not set in either place - clearProjectSecret() - rm(list = userSecretsValueName, envir = .GlobalEnv) - expect_equal(getProjectSecret(), "") + { + rm(list = projectSecretValueName, envir = .GlobalEnv) + rm(list = userSecretsValueName, envir = .GlobalEnv) + expect_equal(getProjectSecret(), "", info = "Case #4") + } }) From 7b8ebfd8daff774f5dba6d6c4451b709ea93b161 Mon Sep 17 00:00:00 2001 From: roman-polak Date: Fri, 28 Nov 2025 16:14:33 +1100 Subject: [PATCH 12/14] SS-1154: Fix tests in CI. --- tests/testthat/test-datamart-secrets.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-datamart-secrets.R b/tests/testthat/test-datamart-secrets.R index 930b37f..8745bc7 100644 --- a/tests/testthat/test-datamart-secrets.R +++ b/tests/testthat/test-datamart-secrets.R @@ -50,7 +50,7 @@ expect_successful_headers_verification <- function() { if (!test_env$headersVerificationResult$asExpected) { fail(test_env$headersVerificationResult$messages) } else { - pass() + testthat::pass() } } From 9e6578700930054e8e35baaa1f8af29c2984b44d Mon Sep 17 00:00:00 2001 From: roman-polak Date: Fri, 28 Nov 2025 16:30:28 +1100 Subject: [PATCH 13/14] SS-1154: Use expect_true(TRUE) instead of pass(). --- tests/testthat/test-datamart-secrets.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-datamart-secrets.R b/tests/testthat/test-datamart-secrets.R index 8745bc7..5b460dc 100644 --- a/tests/testthat/test-datamart-secrets.R +++ b/tests/testthat/test-datamart-secrets.R @@ -50,7 +50,7 @@ expect_successful_headers_verification <- function() { if (!test_env$headersVerificationResult$asExpected) { fail(test_env$headersVerificationResult$messages) } else { - testthat::pass() + expect_true(TRUE) } } From 009415294306d83890eb9895265daaaf00244e0d Mon Sep 17 00:00:00 2001 From: roman-polak Date: Mon, 1 Dec 2025 09:36:03 +1100 Subject: [PATCH 14/14] SS-1154: Address latest review comments. --- R/DataMart.R | 2 +- tests/testthat/test-datamart-secrets.R | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/R/DataMart.R b/R/DataMart.R index 82e5242..6cb7bf0 100644 --- a/R/DataMart.R +++ b/R/DataMart.R @@ -507,7 +507,7 @@ getCompanySecret <- function() return (secret) } -#' Gets document secret from the environment. Throws an error if not found. +#' Gets document secret from the environment or an empty string if not found. #' #' @return Document secret token as a string. #' diff --git a/tests/testthat/test-datamart-secrets.R b/tests/testthat/test-datamart-secrets.R index 5b460dc..787d99e 100644 --- a/tests/testthat/test-datamart-secrets.R +++ b/tests/testthat/test-datamart-secrets.R @@ -3,12 +3,10 @@ localGlobal("projectSecret", "test_project_secret") test_env = new.env() # holds HTTP header verification result that is put there by mocked HTTP function and is used by the tests below -verifyHttpHeaders <- function(headers = list(), expect_company_secret_header_to_be_equivalent_to_company_secret, expect_project_secret_header_to_be_equivalent_to_project_secret) +verifyHttpHeaders <- function(headers = character(0), expect_company_secret_header_to_be_equivalent_to_company_secret, expect_project_secret_header_to_be_equivalent_to_project_secret) { - companySecretHeader = headers["X-Q-Company-Secret"]; - companySecretHeader <- ifelse(is.null(companySecretHeader), "", companySecretHeader) - projectSecretHeader = headers["X-Q-Project-Secret"]; - projectSecretHeader <- ifelse(is.null(projectSecretHeader), "", projectSecretHeader) + companySecretHeader = headers[["X-Q-Company-Secret"]] %||% "" + projectSecretHeader = headers[["X-Q-Project-Secret"]] %||% "" httpResponse <- structure(list(status_code = 200, content = "mock"), class = "response") companySecret <- getCompanySecret() projectSecret <- getProjectSecret()