diff --git a/DESCRIPTION b/DESCRIPTION index 61f46f4d..8853892e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: orderly2 Title: Orderly Next Generation -Version: 1.99.37 +Version: 1.99.38 Authors@R: c(person("Rich", "FitzJohn", role = c("aut", "cre"), email = "rich.fitzjohn@gmail.com"), person("Robert", "Ashton", role = "aut"), diff --git a/R/location.R b/R/location.R index 63f7d07b..03f84665 100644 --- a/R/location.R +++ b/R/location.R @@ -41,8 +41,12 @@ ##' * `url`: The location of the server ##' ##' * `token`: The value for your your login token (currently this is -##' a GitHub token with `read:org` scope). Later we'll expand this -##' as other authentication modes are supported. +##' a GitHub token with `read:org` scope). If missing or NULL, orderly2 will +##' perform an interactive authentication against GitHub to obtain one. +##' +##' * `save_token`: If no token is provided and interactive authentication is +##' used, this controls whether the GitHub token should be saved to disk. +##' Defaults to TRUE if missing. ##' ##' **Custom locations**: ##' @@ -110,7 +114,14 @@ orderly_location_add <- function(name, type, args, root = NULL, locate = TRUE) { assert_scalar_character(loc$args[[1]]$url, name = "args$url", call = environment()) assert_scalar_character(loc$args[[1]]$token, name = "args$token", + allow_null = TRUE, call = environment()) + assert_scalar_logical(loc$args[[1]]$save_token, name = "args$save_token", + allow_null = TRUE, + call = environment()) + if (!is.null(loc$args[[1]]$token) && !is.null(loc$args[[1]]$save_token)) { + cli::cli_abort("Cannot specify both 'token' and 'save_token'") + } } config <- root$config @@ -448,21 +459,22 @@ location_driver <- function(location_name, root) { i <- match(location_name, root$config$location$name) type <- root$config$location$type[[i]] args <- root$config$location$args[[i]] - switch(type, - path = orderly_location_path$new(args$path), - http = orderly_location_http$new(args$url), - packit = orderly_location_packit(args$url, args$token), - custom = orderly_location_custom(args)) + location <- switch(type, + path = orderly_location_path$new, + http = orderly_location_http$new, + packit = orderly_location_packit, + custom = orderly_location_custom) + do.call(location, args) } -orderly_location_custom <- function(args) { - driver <- check_symbol_from_str(args$driver, "args$driver") +orderly_location_custom <- function(driver, ...) { + driver <- check_symbol_from_str(driver, "args$driver") driver <- getExportedValue(driver$namespace, driver$symbol) if (inherits(driver, "R6ClassGenerator")) { driver <- driver$new } - do.call(driver, args[names(args) != "driver"]) + driver(...) } @@ -764,7 +776,7 @@ new_location_entry <- function(name, type, args, call = NULL) { } else if (type == "http") { required <- "url" } else if (type == "packit") { - required <- c("url", "token") + required <- "url" } else if (type == "custom") { required <- "driver" } diff --git a/R/location_http.R b/R/location_http.R index 5ed9df6f..60ce6ed4 100644 --- a/R/location_http.R +++ b/R/location_http.R @@ -1,17 +1,15 @@ orderly_location_http <- R6::R6Class( "orderly_location_http", - private = list( - client = NULL - ), - public = list( - initialize = function(url, auth = NULL) { - private$client <- outpack_http_client$new(url, auth) + client = NULL, + + initialize = function(url, authorise = NULL) { + self$client <- outpack_http_client$new(url, authorise) }, list = function() { - dat <- private$client$request("/metadata/list")$data + dat <- self$client$request("/metadata/list")$data data_frame( packet = vcapply(dat, "[[", "packet"), time = num_to_time(vnapply(dat, "[[", "time")), @@ -21,8 +19,8 @@ orderly_location_http <- R6::R6Class( metadata = function(packet_ids) { ret <- vcapply(packet_ids, function(id) { tryCatch( - trimws(private$client$request(sprintf("/metadata/%s/text", id), - parse_json = FALSE)), + trimws(self$client$request(sprintf("/metadata/%s/text", id), + parse_json = FALSE)), outpack_http_client_error = function(e) { if (e$code == 404) { e$message <- sprintf("Some packet ids not found: '%s'", id) @@ -43,7 +41,7 @@ orderly_location_http <- R6::R6Class( ## progress in the client, but there's not much point until ## then. tryCatch( - private$client$request(sprintf("/file/%s", hash), download = dest), + self$client$request(sprintf("/file/%s", hash), download = dest), outpack_http_client_error = function(e) { if (e$code == 404) { unlink(dest) @@ -56,21 +54,21 @@ orderly_location_http <- R6::R6Class( ## TODO: we could get the schemas here from outpack_server too list_unknown_packets = function(ids) { - res <- private$client$request( + res <- self$client$request( "/packets/missing", function(r) http_body_json(r, list(ids = ids, unpacked = scalar(TRUE)))) list_to_character(res$data) }, list_unknown_files = function(hashes) { - res <- private$client$request( + res <- self$client$request( "/files/missing", function(r) http_body_json(r, list(hashes = hashes))) list_to_character(res$data) }, push_file = function(src, hash) { - res <- private$client$request( + res <- self$client$request( sprintf("/file/%s", hash), function(r) httr2::req_body_file(r, src, "application/octet-stream")) @@ -79,33 +77,10 @@ orderly_location_http <- R6::R6Class( push_metadata = function(packet_id, hash, path) { meta <- read_string(path) - res <- private$client$request( + res <- self$client$request( sprintf("/packet/%s", hash), function(r) httr2::req_body_raw(r, meta, "text/plain")) invisible(NULL) } ) ) - - -orderly_location_packit <- function(url, token) { - assert_scalar_character(url) - assert_scalar_character(token) - if (grepl("^\\$", token)) { - token_variable <- sub("^\\$", "", token) - token <- Sys.getenv(token_variable, NA_character_) - if (is.na(token)) { - cli::cli_abort( - "Environment variable '{token_variable}' was not set") - } - } - - if (!grepl("/$", url)) { - url <- paste0(url, "/") - } - url_login <- paste0(url, "packit/api/auth/login/api") - url_outpack <- paste0(url, "packit/api/outpack") - - auth <- list(url = url_login, data = list(token = scalar(token))) - orderly_location_http$new(url_outpack, auth) -} diff --git a/R/location_packit.R b/R/location_packit.R new file mode 100644 index 00000000..f3cceb5e --- /dev/null +++ b/R/location_packit.R @@ -0,0 +1,87 @@ +github_oauth_client <- function() { + # Surprisingly, we don't actually need the Client ID here to match the one + # used by Packit. It should be fine to hardcode a value regardless of which + # server we are talking to. + httr2::oauth_client( + id = "Ov23liUrbkR0qUtAO1zu", + token_url = "https://github.com/login/oauth/access_token", + name = "orderly2" + ) +} + +do_oauth_device_flow <- function(base_url, cache_disk) { + # httr2 has a pretty unintuitive output when running interactively. + # It waits for the user to press and then opens up a browser, but the + # wording isn't super clear. It also does not work at all if a browser can't + # be opened, eg. in an SSH session. + # + # Thankfully, if we pretend to not be interactive the behaviour is a lot more + # obvious. It will just print the link to the console and with instructions + # for the user to open it up. + res <- rlang::with_interactive(value = FALSE, { + httr2::oauth_token_cached( + client = github_oauth_client(), + flow = httr2::oauth_flow_device, + flow_params = list( + auth_url = "https://github.com/login/device/code", + scope = "read:org"), + cache_disk = cache_disk) + }) + res$access_token +} + +# Logging in with packit is quite slow and we'll want to cache this; but we +# won't be holding a persistent handle to the root. So for now at least we'll +# keep a pool of generated bearer token headers, stored against the hash of the +# auth details. We only store this on successful login. +# +# This does mean there's no way to flush the cache and force a login, but that +# should hopefully not be that big a problem. We'll probably want to refresh +# the tokens from the request anyway. +# +# It also means the user cannot easily use two different identities on the same +# server from within the same session. +auth_cache <- new.env(parent = emptyenv()) +packit_authorisation <- function(base_url, token, save_token) { + key <- rlang::hash(list(base_url = base_url, token = token)) + + if (is.null(auth_cache[[key]])) { + cli::cli_alert_info("Logging in to {base_url}") + if (is.null(token)) { + token <- do_oauth_device_flow(base_url, cache_disk = save_token) + } + + login_url <- paste0(base_url, "packit/api/auth/login/api") + res <- http_client_request( + login_url, + function(r) http_body_json(r, list(token = scalar(token)))) + + cli::cli_alert_success("Logged in successfully") + + auth_cache[[key]] <- list("Authorization" = paste("Bearer", res$token)) + } + auth_cache[[key]] +} + +orderly_location_packit <- function(url, token = NULL, save_token = TRUE) { + assert_scalar_character(url) + assert_scalar_character(token, allow_null = TRUE) + assert_scalar_logical(save_token) + + if (!is.null(token) && grepl("^\\$", token)) { + token_variable <- sub("^\\$", "", token) + token <- Sys.getenv(token_variable, NA_character_) + if (is.na(token)) { + cli::cli_abort( + "Environment variable '{token_variable}' was not set") + } + } + + if (!grepl("/$", url)) { + url <- paste0(url, "/") + } + + orderly_location_http$new( + paste0(url, "packit/api/outpack"), + function() packit_authorisation(url, token, save_token)) +} diff --git a/R/outpack_http_client.R b/R/outpack_http_client.R index 991dc3f2..880a8ba0 100644 --- a/R/outpack_http_client.R +++ b/R/outpack_http_client.R @@ -3,31 +3,24 @@ outpack_http_client <- R6::R6Class( public = list( url = NULL, - auth = NULL, + authorise = NULL, - initialize = function(url, auth) { + initialize = function(url, authorise = NULL) { self$url <- sub("/$", "", url) - if (is.null(auth)) { - self$auth <- list(enabled = FALSE) + if (is.null(authorise)) { + self$authorise <- function() NULL } else { - self$auth <- list(enabled = TRUE, url = auth$url, data = auth$data) - } - }, - - authorise = function() { - needs_auth <- self$auth$enabled && is.null(self$auth$header) - if (needs_auth) { - self$auth$header <- http_client_login(self$url, self$auth) + self$authorise <- authorise } }, request = function(path, customize = identity, ...) { - self$authorise() + auth_headers <- self$authorise() http_client_request( self$url, function(r) { r <- httr2::req_url_path_append(r, path) - r <- httr2::req_headers(r, !!!self$auth$header) + r <- httr2::req_headers(r, !!!auth_headers) customize(r) }, ...) } @@ -92,29 +85,3 @@ http_client_error <- function(msg, code, errors) { class(err) <- c("outpack_http_client_error", "error", "condition") err } - - -## Logging in with packit is quite slow and we'll want to cache this; -## but we won't be holding a persistant handle to the root. So for -## now at least we'll keep a pool of generated bearer token headers, -## stored against the hash of the auth details (so the url and the -## token used to log in with). We only store this on successful -## login. -## -## This does mean there's no way to flush the cache and force a login, -## but that should hopefully not be that big a problem. We'll -## probably want to refresh the tokens from the request anyway. -auth_cache <- new.env(parent = emptyenv()) -http_client_login <- function(name, auth) { - key <- rlang::hash(auth) - if (is.null(auth_cache[[key]])) { - cli::cli_alert_info("Logging in to {name}") - - res <- http_client_request(auth$url, - function(r) http_body_json(r, auth$data)) - - cli::cli_alert_success("Logged in successfully") - auth_cache[[key]] <- list("Authorization" = paste("Bearer", res$token)) - } - auth_cache[[key]] -} diff --git a/man/orderly_location_add.Rd b/man/orderly_location_add.Rd index 742ac018..42ceb069 100644 --- a/man/orderly_location_add.Rd +++ b/man/orderly_location_add.Rd @@ -79,8 +79,11 @@ have more capabilities we think. \itemize{ \item \code{url}: The location of the server \item \code{token}: The value for your your login token (currently this is -a GitHub token with \code{read:org} scope). Later we'll expand this -as other authentication modes are supported. +a GitHub token with \code{read:org} scope). If missing or NULL, orderly2 will +perform an interactive authentication against GitHub to obtain one. +\item \code{save_token}: If no token is provided and interactive authentication is +used, this controls whether the GitHub token should be saved to disk. +Defaults to TRUE if missing. } \strong{Custom locations}: diff --git a/tests/testthat/helper-outpack-http.R b/tests/testthat/helper-outpack-http.R index 12c9c857..af09cb98 100644 --- a/tests/testthat/helper-outpack-http.R +++ b/tests/testthat/helper-outpack-http.R @@ -44,8 +44,8 @@ clear_auth_cache <- function() { rm(list = ls(auth_cache), envir = auth_cache) } -local_mock_response <- function(..., env = rlang::caller_env()) { - mock <- mockery::mock(mock_response(...)) +local_mock_response <- function(..., env = rlang::caller_env(), cycle = FALSE) { + mock <- mockery::mock(mock_response(...), cycle = cycle) httr2::local_mocked_responses(function(req) mock(req), env = env) mock } diff --git a/tests/testthat/test-location-packit.R b/tests/testthat/test-location-packit.R new file mode 100644 index 00000000..664c082d --- /dev/null +++ b/tests/testthat/test-location-packit.R @@ -0,0 +1,153 @@ +test_that("can authenticate with existing token", { + clear_auth_cache() + withr::defer(clear_auth_cache()) + + token <- "my-github-token" + + mock_post <- local_mock_response( + to_json(list(token = jsonlite::unbox("my-packit-token"))), + wrap = FALSE) + + res <- evaluate_promise( + packit_authorisation("http://example.com/", token)) + + expect_length(res$messages, 2) + expect_match(res$messages[[1]], "Logging in to http://example.com") + expect_match(res$messages[[2]], "Logged in successfully") + expect_equal(res$result, + list("Authorization" = paste("Bearer", "my-packit-token"))) + + mockery::expect_called(mock_post, 1) + args <- mockery::mock_args(mock_post)[[1]] + expect_equal(args[[1]]$url, "http://example.com/packit/api/auth/login/api") + expect_equal(args[[1]]$body$data, list(token = scalar("my-github-token"))) + expect_equal(args[[1]]$body$type, "json") + + ## And a second time, does not call mock_post again: + res2 <- expect_silent( + packit_authorisation("http://example.com/", token)) + expect_equal(res2, res$result) + mockery::expect_called(mock_post, 1) +}) + + +test_that("can authenticate using device flow", { + clear_auth_cache() + withr::defer(clear_auth_cache()) + + mock_post <- local_mock_response( + to_json(list(token = jsonlite::unbox("my-packit-token"))), + wrap = FALSE) + + mockery::stub(packit_authorisation, "do_oauth_device_flow", "my-github-token") + + res <- evaluate_promise(packit_authorisation("http://example.com/", + token = NULL, + save_token = TRUE)) + + expect_length(res$messages, 2) + expect_match(res$messages[[1]], "Logging in to http://example.com") + expect_match(res$messages[[2]], "Logged in successfully") + expect_equal(res$result, + list("Authorization" = paste("Bearer", "my-packit-token"))) + + mockery::expect_called(mock_post, 1) + args <- mockery::mock_args(mock_post)[[1]] + expect_equal(args[[1]]$url, "http://example.com/packit/api/auth/login/api") + expect_equal(args[[1]]$body$data, list(token = scalar("my-github-token"))) + expect_equal(args[[1]]$body$type, "json") +}) + +test_that("location_packit uses authentication", { + clear_auth_cache() + withr::defer(clear_auth_cache()) + + token <- "my-github-token" + id <- outpack_id() + metadata <- "packet metadata" + + mock_login <- mock_response( + to_json(list(token = jsonlite::unbox("my-packit-token"))), + wrap = FALSE) + mock_get <- mock_response(metadata) + mock <- mockery::mock(mock_login, mock_get) + httr2::local_mocked_responses(function(req) mock(req)) + + location <- orderly_location_packit("http://example.com", token) + res <- evaluate_promise(location$metadata(id)) + + expect_length(res$messages, 2) + expect_match(res$messages[[1]], "Logging in to http://example.com") + expect_match(res$messages[[2]], "Logged in successfully") + expect_equal(res$result, setNames(metadata, id)) + + mockery::expect_called(mock, 2) + + args <- mockery::mock_args(mock)[[1]] + expect_equal(args[[1]]$url, "http://example.com/packit/api/auth/login/api") + expect_equal(args[[1]]$body$data, list(token = scalar("my-github-token"))) + expect_equal(args[[1]]$body$type, "json") + + args <- mockery::mock_args(mock)[[2]] + expect_match(args[[1]]$url, + "http://example.com/packit/api/outpack/metadata/.*/text") + expect_equal(args[[1]]$headers, + list(Authorization = "Bearer my-packit-token"), + ignore_attr = TRUE) + + mock <- mockery::mock(mock_login, mock_get) +}) + + +test_that("Can configure oauth caching behaviour", { + clear_auth_cache() + withr::defer(clear_auth_cache()) + + mock_token <- mockery::mock("token", cycle = TRUE) + testthat::local_mocked_bindings(do_oauth_device_flow = mock_token) + + local_mock_response(to_json(list(token = jsonlite::unbox("my-packit-token"))), + cycle = TRUE) + + location <- orderly_location_packit("http://example.com", save_token = TRUE) + suppressMessages(location$client$authorise()) + + clear_auth_cache() + + location <- orderly_location_packit("http://example.com", save_token = FALSE) + suppressMessages(location$client$authorise()) + + mockery::expect_called(mock_token, 2) + args <- mockery::mock_args(mock_token) + expect_equal(args[[1]]$cache_disk, TRUE) + expect_equal(args[[2]]$cache_disk, FALSE) +}) + + +test_that("can create a packit location using an environment variable token", { + loc <- withr::with_envvar( + c("PACKIT_TOKEN" = "abc123"), + orderly_location_packit("http://example.com", "$PACKIT_TOKEN")) + + mock_login <- local_mock_response( + to_json(list(token = jsonlite::unbox("my-packit-token"))), + wrap = FALSE) + + evaluate_promise(loc$client$authorise()) + + mockery::expect_called(mock_login, 1) + + args <- mockery::mock_args(mock_login)[[1]] + expect_equal(args[[1]]$url, "http://example.com/packit/api/auth/login/api") + expect_equal(args[[1]]$body$data, list(token = scalar("abc123"))) + expect_equal(args[[1]]$body$type, "json") +}) + + +test_that("error of token variable not found", { + withr::with_envvar( + c("PACKIT_TOKEN" = NA_character_), + expect_error( + orderly_location_packit("https://example.com", "$PACKIT_TOKEN"), + "Environment variable 'PACKIT_TOKEN' was not set")) +}) diff --git a/tests/testthat/test-location.R b/tests/testthat/test-location.R index 9d20f278..114a4c26 100644 --- a/tests/testthat/test-location.R +++ b/tests/testthat/test-location.R @@ -810,11 +810,28 @@ test_that("validate arguments to packit locations", { expect_error( orderly_location_add("other", "packit", list(server = "example.com"), root = root), - "Fields missing from args: 'url' and 'token'") + "Field missing from args: 'url'") + + expect_error( + orderly_location_add("other", "packit", + list(url = "example.com", token = 123), + root = root), + "Expected 'args$token' to be character", fixed = TRUE) + + expect_error( + orderly_location_add("other", "packit", + list(url = "example.com", save_token = "value"), + root = root), + "Expected 'args$save_token' to be logical", fixed = TRUE) + expect_error( - orderly_location_add("other", "packit", list(url = "example.com"), + orderly_location_add("other", "packit", + list(url = "example.com", + token = "xx", + save_token = TRUE), root = root), - "Field missing from args: 'token'") + "Cannot specify both 'token' and 'save_token'", fixed = TRUE) + expect_equal(orderly_location_list(root = root), "local") }) @@ -825,74 +842,52 @@ test_that("can add a packit location", { list(url = "https://example.com", token = "abc123"), root = root) expect_equal(orderly_location_list(root = root), c("local", "other")) + + mock_driver <- mockery::mock() + mockery::stub(location_driver, "orderly_location_packit", mock_driver) + dr <- location_driver("other", root) - expect_s3_class(dr, "orderly_location_http") # not actually packit - cl <- dr$.__enclos_env__$private$client - expect_equal(cl$url, "https://example.com/packit/api/outpack") + + mockery::expect_called(mock_driver, 1) expect_equal( - cl$auth, - list(enabled = TRUE, - url = "https://example.com/packit/api/auth/login/api", - data = list(token = scalar("abc123")))) + mockery::mock_args(mock_driver)[[1]], + list(url = "https://example.com", token = "abc123")) }) +test_that("can add a packit location without a token", { + root <- create_temporary_root() + orderly_location_add("other", "packit", + list(url = "https://example.com"), + root = root) + expect_equal(orderly_location_list(root = root), c("local", "other")) -test_that("can create a packit location using an environment variable token", { - loc <- withr::with_envvar( - c("PACKIT_TOKEN" = "abc123"), - orderly_location_packit("https://example.com", "$PACKIT_TOKEN")) - client <- loc$.__enclos_env__$private$client - expect_equal( - client$auth, - list(enabled = TRUE, - url = "https://example.com/packit/api/auth/login/api", - data = list(token = scalar("abc123")))) - expect_equal( - client$url, - "https://example.com/packit/api/outpack") -}) + mock_driver <- mockery::mock() + mockery::stub(location_driver, "orderly_location_packit", mock_driver) + dr <- location_driver("other", root) -test_that("cope with trailing slash in url if needed", { - loc <- orderly_location_packit("https://example.com/", "abc123") - client <- loc$.__enclos_env__$private$client - expect_equal( - client$auth, - list(enabled = TRUE, - url = "https://example.com/packit/api/auth/login/api", - data = list(token = scalar("abc123")))) + mockery::expect_called(mock_driver, 1) expect_equal( - client$url, - "https://example.com/packit/api/outpack") + mockery::mock_args(mock_driver)[[1]], + list(url = "https://example.com")) }) - -test_that("error of token variable not found", { - withr::with_envvar( - c("PACKIT_TOKEN" = NA_character_), - expect_error( - orderly_location_packit("https://example.com", "$PACKIT_TOKEN"), - "Environment variable 'PACKIT_TOKEN' was not set")) +test_that("cope with trailing slash in url if needed", { + loc <- orderly_location_packit("https://example.com/", "abc123") + expect_equal(loc$client$url, "https://example.com/packit/api/outpack") }) test_that("can create an outpack location, disabling auth", { loc <- orderly_location_http$new("https://example.com", NULL) - client <- loc$.__enclos_env__$private$client - expect_equal( - client$auth, - list(enabled = FALSE)) - expect_equal( - client$url, - "https://example.com") + expect_equal(loc$client$authorise(), NULL) + expect_equal(loc$client$url, "https://example.com") }) test_that("strip trailing slash from outpack url", { loc <- orderly_location_http$new("https://example.com/", NULL) - expect_equal( - loc$.__enclos_env__$private$client$url, - "https://example.com") + expect_equal(loc$client$url, "https://example.com") }) @@ -901,8 +896,8 @@ test_that("can load a custom location driver", { mock_driver <- mockery::mock("value") mock_gev <- mockery::mock(mock_driver) mockery::stub(orderly_location_custom, "getExportedValue", mock_gev) - args <- list(driver = "foo::bar", a = 1, b = "other") - expect_equal(orderly_location_custom(args), "value") + expect_equal(orderly_location_custom(driver = "foo::bar", a = 1, b = "other"), + "value") mockery::expect_called(mock_gev, 1) expect_equal(mockery::mock_args(mock_gev)[[1]], list("foo", "bar")) @@ -919,8 +914,8 @@ test_that("can load a custom location driver using an R6 generator", { class = "R6ClassGenerator") mock_gev <- mockery::mock(mock_driver) mockery::stub(orderly_location_custom, "getExportedValue", mock_gev) - args <- list(driver = "foo::bar", a = 1, b = "other") - expect_equal(orderly_location_custom(args), "value") + expect_equal(orderly_location_custom(driver = "foo::bar", a = 1, b = "other"), + "value") mockery::expect_called(mock_gev, 1) expect_equal(mockery::mock_args(mock_gev)[[1]], list("foo", "bar")) @@ -948,7 +943,7 @@ test_that("can add a custom outpack location", { expect_equal(location_driver(loc$name, root), "value") mockery::expect_called(mock_orderly_location_custom, 1) expect_equal(mockery::mock_args(mock_orderly_location_custom)[[1]], - list(list(driver = "foo::bar", a = 1, b = 2))) + list(driver = "foo::bar", a = 1, b = 2)) }) diff --git a/tests/testthat/test-outpack-http-client.R b/tests/testthat/test-outpack-http-client.R index 58bcee86..699fa64d 100644 --- a/tests/testthat/test-outpack-http-client.R +++ b/tests/testthat/test-outpack-http-client.R @@ -145,111 +145,23 @@ test_that("can use the client to make requests", { test_that("can add auth details to the client", { - auth <- list(url = "http://example.com/api/login", - data = list(token = "mytoken")) - - cl <- outpack_http_client$new("http://example.com", auth) - h <- list("Authorization" = "Bearer yogi") - mock_login <- mockery::mock(h, cycle = TRUE) - mockery::stub(cl$authorise, "http_client_login", mock_login) - - cl$authorise() - - mockery::expect_called(mock_login, 1) - expect_equal( - mockery::mock_args(mock_login)[[1]], - list("http://example.com", - list(enabled = TRUE, url = auth$url, data = auth$data))) - expect_equal(cl$auth$header, h) + mock_authorise <- mockery::mock(h, cycle = TRUE) - ## Second call, does not log in - cl$authorise() - mockery::expect_called(mock_login, 1) + cl <- outpack_http_client$new("http://example.com", mock_authorise) - ## Actually perform an api call now: mock_get <- local_mock_response(json_string("[1,2,3]")) res <- cl$request("/path") expect_mapequal( res, list(status = "success", errors = NULL, data = list(1, 2, 3))) + mockery::expect_called(mock_authorise, 1) mockery::expect_called(mock_get, 1) + args <- mockery::mock_args(mock_get)[[1]] expect_equal(args[[1]]$url, "http://example.com/path") expect_equal(args[[1]]$headers, list(Authorization = "Bearer yogi"), ignore_attr = TRUE) }) - - -test_that("can send authentication request", { - clear_auth_cache() - withr::defer(clear_auth_cache()) - - mock_post <- local_mock_response( - to_json(list(token = jsonlite::unbox("mytoken"))), - wrap = FALSE) - - auth <- list( - url = "https://example.com/login", - data = list(token = "98e02a382db6a3a18e9d2e02c698478b")) - - res <- evaluate_promise( - http_client_login("foo", auth)) - - expect_length(res$messages, 2) - expect_match(res$messages[[1]], "Logging in to foo") - expect_match(res$messages[[2]], "Logged in successfully") - expect_equal(res$result, list("Authorization" = paste("Bearer", "mytoken"))) - - mockery::expect_called(mock_post, 1) - args <- mockery::mock_args(mock_post)[[1]] - expect_equal(args[[1]]$url, auth$url) - expect_equal(args[[1]]$body$data, list(token = auth$data$token)) - expect_equal(args[[1]]$body$type, "json") - - ## And a second time, does not call mock_post again: - expect_silent( - res2 <- http_client_login("foo", auth)) - expect_equal(res2, res$result) - mockery::expect_called(mock_post, 1) -}) - - -test_that("can send authenticated request", { - clear_auth_cache() - withr::defer(clear_auth_cache()) - - mock <- mockery::mock( - mock_response(to_json(list(token = jsonlite::unbox("mytoken"))), - wrap = FALSE), - mock_response(json_string("[1,2,3]"))) - httr2::local_mocked_responses(function(req) mock(req)) - - auth <- list( - url = "https://example.com/api/login", - data = list(token = "98e02a382db6a3a18e9d2e02c698478b")) - - cl <- outpack_http_client$new("http://example.com", auth) - res <- evaluate_promise(cl$request("data")) - - expect_length(res$messages, 2) - expect_match(res$messages[[1]], "Logging in to http://example.com") - expect_match(res$messages[[2]], "Logged in successfully") - expect_mapequal( - res$result, - list(status = "success", errors = NULL, data = list(1, 2, 3))) - - mockery::expect_called(mock, 2) - post_args <- mockery::mock_args(mock)[[1]] - expect_equal(post_args[[1]]$url, auth$url) - expect_equal(post_args[[1]]$body$data, list(token = auth$data$token)) - expect_equal(post_args[[1]]$body$type, "json") - - get_args <- mockery::mock_args(mock)[[2]] - expect_equal(get_args[[1]]$url, "http://example.com/data") - expect_equal(get_args[[1]]$headers, - list(Authorization = paste("Bearer mytoken")), - ignore_attr = TRUE) -})