diff --git a/NAMESPACE b/NAMESPACE index c7a65b7..c10a2d6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,10 +6,10 @@ export(create_folder) export(download_file) export(my_user_id) export(my_workspaces) -export(new_document) -export(new_document_version) +export(new_version) export(objectiveR) export(objectiveR_auth) export(participant_bypass_2fa) export(participants) +export(upload_file) export(workspace_assets) diff --git a/R/download_file.R b/R/download.R similarity index 97% rename from R/download_file.R rename to R/download.R index f030d8d..894d8db 100644 --- a/R/download_file.R +++ b/R/download.R @@ -1,4 +1,4 @@ -#' Download file +#' Download a file #' #' @param document_uuid UUID of existing document #' @param folder Folder to save downloaded file to diff --git a/R/objectiveR.R b/R/objectiveR.R index 529a2d6..ccca20c 100644 --- a/R/objectiveR.R +++ b/R/objectiveR.R @@ -95,6 +95,94 @@ objectiveR <- function(endpoint, } +#' Authenticate HTTP request +#' +#' @description This sets the authorisation header of an HTTP request. If a +#' token variable exists in global environment, this is used, otherwise, +#' the user is prompted to enter an authenticating username and password. +#' +#' @param req An [httr2 request](https://httr2.r-lib.org/reference/request.html) +#' +#' @return A modified [httr2 request](https://httr2.r-lib.org/reference/request.html) +#' +#' @examples +#' \dontrun{ +#' httr2::request("http://example.com") |> +#' objectiveR_auth() +#' } +#' +#' token <- "test" +#' httr2::request("http://example.com") |> objectiveR_auth() +#' +#' @export + +objectiveR_auth <- function(req) { + + # Check request is correct type + if(!inherits(req, "httr2_request")) { + cli::cli_abort(c( + "x" = "{.var req} must be an HTTP2 request object" + )) + } + + if(exists("token", where = parent.frame())) { + + httr2::req_headers( + req, + Authorization = get("token", pos = parent.frame()) + ) + + } else { + + httr2::req_auth_basic( + req, + input_value("usr"), + input_value("pwd") + ) + + } + +} + + +#' Store session token from API response +#' +#' @param response An httr2 response object; must contain an 'Authorization' +#' header. +#' @param store_env The environment to bind the token to. +#' +#' @return Returns the token invisibly. This function is primarily used +#' for its side effect - an environment variable is created called "token". +#' +#' @noRd + +store_token <- function(response, store_env = globalenv()) { + + # Check response is in expected format + if(!inherits(response, "httr2_response")) { + cli::cli_abort(c( + "x" = "{.var response} must be an HTTP response object" + )) + } + + # Check Authorization header exists + if(!httr2::resp_header_exists(response, "Authorization")) { + cli::cli_abort(c( + "x" = "{.var response} must have Authorization header" + )) + } + + token <- httr2::resp_header(response, "Authorization") + + if(!exists("token", where = store_env)) { + rlang::env_poke(env = store_env, nm = "token", value = token) + } + + return(invisible(token)) + +} + + #' Translate error code into helpful message #' #' @param response An httr2 [httr2::response()][response] diff --git a/R/objectiveR_auth.R b/R/objectiveR_auth.R deleted file mode 100644 index 343e759..0000000 --- a/R/objectiveR_auth.R +++ /dev/null @@ -1,48 +0,0 @@ -#' Authenticate HTTP request -#' -#' @description This sets the authorisation header of an HTTP request. If a -#' token variable exists in global environment, this is used, otherwise, -#' the user is prompted to enter an authenticating username and password. -#' -#' @param req An [httr2 request](https://httr2.r-lib.org/reference/request.html) -#' -#' @return A modified [httr2 request](https://httr2.r-lib.org/reference/request.html) -#' -#' @examples -#' \dontrun{ -#' httr2::request("http://example.com") |> -#' objectiveR_auth() -#' } -#' -#' token <- "test" -#' httr2::request("http://example.com") |> objectiveR_auth() -#' -#' @export - -objectiveR_auth <- function(req) { - - # Check request is correct type - if(!inherits(req, "httr2_request")) { - cli::cli_abort(c( - "x" = "{.var req} must be an HTTP2 request object" - )) - } - - if(exists("token", where = parent.frame())) { - - httr2::req_headers( - req, - Authorization = get("token", pos = parent.frame()) - ) - - } else { - - httr2::req_auth_basic( - req, - input_value("usr"), - input_value("pwd") - ) - - } - -} diff --git a/R/random_uuid.R b/R/random_uuid.R deleted file mode 100644 index bc90ea4..0000000 --- a/R/random_uuid.R +++ /dev/null @@ -1,23 +0,0 @@ -#' Generate random UUID -#' -#' @param seed Integer to set seed for random sampling. Default value is NULL. -#' -#' @return A single character value in the format of eight blocks of four -#' letters/integers separated by dashes. -#' -#' @noRd - -random_uuid <- function(seed = NULL) { - - options <- c(letters, 0:9) - - # There should be equal probability of a letter or integer being sampled - prob_weights <- c(rep(1/26, 26), rep(1/10, 10)) - - set.seed(seed) - - replicate(8, paste0(sample(options, 4, replace = TRUE, prob = prob_weights), - collapse = "")) |> - paste0(collapse = "-") - -} diff --git a/R/store_token.R b/R/store_token.R deleted file mode 100644 index 1b6ec5b..0000000 --- a/R/store_token.R +++ /dev/null @@ -1,36 +0,0 @@ -#' Store session token from API response -#' -#' @param response An httr2 response object; must contain an 'Authorization' -#' header. -#' @param store_env The environment to bind the token to. -#' -#' @return Returns the token invisibly. This function is primarily used -#' for its side effect - an environment variable is created called "token". -#' -#' @noRd - -store_token <- function(response, store_env = globalenv()) { - - # Check response is in expected format - if(!inherits(response, "httr2_response")) { - cli::cli_abort(c( - "x" = "{.var response} must be an HTTP response object" - )) - } - - # Check Authorization header exists - if(!httr2::resp_header_exists(response, "Authorization")) { - cli::cli_abort(c( - "x" = "{.var response} must have Authorization header" - )) - } - - token <- httr2::resp_header(response, "Authorization") - - if(!exists("token", where = store_env)) { - rlang::env_poke(env = store_env, nm = "token", value = token) - } - - return(invisible(token)) - -} diff --git a/R/upload_file.R b/R/upload.R similarity index 80% rename from R/upload_file.R rename to R/upload.R index e89e2c5..ad2b5bc 100644 --- a/R/upload_file.R +++ b/R/upload.R @@ -1,4 +1,4 @@ -#' Create a new document +#' Upload a file to create a new document #' #' @param file File path of document to upload #' @param name Name to give document. If this isn't provided, the name of the @@ -12,12 +12,12 @@ #' #' @export -new_document <- function(file, - workspace_uuid, - name = NULL, - description = NULL, - parent_uuid = NULL, - use_proxy = FALSE) { +upload_file <- function(file, + workspace_uuid, + name = NULL, + description = NULL, + parent_uuid = NULL, + use_proxy = FALSE) { # If name not provided, use file name name <- if(is.null(name)) { @@ -49,7 +49,7 @@ new_document <- function(file, } -#' Create a new document version +#' Upload a file to create a new document version #' #' @param file File path of document to upload #' @param document_uuid UUID of existing document @@ -57,9 +57,9 @@ new_document <- function(file, #' #' @export -new_document_version <- function(file, - document_uuid, - use_proxy = FALSE) { +new_version <- function(file, + document_uuid, + use_proxy = FALSE) { response <- objectiveR( endpoint = "documents", @@ -82,4 +82,3 @@ new_document_version <- function(file, invisible(response) } - diff --git a/R/utils.R b/R/utils.R index eecec61..d2464c1 100644 --- a/R/utils.R +++ b/R/utils.R @@ -119,3 +119,28 @@ form_data_null <- function(value) { } } + + +#' Generate random UUID +#' +#' @param seed Integer to set seed for random sampling. Default value is NULL. +#' +#' @return A single character value in the format of eight blocks of four +#' letters/integers separated by dashes. +#' +#' @noRd + +random_uuid <- function(seed = NULL) { + + options <- c(letters, 0:9) + + # There should be equal probability of a letter or integer being sampled + prob_weights <- c(rep(1/26, 26), rep(1/10, 10)) + + set.seed(seed) + + replicate(8, paste0(sample(options, 4, replace = TRUE, prob = prob_weights), + collapse = "")) |> + paste0(collapse = "-") + +} diff --git a/_pkgdown.yml b/_pkgdown.yml index 5c990c6..bef52f1 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -62,9 +62,9 @@ reference: - workspace_assets - asset_info - create_folder - - new_document - - new_document_version - download_file + - upload_file + - new_version - title: API authentication contents: diff --git a/man/download_file.Rd b/man/download_file.Rd index 5383eb2..d48159d 100644 --- a/man/download_file.Rd +++ b/man/download_file.Rd @@ -1,8 +1,8 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/download_file.R +% Please edit documentation in R/download.R \name{download_file} \alias{download_file} -\title{Download file} +\title{Download a file} \usage{ download_file(document_uuid, folder, use_proxy = FALSE) } @@ -14,5 +14,5 @@ download_file(document_uuid, folder, use_proxy = FALSE) \item{use_proxy}{Logical to indicate whether to use proxy} } \description{ -Download file +Download a file } diff --git a/man/new_document_version.Rd b/man/new_version.Rd similarity index 50% rename from man/new_document_version.Rd rename to man/new_version.Rd index 395d918..87ed666 100644 --- a/man/new_document_version.Rd +++ b/man/new_version.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/upload_file.R -\name{new_document_version} -\alias{new_document_version} -\title{Create a new document version} +% Please edit documentation in R/upload.R +\name{new_version} +\alias{new_version} +\title{Upload a file to create a new document version} \usage{ -new_document_version(file, document_uuid, use_proxy = FALSE) +new_version(file, document_uuid, use_proxy = FALSE) } \arguments{ \item{file}{File path of document to upload} @@ -14,5 +14,5 @@ new_document_version(file, document_uuid, use_proxy = FALSE) \item{use_proxy}{Logical to indicate whether to use proxy} } \description{ -Create a new document version +Upload a file to create a new document version } diff --git a/man/objectiveR_auth.Rd b/man/objectiveR_auth.Rd index ca2d533..52c6bb9 100644 --- a/man/objectiveR_auth.Rd +++ b/man/objectiveR_auth.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/objectiveR_auth.R +% Please edit documentation in R/objectiveR.R \name{objectiveR_auth} \alias{objectiveR_auth} \title{Authenticate HTTP request} diff --git a/man/new_document.Rd b/man/upload_file.Rd similarity index 79% rename from man/new_document.Rd rename to man/upload_file.Rd index 5f5471e..20cd27d 100644 --- a/man/new_document.Rd +++ b/man/upload_file.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/upload_file.R -\name{new_document} -\alias{new_document} -\title{Create a new document} +% Please edit documentation in R/upload.R +\name{upload_file} +\alias{upload_file} +\title{Upload a file to create a new document} \usage{ -new_document( +upload_file( file, workspace_uuid, name = NULL, @@ -30,5 +30,5 @@ top-level of the workspace.} \item{use_proxy}{Logical to indicate whether to use proxy} } \description{ -Create a new document +Upload a file to create a new document } diff --git a/tests/testthat/test-objectiveR.R b/tests/testthat/test-objectiveR.R index 74c03fb..75fce1d 100644 --- a/tests/testthat/test-objectiveR.R +++ b/tests/testthat/test-objectiveR.R @@ -44,6 +44,81 @@ with_mock_api({ }) +# objectiveR_auth ---- + +req <- httr2::request("www.example.com") + +test_that("Error if invalid request supplied", { + expect_error(objectiveR_auth("req")) +}) + +test_that("httr2 request returned", { + + # expect_s3_class(objectiveR_auth(req, usr = "test", pwd = "test"), + # "httr2_request") + + .GlobalEnv$token <- "test" + expect_s3_class(objectiveR_auth(req), "httr2_request") + + rm(token, pos = .GlobalEnv) + +}) + +test_that("Correct authentication used", { + + with_envvar( + new = c("OBJECTIVER_USR" = "test_usr", + "OBJECTIVER_PWD" = "test_pwd"), + code = { + exp_usr_pwd <- objectiveR_auth(req) + expect_true(grepl("^Basic ", exp_usr_pwd$headers$Authorization)) + } + ) + + .GlobalEnv$token <- "test" + + exp_token1 <- objectiveR_auth(req) + expect_equal(exp_token1$headers$Authorization, "test") + + # Token used even when username and password supplied + with_envvar( + new = c("OBJECTIVER_USR" = "test_usr", + "OBJECTIVER_PWD" = "test_pwd"), + code = { + exp_token2 <- objectiveR_auth(req) + expect_equal(exp_token1$headers$Authorization, "test") + } + ) + + rm(token, pos = .GlobalEnv) + +}) + + +# store_token ---- + +test_that("Error if invalid response supplied", { + expect_error(store_token("resp")) +}) + +test_that("Error if Authorization header doesn't exist", { + expect_error(store_token(httr2::response())) +}) + +test_that("Function returns invisible object", { + expect_invisible( + httr2::response(headers = list(Authorization = "test1")) |> + store_token(store_env = environment()) + ) +}) + +test_that("Environment value created successfully", { + httr2::response(headers = list(Authorization = "test2")) |> + store_token(store_env = environment()) + expect_true(exists("token")) +}) + + # error ---- test_that("Expect character value returned", { diff --git a/tests/testthat/test-objectiveR_auth.R b/tests/testthat/test-objectiveR_auth.R deleted file mode 100644 index 9cf8ed1..0000000 --- a/tests/testthat/test-objectiveR_auth.R +++ /dev/null @@ -1,49 +0,0 @@ - -req <- httr2::request("www.example.com") - -test_that("Error if invalid request supplied", { - expect_error(objectiveR_auth("req")) -}) - -test_that("httr2 request returned", { - - # expect_s3_class(objectiveR_auth(req, usr = "test", pwd = "test"), - # "httr2_request") - - .GlobalEnv$token <- "test" - expect_s3_class(objectiveR_auth(req), "httr2_request") - - rm(token, pos = .GlobalEnv) - -}) - -test_that("Correct authentication used", { - - with_envvar( - new = c("OBJECTIVER_USR" = "test_usr", - "OBJECTIVER_PWD" = "test_pwd"), - code = { - exp_usr_pwd <- objectiveR_auth(req) - expect_true(grepl("^Basic ", exp_usr_pwd$headers$Authorization)) - } - ) - - .GlobalEnv$token <- "test" - - exp_token1 <- objectiveR_auth(req) - expect_equal(exp_token1$headers$Authorization, "test") - - # Token used even when username and password supplied - with_envvar( - new = c("OBJECTIVER_USR" = "test_usr", - "OBJECTIVER_PWD" = "test_pwd"), - code = { - exp_token2 <- objectiveR_auth(req) - expect_equal(exp_token1$headers$Authorization, "test") - } - ) - - rm(token, pos = .GlobalEnv) - -}) - diff --git a/tests/testthat/test-random_uuid.R b/tests/testthat/test-random_uuid.R deleted file mode 100644 index 91d5b01..0000000 --- a/tests/testthat/test-random_uuid.R +++ /dev/null @@ -1,34 +0,0 @@ - -test_that("Single character value returned", { - - expect_type(random_uuid(), "character") - - expect_length(random_uuid(), 1) - -}) - -test_that("Returned value format correct", { - - pattern_exp <- paste(rep("[a-z0-9]{4}", 8), collapse = "-") - - expect_true(grepl(pattern = pattern_exp, x = random_uuid())) - -}) - -test_that("Not setting seed returns random value", { - - sample_5 <- replicate(5, random_uuid()) - - expect_true(length(unique(sample_5)) == 5) - -}) - -test_that("Setting seed returns consistent value", { - - expect_identical( - random_uuid(1234), - random_uuid(1234) - ) - -}) - diff --git a/tests/testthat/test-store_token.R b/tests/testthat/test-store_token.R deleted file mode 100644 index b56f104..0000000 --- a/tests/testthat/test-store_token.R +++ /dev/null @@ -1,21 +0,0 @@ - -test_that("Error if invalid response supplied", { - expect_error(store_token("resp")) -}) - -test_that("Error if Authorization header doesn't exist", { - expect_error(store_token(httr2::response())) -}) - -test_that("Function returns invisible object", { - expect_invisible( - httr2::response(headers = list(Authorization = "test1")) |> - store_token(store_env = environment()) - ) -}) - -test_that("Environment value created successfully", { - httr2::response(headers = list(Authorization = "test2")) |> - store_token(store_env = environment()) - expect_true(exists("token")) -}) diff --git a/tests/testthat/test-upload_file.R b/tests/testthat/test-upload_file.R index f39c15a..2fa7f5d 100644 --- a/tests/testthat/test-upload_file.R +++ b/tests/testthat/test-upload_file.R @@ -7,7 +7,7 @@ with_file("test", { test_that("Valid request", { expect_POST( - new_document(file = "test", + upload_file(file = "test", workspace_uuid = "test_workspace"), paste0("https://secure.objectiveconnect.co.uk/publicapi/1/documents ", "Multipart form:\n ", @@ -17,7 +17,7 @@ with_file("test", { ) expect_POST( - new_document(file = "test", + upload_file(file = "test", name = "test_file_name", workspace_uuid = "test_workspace"), paste0("https://secure.objectiveconnect.co.uk/publicapi/1/documents ", @@ -28,7 +28,7 @@ with_file("test", { ) expect_POST( - new_document_version(file = "test", + new_version(file = "test", document_uuid = "test_asset"), paste0("https://secure.objectiveconnect.co.uk/publicapi/1/", "documents/test_asset/upload ", @@ -53,12 +53,12 @@ with_file("test", { test_that("Function returns invisible", { expect_invisible( - suppressMessages(new_document_version(file = "test", + suppressMessages(new_version(file = "test", document_uuid = "test_asset")) ) expect_invisible( - suppressMessages(new_document(file = "test", + suppressMessages(upload_file(file = "test", workspace_uuid = "test_workspace")) ) @@ -66,7 +66,7 @@ with_file("test", { test_that("Function returns success message", { - expect_message(new_document_version(file = "test", + expect_message(new_version(file = "test", document_uuid = "test_asset")) }) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 2c13b7d..74a32d4 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -95,3 +95,39 @@ test_that("Correct value returned", { expect_s3_class(form_data_null("test"), "form_data") }) + + +# random_uuid ---- + +test_that("Single character value returned", { + + expect_type(random_uuid(), "character") + + expect_length(random_uuid(), 1) + +}) + +test_that("Returned value format correct", { + + pattern_exp <- paste(rep("[a-z0-9]{4}", 8), collapse = "-") + + expect_true(grepl(pattern = pattern_exp, x = random_uuid())) + +}) + +test_that("Not setting seed returns random value", { + + sample_5 <- replicate(5, random_uuid()) + + expect_true(length(unique(sample_5)) == 5) + +}) + +test_that("Setting seed returns consistent value", { + + expect_identical( + random_uuid(1234), + random_uuid(1234) + ) + +}) diff --git a/vignettes/authentication.Rmd b/vignettes/authentication.Rmd index c9a0382..f3666d0 100644 --- a/vignettes/authentication.Rmd +++ b/vignettes/authentication.Rmd @@ -36,7 +36,7 @@ Add two variables to your `.Renviron` file to define your email address and pass usethis::edit_r_environ() ``` -* Add two variables as follows: +* Add two variables as follows (replacing `XXX` with your credentials): ```{r add_vars} OBJECTIVER_USR = "XXX" @@ -45,7 +45,7 @@ Add two variables to your `.Renviron` file to define your email address and pass * Save and close the `.Renviron` file. -* To check this has worked as expected, first restart your R session then: +* To check this has worked as expected, first restart your R session then run: ```{r check_vars} Sys.getenv("OBJECTIVER_USR") @@ -54,7 +54,7 @@ Add two variables to your `.Renviron` file to define your email address and pass Your credentials should be printed in the console. - Note: For this reason, it is important not to save your R session workspace on close as your console may contain your Objective Connect credentials. + Note: It is important not to save your R session workspace on close as your console may contain your Objective Connect credentials. The benefit of this method is that you can leave this information in your `.Renviron` file and `objectiveR` will automatically find them here each time you use the package. diff --git a/vignettes/objectiveR.Rmd b/vignettes/objectiveR.Rmd index 7c030a2..8e841d2 100644 --- a/vignettes/objectiveR.Rmd +++ b/vignettes/objectiveR.Rmd @@ -87,8 +87,8 @@ This returns a data frame with a row for each asset in the workspace. Among othe ```{r assets-output, eval = TRUE, echo = FALSE} data.frame( - uuid = sapply(3:4, objectiveR:::random_uuid), - name = c("asset1", "asset2") + asset_name = c("asset1", "asset2"), + asset_uuid = sapply(3:4, objectiveR:::random_uuid) ) ``` @@ -116,8 +116,8 @@ cli::cli_alert_success( To upload a file to a workspace, use the UUID of the workspace and the file path of the file to upload: ```{r upload} -new_document(here::here("data", "file2.csv"), - workspace_uuid = "84op-9qdu-c692-t4z1-wa4z-h9k3-8454-i71f") +upload_file(here::here("data", "file2.csv"), + workspace_uuid = "84op-9qdu-c692-t4z1-wa4z-h9k3-8454-i71f") ``` This function doesn't return anything but will display a message in the R console to indicate the upload has been a success and to confirm the name of the file. @@ -136,8 +136,8 @@ assets <- workspace_assets("84op-9qdu-c692-t4z1-wa4z-h9k3-8454-i71f") ```{r assets2-output, eval = TRUE, echo = FALSE} data.frame( - uuid = sapply(3:5, objectiveR:::random_uuid), - name = c("Asset 1", "Asset 2", "file2") + asset_name = c("Asset 1", "Asset 2", "file2"), + asset_uuid = sapply(3:5, objectiveR:::random_uuid) ) ```