diff --git a/NAMESPACE b/NAMESPACE index 45e1c70a..9abd8b71 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -8,8 +8,6 @@ S3method(api_build,op_head) S3method(as.data.frame,connect_integration_list) S3method(as.data.frame,connect_list_hits) S3method(as.data.frame,tbl_connect) -S3method(as_integration,default) -S3method(as_integration,list) S3method(as_tibble,connect_integration_list) S3method(as_tibble,connect_list_hits) S3method(connect_vars,op_base) @@ -60,12 +58,14 @@ export(content_title) export(content_update) export(content_update_access_type) export(content_update_owner) +export(create_integration) export(create_random_name) export(create_tag) export(create_tag_tree) export(dashboard_url) export(delete_bundle) export(delete_image) +export(delete_integration) export(delete_runtime_cache) export(delete_tag) export(delete_thumbnail) @@ -162,6 +162,7 @@ export(swap_vanity_url) export(swap_vanity_urls) export(tbl_connect) export(terminate_jobs) +export(update_integration) export(user_guid_from_username) export(users_create_remote) export(vanity_is_available) diff --git a/NEWS.md b/NEWS.md index b84b23a7..5ed138ec 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,9 @@ from a Connect server. (#431) - `get_integrations()` can now be passed a `Content` class object to retrieve a list of integrations associated with that piece of content. (#432) +- New functions allow you to manage the OAuth integrations on your Connect + server: `create_integration()`, `update_integration()` and + `delete_integration()`. (#434) # connectapi 0.8.0 diff --git a/R/integrations.R b/R/integrations.R index 288244ff..ad833bbc 100644 --- a/R/integrations.R +++ b/R/integrations.R @@ -11,7 +11,7 @@ #' @param x A `Connect` or `Content` R6 object. #' #' @return A list of class `connect_integration_list`, where each element is a `connect_integration` object -#' with the following fields (all character strings unless noted otherwise): +#' with the following fields. (Raw API fields are character strings unless noted otherwise): #' #' * `id`: The internal identifier of this OAuth integration. #' * `guid`: The GUID of this OAuth integration. @@ -67,7 +67,7 @@ get_integrations.default <- function(x) { stop( "Cannot get integrations for an object of class '", class(x)[1], - "'" + "'. 'x' must be a 'Connect' or 'Content' object." ) } @@ -75,7 +75,7 @@ get_integrations.default <- function(x) { get_integrations.Connect <- function(x) { error_if_less_than(x$version, "2024.12.0") integrations <- x$GET(v1_url("oauth", "integrations")) - integrations <- lapply(integrations, as_integration) + integrations <- purrr::map(integrations, ~ as_integration(.x, client = x)) class(integrations) <- c("connect_integration_list", class(integrations)) integrations } @@ -101,7 +101,7 @@ get_integrations.Content <- function(x) { #' Convert integrations list to a data frame #' #' @description -#' Converts an list returned by [get_integrations()] into a data frame. +#' Converts a list returned by [get_integrations()] into a data frame. #' #' @param x A `connect_integration_list` object (from [get_integrations()]). #' @param row.names Passed to [base::as.data.frame()]. @@ -144,25 +144,20 @@ as_tibble.connect_integration_list <- function(x, ...) { #' Convert objects to integration class #' #' @param x An object to convert to an integration. -#' @param ... Unused. +#' @param client The Connect client object where the integration comes from. #' -#' @return An integration object -as_integration <- function(x, ...) { - UseMethod("as_integration") -} - -#' @export -as_integration.default <- function(x, ...) { - stop( - "Cannot convert object of class '", - class(x)[1], - "' to an integration" - ) -} - -#' @export -as_integration.list <- function(x, ...) { - structure(x, class = c("connect_integration", "list")) +#' @return An integration object. The object has all the fields from the +#' integrations endpoint (see [get_integrations()]) and a Connect client as a +#' `client` attribute (`attr(x, "client")`) +as_integration <- function(x, client) { + if (!inherits(x, "list")) { + stop( + "Cannot convert object of class '", + class(x)[1], + "' to an integration" + ) + } + structure(x, class = c("connect_integration", "list"), client = client) } #' @export @@ -212,10 +207,11 @@ print.connect_integration <- function(x, ...) { #' @export get_integration <- function(client, guid) { validate_R6_class(client, "Connect") - as_integration(client$GET(v1_url("oauth", "integrations", guid))) + error_if_less_than(client$version, "2024.12.0") + as_integration(client$GET(v1_url("oauth", "integrations", guid)), client) } -# Get and set integrations on content +# Get and set integrations on content ---- #' Set all OAuth integrations for a content item #' @@ -341,3 +337,209 @@ get_associations <- function(x) { "associations" )) } + + +# Manage integrations ---- + +#' Create an OAuth integration +#' +#' @description +#' Creates a new OAuth integration on the Posit Connect server. OAuth integrations +#' allow content to access external resources using OAuth credentials. +#' +#' You must have administrator privileges to perform this action. +#' +#' See the Posit Connect documentation on +#' [OAuth integrations](https://docs.posit.co/connect/admin/integrations/oauth-integrations/) for +#' more information. +#' +#' @param client A `Connect` R6 client object. +#' @param name A descriptive name to identify the integration. +#' @param description Optional, default `NULL.` A brief description of the integration. +#' @param template The template to use to configure this integration (e.g., +#' "custom", "github", "google", "connect"). +#' @param config A list containing the configuration for the integration. The +#' required fields vary depending on the template selected. +#' +#' @return A `connect_integration` object representing the newly created +#' integration. See [get_integration()] for details on the returned object. +#' +#' @seealso [get_integrations()], [get_integration()], [update_integration()], +#' [delete_integration()] +#' +#' @examples +#' \dontrun{ +#' client <- connect() +#' +#' # Create a GitHub OAuth integration +#' github_integration <- create_integration( +#' client, +#' name = "GitHub Integration", +#' description = "Integration with GitHub for OAuth access", +#' template = "github", +#' config = list( +#' client_id = "your-client-id", +#' client_secret = "your-client-secret" +#' ) +#' ) +#' +#' # Create a custom OAuth integration +#' custom_integration <- create_integration( +#' client, +#' name = "Custom API Integration", +#' description = "Integration with our custom API", +#' template = "custom", +#' config = list( +#' auth_mode = "Confidential", +#' auth_type = "Viewer", +#' authorization_uri = "https://api.example.com/oauth/authorize", +#' client_id = "your-client-id", +#' client_secret = "your-client-secret", +#' token_uri = "https://api.example.com/oauth/token" +#' ) +#' ) +#' } +#' +#' @family oauth integration functions +#' @export +create_integration <- function( + client, + name, + description = NULL, + template, + config +) { + validate_R6_class(client, "Connect") + error_if_less_than(client$version, "2024.12.0") + result <- client$POST( + v1_url("oauth", "integrations"), + body = list( + name = name, + description = description, + template = template, + config = config + ) + ) + as_integration(result, client) +} + +#' Update an OAuth integration +#' +#' @description +#' Updates an existing OAuth integration. All fields except `integration` are optional, +#' and are unchanged if not provided. +#' +#' You must have administrator privileges to perform this action. +#' +#' See the Posit Connect documentation on +#' [OAuth integrations](https://docs.posit.co/connect/admin/integrations/oauth-integrations/) for +#' more information. +#' +#' @param integration A `connect_integration` object (as returned by [get_integrations()], +#' [get_integration()], or [create_integration()]). +#' @param name A new name for the integration. +#' @param description A new description for the integration. +#' @param template The template to use (generally not changed after creation). +#' @param config A list with updated OAuth integration configuration. If `NULL` +#' (default), the configuration remains unchanged. You can update individual +#' configuration fields without affecting others. +#' +#' @return A `connect_integration` object representing the updated OAuth +#' integration. See [get_integration()] for details on the returned object. +#' +#' @seealso [get_integrations()], [get_integration()], [create_integration()], +#' [delete_integration()] +#' +#' @examples +#' \dontrun{ +#' client <- connect() +#' +#' # Get an existing integration +#' integration <- get_integration(client, "your-integration-guid") +#' +#' # Update the integration's name and description +#' updated_integration <- update_integration( +#' integration, +#' name = "Updated GitHub Integration", +#' description = "A more descriptive description." +#' ) +#' +#' # Update only the client secret in the configuration +#' updated_integration <- update_integration( +#' integration, +#' config = list( +#' client_secret = "your-new-client-secret" +#' ) +#' ) +#' } +#' +#' @family oauth integration functions +#' @export +update_integration <- function( + integration, + name = NULL, + description = NULL, + template = NULL, + config = NULL +) { + if (!inherits(integration, "connect_integration")) { + stop("'integration' must be a 'connect_integration' object") + } + client <- attr(integration, "client") + validate_R6_class(client, "Connect") + error_if_less_than(client$version, "2024.12.0") + result <- client$PATCH( + v1_url("oauth", "integrations", integration$guid), + body = list( + name = name, + description = description, + template = template, + config = config + ) + ) + as_integration(result, client) +} + +#' Delete an OAuth integration +#' +#' @description +#' Deletes an OAuth integration from the Posit Connect server. This permanently +#' removes the integration and any associated content associations. +#' +#' You must have administrator privileges to perform this action. +#' +#' See the Posit Connect documentation on +#' [OAuth integrations](https://docs.posit.co/connect/admin/integrations/oauth-integrations/) for +#' more information. +#' +#' @param integration A `connect_integration` object (as returned by [get_integrations()], +#' [get_integration()], or [create_integration()]). +#' +#' @return Returns `NULL` invisibly if successful. +#' +#' @seealso [get_integrations()], [get_integration()], [create_integration()], +#' [update_integration()] +#' +#' @examples +#' \dontrun{ +#' client <- connect() +#' +#' # Get an integration to delete +#' integration <- get_integration(client, "your-integration-guid") +#' +#' # Delete the integration +#' delete_integration(integration) +#' } +#' +#' @family oauth integration functions +#' @export +delete_integration <- function(integration) { + if (!inherits(integration, "connect_integration")) { + stop("'integration' must be a 'connect_integration' object") + } + client <- attr(integration, "client") + validate_R6_class(client, "Connect") + error_if_less_than(client$version, "2024.12.0") + client$DELETE(v1_url("oauth", "integrations", integration$guid)) + invisible(NULL) +} diff --git a/R/utils-ci.R b/R/utils-ci.R index aa7272c8..1b0974ed 100644 --- a/R/utils-ci.R +++ b/R/utils-ci.R @@ -107,10 +107,6 @@ compose_find_hosts <- function(prefix) { ports <- sub(".*0\\.0\\.0\\.0:([0-9]+)->3939.*", "\\1", containers) cat_line(glue::glue("docker: got ports {ports[1]} and {ports[2]}")) - # TODO: make this silly sleep more savvy - cat_line("connect: sleeping - waiting for connect to start") - Sys.sleep(10) - paste0("http://localhost:", ports) } @@ -139,7 +135,9 @@ update_renviron_creds <- function( "{prefix}_API_KEY={api_key}", .sep = "\n" ) - if (!fs::file_exists(.file)) fs::file_touch(.file) + if (!fs::file_exists(.file)) { + fs::file_touch(.file) + } writeLines(output_environ, .file) invisible() } @@ -164,6 +162,36 @@ build_test_env <- function( # this is a regex so it will match either hosts <- compose_find_hosts(prefix = "ci.connect") + wait_for_connect_ready <- function(host, timeout = 120) { + client <- HackyConnect$new(server = host, api_key = NULL) + start_time <- Sys.time() + last_msg <- start_time + ping_url <- client$server_url("__ping__") + + while ( + as.numeric(difftime(Sys.time(), start_time, units = "secs")) < timeout + ) { + ok <- try( + { + res <- client$GET(url = client$server_url("__ping__"), parser = NULL) + httr::status_code(res) == 200 + } + ) + if (isTRUE(ok)) { + return(invisible(TRUE)) + } + if (difftime(Sys.time(), last_msg, units = "secs") >= 5) { + cat_line(glue::glue("waiting for {ping_url} ...")) + last_msg <- Sys.time() + } + Sys.sleep(1) + } + stop("Connect did not become ready in time: ", ping_url) + } + + wait_for_connect_ready(hosts[1]) + wait_for_connect_ready(hosts[2]) + cat_line("connect: creating first admin...") a1 <- create_first_admin( hosts[1], diff --git a/_pkgdown.yml b/_pkgdown.yml index ea87909f..dd5fe2b1 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -72,6 +72,9 @@ reference: - get_associations - get_oauth_credentials - get_oauth_content_credentials + - create_integration + - update_integration + - delete_integration - title: "Reporting" desc: > diff --git a/man/as.data.frame.connect_integration_list.Rd b/man/as.data.frame.connect_integration_list.Rd index b7cfb1d0..cf7bb0c6 100644 --- a/man/as.data.frame.connect_integration_list.Rd +++ b/man/as.data.frame.connect_integration_list.Rd @@ -19,5 +19,5 @@ A \code{data.frame} with one row per integration. } \description{ -Converts an list returned by \code{\link[=get_integrations]{get_integrations()}} into a data frame. +Converts a list returned by \code{\link[=get_integrations]{get_integrations()}} into a data frame. } diff --git a/man/as_integration.Rd b/man/as_integration.Rd index 43c312ae..4d02f1ff 100644 --- a/man/as_integration.Rd +++ b/man/as_integration.Rd @@ -4,15 +4,17 @@ \alias{as_integration} \title{Convert objects to integration class} \usage{ -as_integration(x, ...) +as_integration(x, client) } \arguments{ \item{x}{An object to convert to an integration.} -\item{...}{Unused.} +\item{client}{The Connect client object where the integration comes from.} } \value{ -An integration object +An integration object. The object has all the fields from the +integrations endpoint (see \code{\link[=get_integrations]{get_integrations()}}) and a Connect client as a +\code{client} attribute (\code{attr(x, "client")}) } \description{ Convert objects to integration class diff --git a/man/create_integration.Rd b/man/create_integration.Rd new file mode 100644 index 00000000..f7f72a07 --- /dev/null +++ b/man/create_integration.Rd @@ -0,0 +1,82 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/integrations.R +\name{create_integration} +\alias{create_integration} +\title{Create an OAuth integration} +\usage{ +create_integration(client, name, description = NULL, template, config) +} +\arguments{ +\item{client}{A \code{Connect} R6 client object.} + +\item{name}{A descriptive name to identify the integration.} + +\item{description}{Optional, default \code{NULL.} A brief description of the integration.} + +\item{template}{The template to use to configure this integration (e.g., +"custom", "github", "google", "connect").} + +\item{config}{A list containing the configuration for the integration. The +required fields vary depending on the template selected.} +} +\value{ +A \code{connect_integration} object representing the newly created +integration. See \code{\link[=get_integration]{get_integration()}} for details on the returned object. +} +\description{ +Creates a new OAuth integration on the Posit Connect server. OAuth integrations +allow content to access external resources using OAuth credentials. + +You must have administrator privileges to perform this action. + +See the Posit Connect documentation on +\href{https://docs.posit.co/connect/admin/integrations/oauth-integrations/}{OAuth integrations} for +more information. +} +\examples{ +\dontrun{ +client <- connect() + +# Create a GitHub OAuth integration +github_integration <- create_integration( + client, + name = "GitHub Integration", + description = "Integration with GitHub for OAuth access", + template = "github", + config = list( + client_id = "your-client-id", + client_secret = "your-client-secret" + ) +) + +# Create a custom OAuth integration +custom_integration <- create_integration( + client, + name = "Custom API Integration", + description = "Integration with our custom API", + template = "custom", + config = list( + auth_mode = "Confidential", + auth_type = "Viewer", + authorization_uri = "https://api.example.com/oauth/authorize", + client_id = "your-client-id", + client_secret = "your-client-secret", + token_uri = "https://api.example.com/oauth/token" + ) +) +} + +} +\seealso{ +\code{\link[=get_integrations]{get_integrations()}}, \code{\link[=get_integration]{get_integration()}}, \code{\link[=update_integration]{update_integration()}}, +\code{\link[=delete_integration]{delete_integration()}} + +Other oauth integration functions: +\code{\link{delete_integration}()}, +\code{\link{get_associations}()}, +\code{\link{get_integration}()}, +\code{\link{get_integrations}()}, +\code{\link{set_integrations}()}, +\code{\link{update_integration}()} +} +\concept{oauth integration functions} diff --git a/man/delete_integration.Rd b/man/delete_integration.Rd new file mode 100644 index 00000000..416e6384 --- /dev/null +++ b/man/delete_integration.Rd @@ -0,0 +1,50 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/integrations.R +\name{delete_integration} +\alias{delete_integration} +\title{Delete an OAuth integration} +\usage{ +delete_integration(integration) +} +\arguments{ +\item{integration}{A \code{connect_integration} object (as returned by \code{\link[=get_integrations]{get_integrations()}}, +\code{\link[=get_integration]{get_integration()}}, or \code{\link[=create_integration]{create_integration()}}).} +} +\value{ +Returns \code{NULL} invisibly if successful. +} +\description{ +Deletes an OAuth integration from the Posit Connect server. This permanently +removes the integration and any associated content associations. + +You must have administrator privileges to perform this action. + +See the Posit Connect documentation on +\href{https://docs.posit.co/connect/admin/integrations/oauth-integrations/}{OAuth integrations} for +more information. +} +\examples{ +\dontrun{ +client <- connect() + +# Get an integration to delete +integration <- get_integration(client, "your-integration-guid") + +# Delete the integration +delete_integration(integration) +} + +} +\seealso{ +\code{\link[=get_integrations]{get_integrations()}}, \code{\link[=get_integration]{get_integration()}}, \code{\link[=create_integration]{create_integration()}}, +\code{\link[=update_integration]{update_integration()}} + +Other oauth integration functions: +\code{\link{create_integration}()}, +\code{\link{get_associations}()}, +\code{\link{get_integration}()}, +\code{\link{get_integrations}()}, +\code{\link{set_integrations}()}, +\code{\link{update_integration}()} +} +\concept{oauth integration functions} diff --git a/man/get_associations.Rd b/man/get_associations.Rd index 92bcde3f..c2c762ad 100644 --- a/man/get_associations.Rd +++ b/man/get_associations.Rd @@ -48,9 +48,12 @@ my_app_integrations <- purrr::map( \code{\link[=set_integrations]{set_integrations()}}, \code{\link[=get_integrations]{get_integrations()}}, \code{\link[=get_integration]{get_integration()}} Other oauth integration functions: +\code{\link{create_integration}()}, +\code{\link{delete_integration}()}, \code{\link{get_integration}()}, \code{\link{get_integrations}()}, -\code{\link{set_integrations}()} +\code{\link{set_integrations}()}, +\code{\link{update_integration}()} Other content functions: \code{\link{content_delete}()}, diff --git a/man/get_integration.Rd b/man/get_integration.Rd index e92a6346..b7a8055b 100644 --- a/man/get_integration.Rd +++ b/man/get_integration.Rd @@ -46,8 +46,11 @@ x <- get_integration(client, guid) \code{\link[=get_integrations]{get_integrations()}}, \code{\link[=get_associations]{get_associations()}}, \code{\link[=set_integrations]{set_integrations()}} Other oauth integration functions: +\code{\link{create_integration}()}, +\code{\link{delete_integration}()}, \code{\link{get_associations}()}, \code{\link{get_integrations}()}, -\code{\link{set_integrations}()} +\code{\link{set_integrations}()}, +\code{\link{update_integration}()} } \concept{oauth integration functions} diff --git a/man/get_integrations.Rd b/man/get_integrations.Rd index 18e6c475..25b354a0 100644 --- a/man/get_integrations.Rd +++ b/man/get_integrations.Rd @@ -11,7 +11,7 @@ get_integrations(x) } \value{ A list of class \code{connect_integration_list}, where each element is a \code{connect_integration} object -with the following fields (all character strings unless noted otherwise): +with the following fields. (Raw API fields are character strings unless noted otherwise): \itemize{ \item \code{id}: The internal identifier of this OAuth integration. \item \code{guid}: The GUID of this OAuth integration. @@ -66,8 +66,11 @@ snowflake_integrations <- purrr::keep(content_integrations, ~ .x$template == "sn \code{\link[=get_integration]{get_integration()}}, \code{\link[=set_integrations]{set_integrations()}}, \code{\link[=get_associations]{get_associations()}} Other oauth integration functions: +\code{\link{create_integration}()}, +\code{\link{delete_integration}()}, \code{\link{get_associations}()}, \code{\link{get_integration}()}, -\code{\link{set_integrations}()} +\code{\link{set_integrations}()}, +\code{\link{update_integration}()} } \concept{oauth integration functions} diff --git a/man/set_integrations.Rd b/man/set_integrations.Rd index e8c6b6d2..615f8ebd 100644 --- a/man/set_integrations.Rd +++ b/man/set_integrations.Rd @@ -48,9 +48,12 @@ set_integrations(content, NULL) \code{\link[=get_integrations]{get_integrations()}}, \code{\link[=get_integration]{get_integration()}}, \code{\link[=get_associations]{get_associations()}}, \code{\link[=content_item]{content_item()}} Other oauth integration functions: +\code{\link{create_integration}()}, +\code{\link{delete_integration}()}, \code{\link{get_associations}()}, \code{\link{get_integration}()}, -\code{\link{get_integrations}()} +\code{\link{get_integrations}()}, +\code{\link{update_integration}()} Other content functions: \code{\link{content_delete}()}, diff --git a/man/update_integration.Rd b/man/update_integration.Rd new file mode 100644 index 00000000..a4ca7aaa --- /dev/null +++ b/man/update_integration.Rd @@ -0,0 +1,79 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/integrations.R +\name{update_integration} +\alias{update_integration} +\title{Update an OAuth integration} +\usage{ +update_integration( + integration, + name = NULL, + description = NULL, + template = NULL, + config = NULL +) +} +\arguments{ +\item{integration}{A \code{connect_integration} object (as returned by \code{\link[=get_integrations]{get_integrations()}}, +\code{\link[=get_integration]{get_integration()}}, or \code{\link[=create_integration]{create_integration()}}).} + +\item{name}{A new name for the integration.} + +\item{description}{A new description for the integration.} + +\item{template}{The template to use (generally not changed after creation).} + +\item{config}{A list with updated OAuth integration configuration. If \code{NULL} +(default), the configuration remains unchanged. You can update individual +configuration fields without affecting others.} +} +\value{ +A \code{connect_integration} object representing the updated OAuth +integration. See \code{\link[=get_integration]{get_integration()}} for details on the returned object. +} +\description{ +Updates an existing OAuth integration. All fields except \code{integration} are optional, +and are unchanged if not provided. + +You must have administrator privileges to perform this action. + +See the Posit Connect documentation on +\href{https://docs.posit.co/connect/admin/integrations/oauth-integrations/}{OAuth integrations} for +more information. +} +\examples{ +\dontrun{ +client <- connect() + +# Get an existing integration +integration <- get_integration(client, "your-integration-guid") + +# Update the integration's name and description +updated_integration <- update_integration( + integration, + name = "Updated GitHub Integration", + description = "A more descriptive description." +) + +# Update only the client secret in the configuration +updated_integration <- update_integration( + integration, + config = list( + client_secret = "your-new-client-secret" + ) +) +} + +} +\seealso{ +\code{\link[=get_integrations]{get_integrations()}}, \code{\link[=get_integration]{get_integration()}}, \code{\link[=create_integration]{create_integration()}}, +\code{\link[=delete_integration]{delete_integration()}} + +Other oauth integration functions: +\code{\link{create_integration}()}, +\code{\link{delete_integration}()}, +\code{\link{get_associations}()}, +\code{\link{get_integration}()}, +\code{\link{get_integrations}()}, +\code{\link{set_integrations}()} +} +\concept{oauth integration functions} diff --git a/tests/testthat/2025.07.0/__api__/v1/oauth/integrations-7ae937-POST.R b/tests/testthat/2025.07.0/__api__/v1/oauth/integrations-7ae937-POST.R new file mode 100644 index 00000000..fdeb688b --- /dev/null +++ b/tests/testthat/2025.07.0/__api__/v1/oauth/integrations-7ae937-POST.R @@ -0,0 +1,14 @@ +structure( + list( + url = "__api__/v1/oauth/integrations", + status_code = 400L, + headers = structure( + list(`content-type` = "application/json"), + class = c("insensitive", "list") + ), + content = charToRaw( + "{\"code\":228,\"error\":\"The config key max_role must be one of (Viewer, Publisher, Admin)\",\"payload\":null}" + ) + ), + class = "response" +) diff --git a/tests/testthat/2025.07.0/__api__/v1/oauth/integrations-aef9b2-POST.json b/tests/testthat/2025.07.0/__api__/v1/oauth/integrations-aef9b2-POST.json new file mode 100644 index 00000000..81565cbb --- /dev/null +++ b/tests/testthat/2025.07.0/__api__/v1/oauth/integrations-aef9b2-POST.json @@ -0,0 +1,13 @@ +{ + "id": "77", + "guid": "60586f1c", + "created_time": "2025-08-06T23:06:42Z", + "updated_time": "2025-08-06T23:06:42Z", + "name": "Connect API Integration", + "description": "Authenticate against the Connect API, but only as a publisher", + "template": "connect", + "auth_type": "Visitor API Key", + "config": { + "max_role": "Publisher" + } +} diff --git a/tests/testthat/2025.07.0/__api__/v1/oauth/integrations/60586f1c-DELETE.204 b/tests/testthat/2025.07.0/__api__/v1/oauth/integrations/60586f1c-DELETE.204 new file mode 100644 index 00000000..e69de29b diff --git a/tests/testthat/2025.07.0/__api__/v1/oauth/integrations/60586f1c-b90ada-PATCH.R b/tests/testthat/2025.07.0/__api__/v1/oauth/integrations/60586f1c-b90ada-PATCH.R new file mode 100644 index 00000000..277d09a7 --- /dev/null +++ b/tests/testthat/2025.07.0/__api__/v1/oauth/integrations/60586f1c-b90ada-PATCH.R @@ -0,0 +1,14 @@ +structure( + list( + url = "__api__/v1/oauth/integrations/60586f1c", + status_code = 400L, + headers = structure( + list(`content-type` = "application/json"), + class = c("insensitive", "list") + ), + content = charToRaw( + "{\"code\":228,\"error\":\"The config key max_role must be one of (Viewer, Publisher, Admin)\",\"payload\":null}" + ) + ), + class = "response" +) diff --git a/tests/testthat/2025.07.0/__api__/v1/oauth/integrations/60586f1c-c0720f-PATCH.json b/tests/testthat/2025.07.0/__api__/v1/oauth/integrations/60586f1c-c0720f-PATCH.json new file mode 100644 index 00000000..3e7c5783 --- /dev/null +++ b/tests/testthat/2025.07.0/__api__/v1/oauth/integrations/60586f1c-c0720f-PATCH.json @@ -0,0 +1,13 @@ +{ + "id": "77", + "guid": "60586f1c", + "created_time": "2025-08-06T23:06:42Z", + "updated_time": "2025-08-06T23:06:58Z", + "name": "Connect API Integration", + "description": "Improved description too, and now Viewer role", + "template": "connect", + "auth_type": "Visitor API Key", + "config": { + "max_role": "Viewer" + } +} diff --git a/tests/testthat/test-integrations.R b/tests/testthat/test-integrations.R index c34e626c..b82ef388 100644 --- a/tests/testthat/test-integrations.R +++ b/tests/testthat/test-integrations.R @@ -3,7 +3,6 @@ with_mock_dir("2024.12.0", { client <- Connect$new(server = "https://connect.example", api_key = "fake") integrations <- get_integrations(client) expect_s3_class(integrations, "connect_integration_list") - expect_equal(integrations[[1]]$name, "GitHub Integration") expect_equal(integrations[[2]]$updated_time, "2025-03-25T19:07:01Z") expect_equal(integrations[[1]]$config$client_id, "client_id_123") @@ -48,6 +47,7 @@ test_that("get_integrations() errs on older Connect versions", { }) test_that("as_integration correctly converts lists to integration objects", { + client <- MockConnect$new("2024.11.1") valid_integration <- list( id = "123", guid = "abc-123", @@ -60,9 +60,10 @@ test_that("as_integration correctly converts lists to integration objects", { config = list(client_id = "client_id") ) - result <- as_integration(valid_integration) + result <- as_integration(valid_integration, client) expect_s3_class(result, "connect_integration") expect_identical(result$guid, valid_integration$guid) + expect_identical(attr(result, "client"), client) }) test_that("as_integration.default errors on non-list input", { @@ -92,7 +93,7 @@ test_that("print.integration produces expected output", { }) with_mock_dir("2024.12.0", { - test_that("integration creates a single integration", { + test_that("get_integration() gets a single integration", { client <- Connect$new(server = "https://connect.example", api_key = "fake") x <- get_integration(client, "f8688548") expect_s3_class(x, "connect_integration") @@ -101,6 +102,14 @@ with_mock_dir("2024.12.0", { }) }) +test_that("get_integration() errs with old Connect", { + client <- MockConnect$new("2024.11.1") + expect_error( + get_integration(client, "12345678"), + "This feature requires Posit Connect version 2024.12.0 but you are using 2024.11.1" + ) +}) + test_that("set_integrations() sends expected request", { with_mock_dir("2024.12.0", { client <- Connect$new(server = "https://connect.example", api_key = "fake") @@ -182,3 +191,97 @@ test_that("get_integrations() fails when provided the wrong class", { "Cannot get integrations for an object of class 'list'" ) }) + +with_mock_dir("2025.07.0", { + client <- Connect$new(server = "https://connect.example", api_key = "fake") + + test_that("create_integration() with bad data returns an error", { + expect_error( + create_integration( + client, + name = "Connect API Integration", + description = "Authenticate against the Connect API, but only as a publisher", + template = "connect", + config = list( + max_role = "Not a Role" + ) + ), + "The config key max_role must be one of \\(Viewer, Publisher, Admin\\)" + ) + }) + + test_that("create_integration() with good data creates an integration", { + created <- create_integration( + client, + name = "Connect API Integration", + description = "Authenticate against the Connect API, but only as a publisher", + template = "connect", + config = list( + max_role = "Publisher" + ) + ) + expect_s3_class(created, "connect_integration") + expect_equal(created$guid, "60586f1c") + expect_equal(created$name, "Connect API Integration") + expect_equal(created$config$max_role, "Publisher") + }) + + test_that("update_integration() with bad data returns an error", { + created <- create_integration( + client, + name = "Connect API Integration", + description = "Authenticate against the Connect API, but only as a publisher", + template = "connect", + config = list( + max_role = "Publisher" + ) + ) + + expect_error( + update_integration( + created, + config = list( + max_role = "Not a Role" + ) + ), + "The config key max_role must be one of \\(Viewer, Publisher, Admin\\)" + ) + }) + + test_that("update_integration() with good data returns an integration object", { + created <- create_integration( + client, + name = "Connect API Integration", + description = "Authenticate against the Connect API, but only as a publisher", + template = "connect", + config = list( + max_role = "Publisher" + ) + ) + updated <- update_integration( + created, + description = "Improved description too, and now Viewer role", + config = list(max_role = "Viewer") + ) + expect_s3_class(created, "connect_integration") + expect_equal(updated$guid, "60586f1c") + expect_equal( + updated$description, + "Improved description too, and now Viewer role" + ) + expect_equal(updated$config$max_role, "Viewer") + }) + + test_that("delete_integration() returns NULL on success", { + created <- create_integration( + client, + name = "Connect API Integration", + description = "Authenticate against the Connect API, but only as a publisher", + template = "connect", + config = list( + max_role = "Publisher" + ) + ) + expect_null(delete_integration(created)) + }) +})