From 897cbc6f4437c60b121399f5b2da24690ee1bd93 Mon Sep 17 00:00:00 2001 From: Kevin Ushey Date: Wed, 10 Jun 2026 16:47:50 -0700 Subject: [PATCH] add 'as' parameter to getDelegatedAzureToken() With as = "AzureToken", the token is wrapped in an R6 object compatible with the AzureToken class from the AzureAuth package, so it can be passed directly to packages like AzureGraph and Microsoft365R. The object's refresh() method requests a new delegated token from Workbench. Also normalizes the token shape across IDEs: the direct Workbench RPC path now converts the relative 'expires_in' to an absolute 'expires_at', matching what RStudio sessions already returned. Addresses rstudio/rstudio#17619. --- DESCRIPTION | 3 +- NEWS.md | 9 ++ R/auth.R | 165 ++++++++++++++++++++++++++--- man/getDelegatedAzureToken.Rd | 24 ++++- man/jobAdd.Rd | 20 ++-- man/jobAddOutput.Rd | 20 ++-- man/jobAddProgress.Rd | 20 ++-- man/jobGetState.Rd | 20 ++-- man/jobList.Rd | 20 ++-- man/jobRemove.Rd | 20 ++-- man/jobRunScript.Rd | 20 ++-- man/jobSetProgress.Rd | 20 ++-- man/jobSetState.Rd | 20 ++-- man/jobSetStatus.Rd | 20 ++-- man/launcherAvailable.Rd | 26 ++--- man/launcherConfig.Rd | 26 ++--- man/launcherContainer.Rd | 26 ++--- man/launcherControlJob.Rd | 26 ++--- man/launcherGetInfo.Rd | 26 ++--- man/launcherGetJob.Rd | 26 ++--- man/launcherGetJobs.Rd | 26 ++--- man/launcherHostMount.Rd | 26 ++--- man/launcherNfsMount.Rd | 26 ++--- man/launcherPlacementConstraint.Rd | 26 ++--- man/launcherResourceLimit.Rd | 26 ++--- man/launcherSubmitJob.Rd | 26 ++--- man/launcherSubmitR.Rd | 26 ++--- tests/testthat/test-oauth.R | 95 +++++++++++++++++ 28 files changed, 546 insertions(+), 288 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index ccf1c71..3f450c0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -16,7 +16,6 @@ URL: https://rstudio.github.io/rstudioapi/, https://github.com/rstudio/rstudioapi BugReports: https://github.com/rstudio/rstudioapi/issues Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.3 Suggests: testthat, knitr, @@ -25,6 +24,8 @@ Suggests: covr, curl, jsonlite, + R6, withr VignetteBuilder: knitr Encoding: UTF-8 +Config/roxygen2/version: 8.0.0 diff --git a/NEWS.md b/NEWS.md index 3d39b0c..8a6abaf 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,14 @@ # rstudioapi (development version) +* `getDelegatedAzureToken()` gains an `as` argument. With `as = "AzureToken"`, + the token is returned as an R6 object compatible with the `AzureToken` class + from the `AzureAuth` package, so it can be passed directly to packages like + `AzureGraph` and `Microsoft365R`. (rstudio/rstudio#17619) + +* `getDelegatedAzureToken()` now returns the same token shape in all IDEs: + the relative `expires_in` field is converted to an absolute `expires_at` + timestamp, matching what RStudio sessions already returned. + * `getMode()` no longer fails on very old versions of RStudio that lack the internal `.rs.isDesktop()` helper. In that case, it now falls back to `versionInfo()$mode`, which has been available since RStudio 0.97.124. (#326) diff --git a/R/auth.R b/R/auth.R index e09a28e..75e8bda 100644 --- a/R/auth.R +++ b/R/auth.R @@ -14,41 +14,174 @@ #' #' @param resource The name of an Azure resource or service, normally a URL. #' -#' @return A list containing the OAuth2 token details. Throws an error if unavailable. +#' @param as The form of the returned token. `"list"` (the default) returns a +#' plain list of OAuth2 token details. `"AzureToken"` returns an R6 object +#' compatible with the `AzureToken` class from the \pkg{AzureAuth} package, +#' so it can be passed directly to packages like \pkg{AzureGraph} and +#' \pkg{Microsoft365R}. The \pkg{R6} package must be installed for this +#' option. +#' +#' @return When `as = "list"`, a list containing the OAuth2 token details, +#' including the fields `access_token`, `token_type`, `scope`, and +#' `expires_at` (the expiry time, in seconds since the Unix epoch). When +#' `as = "AzureToken"`, an R6 object of class `AzureToken` wrapping the same +#' details, with a `refresh()` method that requests a new delegated token +#' from Workbench. Throws an error if a token is unavailable. #' #' @examples #' \dontrun{ #' getDelegatedAzureToken("https://storage.azure.com") +#' +#' # Authenticate with Microsoft Graph using AzureGraph / Microsoft365R +#' token <- getDelegatedAzureToken("https://graph.microsoft.com/", as = "AzureToken") +#' gr <- AzureGraph::ms_graph$new(token = token) +#' site <- Microsoft365R::get_sharepoint_site( +#' site_url = "https://example.sharepoint.com/sites/my-site", +#' token = token +#' ) #' } #' @export -getDelegatedAzureToken <- function(resource) { +getDelegatedAzureToken <- function(resource, as = c("list", "AzureToken")) { if (missing(resource) || !is.character(resource) || length(resource) != 1 || !nzchar(resource)) { stop("resource must be a non-empty character string") } + as <- match.arg(as) # Try the internal RStudio API first (works in RStudio IDE) if (hasFun("getDelegatedAzureToken")) { - return(callFun("getDelegatedAzureToken", resource)) + token <- callFun("getDelegatedAzureToken", resource) + } else { + assertWorkbenchSession() + assertWorkbenchVersion(.WORKBENCH_FEATURE_DELEGATED_AZURE) + + body <- list( + params = list(jsonlite::unbox(resource)) + ) + + response <- callWorkbenchRPC( + method = "delegated_azure_token", + body = body, + error_context = "retrieving delegated Azure token" + ) + + if (is.null(response$token)) { + stop("Malformed response: missing 'token' field") + } + + token <- response$token } - assertWorkbenchSession() - assertWorkbenchVersion(.WORKBENCH_FEATURE_DELEGATED_AZURE) + token <- normalizeDelegatedAzureToken(token) - body <- list( - params = list(jsonlite::unbox(resource)) - ) + if (as == "AzureToken") { + return(asDelegatedAzureToken(token, resource)) + } - response <- callWorkbenchRPC( - method = "delegated_azure_token", - body = body, - error_context = "retrieving delegated Azure token" - ) + token +} + +# Internal helper to normalize delegated Azure token lists. The internal +# RStudio API converts the relative 'expires_in' to an absolute 'expires_at'; +# the direct Workbench RPC path returns the raw token endpoint response. Make +# both paths return the same shape. +normalizeDelegatedAzureToken <- function(token) { + if (is.null(token$expires_at) && !is.null(token$expires_in)) { + token$expires_at <- as.numeric(Sys.time()) + as.numeric(token$expires_in) + token$expires_in <- NULL + token$ext_expires_in <- NULL + } + + token +} + +# Internal helper to add the credential fields expected by consumers of the +# 'AzureToken' interface (e.g. AzureGraph::process_headers reads 'token_type' +# and 'access_token'; AzureAuth's validate() reads 'expires_on'). +delegatedAzureCredentials <- function(token) { + if (is.null(token$token_type)) { + token$token_type <- "Bearer" + } + + if (!is.null(token$expires_at)) { + token$expires_on <- as.character(round(as.numeric(token$expires_at))) + } + + token +} - if (is.null(response$token)) { - stop("Malformed response: missing 'token' field") +# Internal helper to wrap a delegated Azure token in an R6 object compatible +# with the 'AzureToken' class from the AzureAuth package. The object is not a +# subclass of AzureAuth's implementation (AzureAuth need not be installed); +# it provides the fields and methods that AzureGraph, Microsoft365R, and +# related packages rely on. Refreshing requests a new delegated token from +# Workbench, which owns the underlying refresh token. +asDelegatedAzureToken <- function(token, resource) { + if (!requireNamespace("R6", quietly = TRUE)) { + stop("Package 'R6' is required when as = \"AzureToken\". Please install it with: install.packages('R6')") } - response$token + generator <- R6::R6Class("AzureToken", + + public = list( + + version = 1, + resource = NULL, + scope = NULL, + tenant = NULL, + aad_host = NULL, + auth_type = "delegated", + client = NULL, + token_args = list(), + authorize_args = list(), + credentials = NULL, + + initialize = function(token, resource) { + self$credentials <- delegatedAzureCredentials(token) + self$resource <- resource + }, + + cache = function() { + invisible(self) + }, + + hash = function() { + paste0("rstudioapi-delegated-", gsub("[^A-Za-z0-9._-]+", "-", self$resource)) + }, + + validate = function() { + expires_on <- self$credentials$expires_on + if (is.null(expires_on) || is.na(expires_on)) { + return(TRUE) + } + + as.numeric(Sys.time()) < as.numeric(expires_on) + }, + + can_refresh = function() { + TRUE + }, + + refresh = function() { + token <- getDelegatedAzureToken(self$resource, as = "list") + self$credentials <- delegatedAzureCredentials(token) + invisible(self) + }, + + print = function(...) { + cat("\n") + cat(" resource:", self$resource, "\n") + expires_on <- self$credentials$expires_on + if (!is.null(expires_on)) { + expiry <- as.POSIXct(as.numeric(expires_on), origin = "1970-01-01") + cat(" expires:", format(expiry), "\n") + } + invisible(self) + } + + ) + ) + + generator$new(token, resource) } #' Get the User's Identity Token diff --git a/man/getDelegatedAzureToken.Rd b/man/getDelegatedAzureToken.Rd index 42ea325..72d1bcc 100644 --- a/man/getDelegatedAzureToken.Rd +++ b/man/getDelegatedAzureToken.Rd @@ -4,13 +4,25 @@ \alias{getDelegatedAzureToken} \title{OAuth2 Tokens for Delegated Azure Resources} \usage{ -getDelegatedAzureToken(resource) +getDelegatedAzureToken(resource, as = c("list", "AzureToken")) } \arguments{ \item{resource}{The name of an Azure resource or service, normally a URL.} + +\item{as}{The form of the returned token. \code{"list"} (the default) returns a +plain list of OAuth2 token details. \code{"AzureToken"} returns an R6 object +compatible with the \code{AzureToken} class from the \pkg{AzureAuth} package, +so it can be passed directly to packages like \pkg{AzureGraph} and +\pkg{Microsoft365R}. The \pkg{R6} package must be installed for this +option.} } \value{ -A list containing the OAuth2 token details. Throws an error if unavailable. +When \code{as = "list"}, a list containing the OAuth2 token details, +including the fields \code{access_token}, \code{token_type}, \code{scope}, and +\code{expires_at} (the expiry time, in seconds since the Unix epoch). When +\code{as = "AzureToken"}, an R6 object of class \code{AzureToken} wrapping the same +details, with a \code{refresh()} method that requests a new delegated token +from Workbench. Throws an error if a token is unavailable. } \description{ When Workbench is using Azure Active Directory for sign-in, this function can @@ -20,5 +32,13 @@ to. This requires configuring delegated permissions in Azure itself. \examples{ \dontrun{ getDelegatedAzureToken("https://storage.azure.com") + +# Authenticate with Microsoft Graph using AzureGraph / Microsoft365R +token <- getDelegatedAzureToken("https://graph.microsoft.com/", as = "AzureToken") +gr <- AzureGraph::ms_graph$new(token = token) +site <- Microsoft365R::get_sharepoint_site( + site_url = "https://example.sharepoint.com/sites/my-site", + token = token +) } } diff --git a/man/jobAdd.Rd b/man/jobAdd.Rd index e2d47a4..c6c51c2 100644 --- a/man/jobAdd.Rd +++ b/man/jobAdd.Rd @@ -60,15 +60,15 @@ the button will invoke the \code{replay} action.}} } \seealso{ -Other jobs: -\code{\link{jobAddOutput}()}, -\code{\link{jobAddProgress}()}, -\code{\link{jobGetState}()}, -\code{\link{jobList}()}, -\code{\link{jobRemove}()}, -\code{\link{jobRunScript}()}, -\code{\link{jobSetProgress}()}, -\code{\link{jobSetState}()}, -\code{\link{jobSetStatus}()} +Other jobs: +\code{\link[=jobAddOutput]{jobAddOutput()}}, +\code{\link[=jobAddProgress]{jobAddProgress()}}, +\code{\link[=jobGetState]{jobGetState()}}, +\code{\link[=jobList]{jobList()}}, +\code{\link[=jobRemove]{jobRemove()}}, +\code{\link[=jobRunScript]{jobRunScript()}}, +\code{\link[=jobSetProgress]{jobSetProgress()}}, +\code{\link[=jobSetState]{jobSetState()}}, +\code{\link[=jobSetStatus]{jobSetStatus()}} } \concept{jobs} diff --git a/man/jobAddOutput.Rd b/man/jobAddOutput.Rd index e0dc5e6..d3f6f7e 100644 --- a/man/jobAddOutput.Rd +++ b/man/jobAddOutput.Rd @@ -17,15 +17,15 @@ jobAddOutput(job, output, error = FALSE) Adds text output to a background job. } \seealso{ -Other jobs: -\code{\link{jobAdd}()}, -\code{\link{jobAddProgress}()}, -\code{\link{jobGetState}()}, -\code{\link{jobList}()}, -\code{\link{jobRemove}()}, -\code{\link{jobRunScript}()}, -\code{\link{jobSetProgress}()}, -\code{\link{jobSetState}()}, -\code{\link{jobSetStatus}()} +Other jobs: +\code{\link[=jobAdd]{jobAdd()}}, +\code{\link[=jobAddProgress]{jobAddProgress()}}, +\code{\link[=jobGetState]{jobGetState()}}, +\code{\link[=jobList]{jobList()}}, +\code{\link[=jobRemove]{jobRemove()}}, +\code{\link[=jobRunScript]{jobRunScript()}}, +\code{\link[=jobSetProgress]{jobSetProgress()}}, +\code{\link[=jobSetState]{jobSetState()}}, +\code{\link[=jobSetStatus]{jobSetStatus()}} } \concept{jobs} diff --git a/man/jobAddProgress.Rd b/man/jobAddProgress.Rd index 348aa96..27e7072 100644 --- a/man/jobAddProgress.Rd +++ b/man/jobAddProgress.Rd @@ -15,15 +15,15 @@ jobAddProgress(job, units) Adds incremental progress units to a background job. } \seealso{ -Other jobs: -\code{\link{jobAdd}()}, -\code{\link{jobAddOutput}()}, -\code{\link{jobGetState}()}, -\code{\link{jobList}()}, -\code{\link{jobRemove}()}, -\code{\link{jobRunScript}()}, -\code{\link{jobSetProgress}()}, -\code{\link{jobSetState}()}, -\code{\link{jobSetStatus}()} +Other jobs: +\code{\link[=jobAdd]{jobAdd()}}, +\code{\link[=jobAddOutput]{jobAddOutput()}}, +\code{\link[=jobGetState]{jobGetState()}}, +\code{\link[=jobList]{jobList()}}, +\code{\link[=jobRemove]{jobRemove()}}, +\code{\link[=jobRunScript]{jobRunScript()}}, +\code{\link[=jobSetProgress]{jobSetProgress()}}, +\code{\link[=jobSetState]{jobSetState()}}, +\code{\link[=jobSetStatus]{jobSetStatus()}} } \concept{jobs} diff --git a/man/jobGetState.Rd b/man/jobGetState.Rd index b2fbb1e..8dd5886 100644 --- a/man/jobGetState.Rd +++ b/man/jobGetState.Rd @@ -13,15 +13,15 @@ jobGetState(job) Get Background Job State } \seealso{ -Other jobs: -\code{\link{jobAdd}()}, -\code{\link{jobAddOutput}()}, -\code{\link{jobAddProgress}()}, -\code{\link{jobList}()}, -\code{\link{jobRemove}()}, -\code{\link{jobRunScript}()}, -\code{\link{jobSetProgress}()}, -\code{\link{jobSetState}()}, -\code{\link{jobSetStatus}()} +Other jobs: +\code{\link[=jobAdd]{jobAdd()}}, +\code{\link[=jobAddOutput]{jobAddOutput()}}, +\code{\link[=jobAddProgress]{jobAddProgress()}}, +\code{\link[=jobList]{jobList()}}, +\code{\link[=jobRemove]{jobRemove()}}, +\code{\link[=jobRunScript]{jobRunScript()}}, +\code{\link[=jobSetProgress]{jobSetProgress()}}, +\code{\link[=jobSetState]{jobSetState()}}, +\code{\link[=jobSetStatus]{jobSetStatus()}} } \concept{jobs} diff --git a/man/jobList.Rd b/man/jobList.Rd index 67129c8..76d877b 100644 --- a/man/jobList.Rd +++ b/man/jobList.Rd @@ -10,15 +10,15 @@ jobList() List any registered background jobs. } \seealso{ -Other jobs: -\code{\link{jobAdd}()}, -\code{\link{jobAddOutput}()}, -\code{\link{jobAddProgress}()}, -\code{\link{jobGetState}()}, -\code{\link{jobRemove}()}, -\code{\link{jobRunScript}()}, -\code{\link{jobSetProgress}()}, -\code{\link{jobSetState}()}, -\code{\link{jobSetStatus}()} +Other jobs: +\code{\link[=jobAdd]{jobAdd()}}, +\code{\link[=jobAddOutput]{jobAddOutput()}}, +\code{\link[=jobAddProgress]{jobAddProgress()}}, +\code{\link[=jobGetState]{jobGetState()}}, +\code{\link[=jobRemove]{jobRemove()}}, +\code{\link[=jobRunScript]{jobRunScript()}}, +\code{\link[=jobSetProgress]{jobSetProgress()}}, +\code{\link[=jobSetState]{jobSetState()}}, +\code{\link[=jobSetStatus]{jobSetStatus()}} } \concept{jobs} diff --git a/man/jobRemove.Rd b/man/jobRemove.Rd index 9e55bb9..9d28939 100644 --- a/man/jobRemove.Rd +++ b/man/jobRemove.Rd @@ -13,15 +13,15 @@ jobRemove(job) Remove a background job from RStudio's Background Jobs pane. } \seealso{ -Other jobs: -\code{\link{jobAdd}()}, -\code{\link{jobAddOutput}()}, -\code{\link{jobAddProgress}()}, -\code{\link{jobGetState}()}, -\code{\link{jobList}()}, -\code{\link{jobRunScript}()}, -\code{\link{jobSetProgress}()}, -\code{\link{jobSetState}()}, -\code{\link{jobSetStatus}()} +Other jobs: +\code{\link[=jobAdd]{jobAdd()}}, +\code{\link[=jobAddOutput]{jobAddOutput()}}, +\code{\link[=jobAddProgress]{jobAddProgress()}}, +\code{\link[=jobGetState]{jobGetState()}}, +\code{\link[=jobList]{jobList()}}, +\code{\link[=jobRunScript]{jobRunScript()}}, +\code{\link[=jobSetProgress]{jobSetProgress()}}, +\code{\link[=jobSetState]{jobSetState()}}, +\code{\link[=jobSetStatus]{jobSetStatus()}} } \concept{jobs} diff --git a/man/jobRunScript.Rd b/man/jobRunScript.Rd index aa28654..4992773 100644 --- a/man/jobRunScript.Rd +++ b/man/jobRunScript.Rd @@ -35,15 +35,15 @@ environment object to create an object with that name.} Starts an R script as a background job. } \seealso{ -Other jobs: -\code{\link{jobAdd}()}, -\code{\link{jobAddOutput}()}, -\code{\link{jobAddProgress}()}, -\code{\link{jobGetState}()}, -\code{\link{jobList}()}, -\code{\link{jobRemove}()}, -\code{\link{jobSetProgress}()}, -\code{\link{jobSetState}()}, -\code{\link{jobSetStatus}()} +Other jobs: +\code{\link[=jobAdd]{jobAdd()}}, +\code{\link[=jobAddOutput]{jobAddOutput()}}, +\code{\link[=jobAddProgress]{jobAddProgress()}}, +\code{\link[=jobGetState]{jobGetState()}}, +\code{\link[=jobList]{jobList()}}, +\code{\link[=jobRemove]{jobRemove()}}, +\code{\link[=jobSetProgress]{jobSetProgress()}}, +\code{\link[=jobSetState]{jobSetState()}}, +\code{\link[=jobSetStatus]{jobSetStatus()}} } \concept{jobs} diff --git a/man/jobSetProgress.Rd b/man/jobSetProgress.Rd index 7f435fc..fb51fac 100644 --- a/man/jobSetProgress.Rd +++ b/man/jobSetProgress.Rd @@ -15,15 +15,15 @@ jobSetProgress(job, units) Updates the progress for a background job. } \seealso{ -Other jobs: -\code{\link{jobAdd}()}, -\code{\link{jobAddOutput}()}, -\code{\link{jobAddProgress}()}, -\code{\link{jobGetState}()}, -\code{\link{jobList}()}, -\code{\link{jobRemove}()}, -\code{\link{jobRunScript}()}, -\code{\link{jobSetState}()}, -\code{\link{jobSetStatus}()} +Other jobs: +\code{\link[=jobAdd]{jobAdd()}}, +\code{\link[=jobAddOutput]{jobAddOutput()}}, +\code{\link[=jobAddProgress]{jobAddProgress()}}, +\code{\link[=jobGetState]{jobGetState()}}, +\code{\link[=jobList]{jobList()}}, +\code{\link[=jobRemove]{jobRemove()}}, +\code{\link[=jobRunScript]{jobRunScript()}}, +\code{\link[=jobSetState]{jobSetState()}}, +\code{\link[=jobSetStatus]{jobSetStatus()}} } \concept{jobs} diff --git a/man/jobSetState.Rd b/man/jobSetState.Rd index e38a86b..bfa4005 100644 --- a/man/jobSetState.Rd +++ b/man/jobSetState.Rd @@ -27,15 +27,15 @@ job was cancelled.} \item{failed}{The job finished but did not succeed.} } } \seealso{ -Other jobs: -\code{\link{jobAdd}()}, -\code{\link{jobAddOutput}()}, -\code{\link{jobAddProgress}()}, -\code{\link{jobGetState}()}, -\code{\link{jobList}()}, -\code{\link{jobRemove}()}, -\code{\link{jobRunScript}()}, -\code{\link{jobSetProgress}()}, -\code{\link{jobSetStatus}()} +Other jobs: +\code{\link[=jobAdd]{jobAdd()}}, +\code{\link[=jobAddOutput]{jobAddOutput()}}, +\code{\link[=jobAddProgress]{jobAddProgress()}}, +\code{\link[=jobGetState]{jobGetState()}}, +\code{\link[=jobList]{jobList()}}, +\code{\link[=jobRemove]{jobRemove()}}, +\code{\link[=jobRunScript]{jobRunScript()}}, +\code{\link[=jobSetProgress]{jobSetProgress()}}, +\code{\link[=jobSetStatus]{jobSetStatus()}} } \concept{jobs} diff --git a/man/jobSetStatus.Rd b/man/jobSetStatus.Rd index 53a57e8..e85f193 100644 --- a/man/jobSetStatus.Rd +++ b/man/jobSetStatus.Rd @@ -15,15 +15,15 @@ jobSetStatus(job, status) Update a background job's informational status text. } \seealso{ -Other jobs: -\code{\link{jobAdd}()}, -\code{\link{jobAddOutput}()}, -\code{\link{jobAddProgress}()}, -\code{\link{jobGetState}()}, -\code{\link{jobList}()}, -\code{\link{jobRemove}()}, -\code{\link{jobRunScript}()}, -\code{\link{jobSetProgress}()}, -\code{\link{jobSetState}()} +Other jobs: +\code{\link[=jobAdd]{jobAdd()}}, +\code{\link[=jobAddOutput]{jobAddOutput()}}, +\code{\link[=jobAddProgress]{jobAddProgress()}}, +\code{\link[=jobGetState]{jobGetState()}}, +\code{\link[=jobList]{jobList()}}, +\code{\link[=jobRemove]{jobRemove()}}, +\code{\link[=jobRunScript]{jobRunScript()}}, +\code{\link[=jobSetProgress]{jobSetProgress()}}, +\code{\link[=jobSetState]{jobSetState()}} } \concept{jobs} diff --git a/man/launcherAvailable.Rd b/man/launcherAvailable.Rd index 83bfc9b..ac33d97 100644 --- a/man/launcherAvailable.Rd +++ b/man/launcherAvailable.Rd @@ -12,18 +12,18 @@ Workbench jobs; that is, jobs normally launched by the user through the RStudio IDE's user interface. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherConfig}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitJob}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherConfig.Rd b/man/launcherConfig.Rd index 562361d..8eebfe2 100644 --- a/man/launcherConfig.Rd +++ b/man/launcherConfig.Rd @@ -17,18 +17,18 @@ Define a Workbench launcher configuration, suitable for use with the \code{confi argument to \code{\link[=launcherSubmitJob]{launcherSubmitJob()}}. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitJob}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherContainer.Rd b/man/launcherContainer.Rd index 2d9f1c5..7bb1f99 100644 --- a/man/launcherContainer.Rd +++ b/man/launcherContainer.Rd @@ -20,18 +20,18 @@ Define a launcher container, suitable for use with the \code{container} argument to \code{\link[=launcherSubmitJob]{launcherSubmitJob()}}. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherConfig}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitJob}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherControlJob.Rd b/man/launcherControlJob.Rd index 2298775..dbf06f1 100644 --- a/man/launcherControlJob.Rd +++ b/man/launcherControlJob.Rd @@ -21,18 +21,18 @@ your launcher plugin documentation to see which operations are supported.} Interact with a Workbench job. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherConfig}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitJob}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherGetInfo.Rd b/man/launcherGetInfo.Rd index 21289d5..5570301 100644 --- a/man/launcherGetInfo.Rd +++ b/man/launcherGetInfo.Rd @@ -11,18 +11,18 @@ Retrieve information about the Workbench launcher, as well as the different clus that the launcher has been configured to use. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherConfig}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitJob}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherGetJob.Rd b/man/launcherGetJob.Rd index ac3d8fa..dfa3e54 100644 --- a/man/launcherGetJob.Rd +++ b/man/launcherGetJob.Rd @@ -13,18 +13,18 @@ launcherGetJob(jobId) Retrieve information on a Workbench job with id \code{jobId}. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherConfig}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitJob}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherGetJobs.Rd b/man/launcherGetJobs.Rd index b3220bb..ebf4833 100644 --- a/man/launcherGetJobs.Rd +++ b/man/launcherGetJobs.Rd @@ -29,18 +29,18 @@ as RStudio R sessions?} Retrieve information on Workbench jobs. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherConfig}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitJob}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherHostMount.Rd b/man/launcherHostMount.Rd index 91a04bf..7f63a70 100644 --- a/man/launcherHostMount.Rd +++ b/man/launcherHostMount.Rd @@ -19,18 +19,18 @@ argument to \code{\link[=launcherSubmitJob]{launcherSubmitJob()}}. This can be used to mount a path from the host into the generated container. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherConfig}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitJob}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherNfsMount.Rd b/man/launcherNfsMount.Rd index 60d107a..7d1ff8d 100644 --- a/man/launcherNfsMount.Rd +++ b/man/launcherNfsMount.Rd @@ -22,18 +22,18 @@ be used to mount a path from a networked filesystem into a newly generated container. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherConfig}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitJob}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherPlacementConstraint.Rd b/man/launcherPlacementConstraint.Rd index 5b5c5a7..f69c74b 100644 --- a/man/launcherPlacementConstraint.Rd +++ b/man/launcherPlacementConstraint.Rd @@ -18,18 +18,18 @@ Define a launcher placement constraint, suitable for use with the \code{\link[=launcherSubmitJob]{launcherSubmitJob()}}. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherConfig}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitJob}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherResourceLimit.Rd b/man/launcherResourceLimit.Rd index 93a80f8..c689424 100644 --- a/man/launcherResourceLimit.Rd +++ b/man/launcherResourceLimit.Rd @@ -20,18 +20,18 @@ Define a launcher resource limit, suitable for use with the \code{\link[=launcherSubmitJob]{launcherSubmitJob()}}. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherConfig}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherSubmitJob}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherSubmitJob.Rd b/man/launcherSubmitJob.Rd index 7041024..56647df 100644 --- a/man/launcherSubmitJob.Rd +++ b/man/launcherSubmitJob.Rd @@ -99,18 +99,18 @@ Submit a Workbench job. See information. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherConfig}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitR}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitR]{launcherSubmitR()}} } \concept{job-launcher functionality} diff --git a/man/launcherSubmitR.Rd b/man/launcherSubmitR.Rd index d1214c0..1f36a86 100644 --- a/man/launcherSubmitR.Rd +++ b/man/launcherSubmitR.Rd @@ -24,18 +24,18 @@ See \code{\link[=launcherSubmitJob]{launcherSubmitJob()}} for running jobs with full control over command, environment, and so forth. } \seealso{ -Other job-launcher functionality: -\code{\link{launcherAvailable}()}, -\code{\link{launcherConfig}()}, -\code{\link{launcherContainer}()}, -\code{\link{launcherControlJob}()}, -\code{\link{launcherGetInfo}()}, -\code{\link{launcherGetJob}()}, -\code{\link{launcherGetJobs}()}, -\code{\link{launcherHostMount}()}, -\code{\link{launcherNfsMount}()}, -\code{\link{launcherPlacementConstraint}()}, -\code{\link{launcherResourceLimit}()}, -\code{\link{launcherSubmitJob}()} +Other job-launcher functionality: +\code{\link[=launcherAvailable]{launcherAvailable()}}, +\code{\link[=launcherConfig]{launcherConfig()}}, +\code{\link[=launcherContainer]{launcherContainer()}}, +\code{\link[=launcherControlJob]{launcherControlJob()}}, +\code{\link[=launcherGetInfo]{launcherGetInfo()}}, +\code{\link[=launcherGetJob]{launcherGetJob()}}, +\code{\link[=launcherGetJobs]{launcherGetJobs()}}, +\code{\link[=launcherHostMount]{launcherHostMount()}}, +\code{\link[=launcherNfsMount]{launcherNfsMount()}}, +\code{\link[=launcherPlacementConstraint]{launcherPlacementConstraint()}}, +\code{\link[=launcherResourceLimit]{launcherResourceLimit()}}, +\code{\link[=launcherSubmitJob]{launcherSubmitJob()}} } \concept{job-launcher functionality} diff --git a/tests/testthat/test-oauth.R b/tests/testthat/test-oauth.R index ea428be..ff3ab05 100644 --- a/tests/testthat/test-oauth.R +++ b/tests/testthat/test-oauth.R @@ -134,3 +134,98 @@ test_that("getIdentityToken handles missing RPC cookie", { "RPC cookie not found" ) }) + +test_that("getDelegatedAzureToken validates the 'as' parameter", { + expect_error( + getDelegatedAzureToken("https://storage.azure.com", as = "oops"), + "'arg' should be one of" + ) +}) + +test_that("normalizeDelegatedAzureToken converts expires_in to expires_at", { + token <- list( + access_token = "abc123", + expires_in = 3600, + ext_expires_in = 3600 + ) + + before <- as.numeric(Sys.time()) + normalized <- normalizeDelegatedAzureToken(token) + after <- as.numeric(Sys.time()) + + expect_null(normalized$expires_in) + expect_null(normalized$ext_expires_in) + expect_gte(normalized$expires_at, before + 3600) + expect_lte(normalized$expires_at, after + 3600) + + # Tokens that already carry expires_at are left alone + token <- list(access_token = "abc123", expires_at = 42) + expect_identical(normalizeDelegatedAzureToken(token), token) +}) + +test_that("asDelegatedAzureToken returns an AzureToken-compatible object", { + skip_if_not_installed("R6") + + token <- list( + access_token = "abc123", + scope = "https://graph.microsoft.com/.default", + expires_at = as.numeric(Sys.time()) + 3600 + ) + + obj <- asDelegatedAzureToken(token, "https://graph.microsoft.com/") + + # AzureAuth::is_azure_token() checks R6-ness and the class name + expect_true(R6::is.R6(obj)) + expect_s3_class(obj, "AzureToken") + + expect_identical(obj$resource, "https://graph.microsoft.com/") + expect_identical(obj$version, 1) + expect_identical(obj$credentials$access_token, "abc123") + expect_identical(obj$credentials$token_type, "Bearer") + expect_identical( + obj$credentials$expires_on, + as.character(round(token$expires_at)) + ) + + expect_true(obj$validate()) + expect_true(obj$can_refresh()) + expect_output(print(obj), "Delegated Azure token") +}) + +test_that("delegated AzureToken objects fail validation once expired", { + skip_if_not_installed("R6") + + token <- list( + access_token = "abc123", + expires_at = as.numeric(Sys.time()) - 10 + ) + + obj <- asDelegatedAzureToken(token, "https://graph.microsoft.com/") + expect_false(obj$validate()) +}) + +test_that("delegated AzureToken refresh requests a new token", { + skip_if_not_installed("R6") + + token <- list( + access_token = "old-token", + expires_at = as.numeric(Sys.time()) - 10 + ) + obj <- asDelegatedAzureToken(token, "https://graph.microsoft.com/") + + local_mocked_bindings( + getDelegatedAzureToken = function(resource, as = "list") { + list( + access_token = paste0("new-token-for-", resource), + expires_at = as.numeric(Sys.time()) + 3600 + ) + } + ) + + obj$refresh() + expect_identical( + obj$credentials$access_token, + "new-token-for-https://graph.microsoft.com/" + ) + expect_true(obj$validate()) +})