diff --git a/.gitignore b/.gitignore index f99d6a3..2e7e4ca 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ rsconnect/ .Rproj.user docs inst/doc +.claude/settings.local.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dcf3593..377f512 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: no-debug-statement - id: deps-in-desc - repo: https://github.com/posit-dev/air-pre-commit - rev: 0.8.2 + rev: 0.9.0 hooks: - id: air-format - repo: https://github.com/crate-ci/typos diff --git a/DESCRIPTION b/DESCRIPTION index 6d02f7b..554486b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -9,12 +9,13 @@ Authors@R: c( person("Alex's Lemonade Stand Foundation", role = c("cph", "fnd"), comment = c(ROR = "038ja4880")) ) -Description: An R package to interact with the Single-Cell Pediatric Cancer Atlas Portal API. +Description: An R package to interact with the Single-cell Pediatric Cancer Atlas Portal API. License: BSD_3_clause + file LICENSE URL: https://alexslemonade.github.io/ScPCAr, https://github.com/AlexsLemonade/ScPCAr BugReports: https://github.com/AlexsLemonade/ScPCAr/issues Encoding: UTF-8 Roxygen: list(markdown = TRUE) +Depends: R (>= 4.1.0) Imports: curl, dplyr, @@ -33,6 +34,7 @@ Suggests: httptest2, jsonlite, knitr, + rlang, rmarkdown, scater, SingleCellExperiment, diff --git a/R/auth.R b/R/auth.R index cba00ee..1fd0c17 100644 --- a/R/auth.R +++ b/R/auth.R @@ -6,10 +6,16 @@ #' To view the terms of use before agreeing to them, use [view_terms()], #' which opens the terms of use page in a web browser. #' +#' The token is stored in the `SCPCA_AUTH_TOKEN` environment variable, which the +#' package's authenticated functions (e.g. [download_sample()]) read automatically, +#' so you usually do not need to pass it explicitly. The token is also returned +#' invisibly, in case you wish to capture it. +#' #' @param email The user's email address #' @param agree A logical indicating whether the user agrees to the terms of service #' -#' @returns A string containing the authorization token +#' @returns The authorization token string (invisibly). The token is also stored +#' in the `SCPCA_AUTH_TOKEN` environment variable. #' #' @import httr2 #' @@ -47,7 +53,35 @@ get_auth <- function(email, agree = FALSE) { } ) - response$id + token <- response$id + Sys.setenv(SCPCA_AUTH_TOKEN = token) + invisible(token) +} + + +#' Resolve an authorization token, falling back to the environment +#' +#' Returns the supplied `auth_token` if it is a non-empty string. Otherwise it +#' falls back to the `SCPCA_AUTH_TOKEN` environment variable, which is set +#' automatically by [get_auth()]. An informative error is raised if neither +#' source yields a token. +#' +#' @param auth_token an authorization token string. Defaults to the +#' `SCPCA_AUTH_TOKEN` environment variable. +#' +#' @keywords internal +#' +#' @returns the resolved token as a length-1 character string +resolve_auth_token <- function(auth_token = Sys.getenv("SCPCA_AUTH_TOKEN")) { + if (length(auth_token) != 1 || is.null(auth_token) || !nzchar(auth_token)) { + stop( + "Authorization token must be provided via the `auth_token` argument", + " or the `SCPCA_AUTH_TOKEN` environment variable", + " (use `get_auth()` to obtain one and set the environment variable).", + call. = FALSE + ) + } + auth_token } diff --git a/R/datasets.R b/R/datasets.R index e5af8d1..598ca76 100644 --- a/R/datasets.R +++ b/R/datasets.R @@ -49,7 +49,7 @@ build_dataset_data <- function(samples = NULL, projects = NULL, include_bulk = F #' #' Accepts either a dataset UUID string or a list with an `$id` element (such as #' the return value of [create_dataset()] or [get_dataset_detail()]) and returns -#' the ID string. +#' the ID string, after checking that it is a valid UUID. #' #' @param dataset a dataset UUID string, or a list with an `$id` element #' @@ -59,13 +59,20 @@ build_dataset_data <- function(samples = NULL, projects = NULL, include_bulk = F resolve_dataset_id <- function(dataset) { if (is.list(dataset)) { stopifnot("dataset must be an id string or contain an $id element" = !is.null(dataset$id)) - return(dataset$id) + id <- dataset$id + } else { + stopifnot( + "dataset must be an id string or contain an $id element" = is.character(dataset) && + length(dataset) == 1 + ) + id <- dataset } + + # dataset IDs are UUIDs (e.g. "123e4567-e89b-12d3-a456-426614174000") stopifnot( - "dataset must be an id string or contain an $id element" = is.character(dataset) && - length(dataset) == 1 + "dataset id must be a valid UUID" = is_uuid(id) ) - dataset + id } @@ -115,15 +122,16 @@ update_dataset <- function(dataset_id, body, auth_token) { #' Creates a new user dataset without starting processing. #' The returned list includes the dataset `$id` along with its current contents and status. #' -#' @param auth_token an authorization token obtained from [get_auth()] #' @param format the desired file format: "sce" (SingleCellExperiment, default) or #' "anndata" (AnnData/H5AD). Spatial data is not a valid format option here; #' spatial samples are always returned in Space Ranger format. #' @param samples optional character vector of ScPCA sample IDs (e.g. "SCPCS000001") #' @param projects optional character vector of ScPCA project IDs (e.g. "SCPCP000001"); #' all samples from each project are included -#' @param email optional email address for download notification #' @param include_bulk logical; whether to include bulk RNA-seq files. Default is FALSE. +#' @param email optional email address for download notification +#' @param auth_token an authorization token from [get_auth()]. Defaults to the +#' `SCPCA_AUTH_TOKEN` environment variable, which [get_auth()] sets automatically. #' #' @returns the API response as a list (invisibly), including the dataset `$id` #' @@ -140,17 +148,17 @@ update_dataset <- function(dataset_id, body, auth_token) { #' ds$id #' } create_dataset <- function( - auth_token, format = "sce", samples = NULL, projects = NULL, include_bulk = FALSE, - email = NULL + email = NULL, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") ) { + auth_token <- resolve_auth_token(auth_token) stopifnot( "At least one of 'samples' or 'projects' must be provided" = !is.null(samples) || !is.null(projects), - "Authorization token must be provided" = is.character(auth_token) && nchar(auth_token) > 0, "include_bulk must be a logical value" = is.logical(include_bulk) && length(include_bulk) == 1 ) @@ -237,11 +245,12 @@ get_dataset_detail <- function(dataset, auth_token) { #' A dataset that has already started processing cannot be updated. #' #' @param dataset the dataset UUID string, or a list with an `$id` element. -#' @param auth_token an authorization token obtained from [get_auth()]. #' @param samples optional character vector of ScPCA sample IDs (e.g. "SCPCS000001"). #' @param projects optional character vector of ScPCA project IDs (e.g. "SCPCP000001"); #' all samples from each project are included. #' @param include_bulk logical; whether to include bulk RNA-seq files. Default is FALSE. +#' @param auth_token an authorization token from [get_auth()]. Defaults to the +#' `SCPCA_AUTH_TOKEN` environment variable, which [get_auth()] sets automatically. #' #' @returns the updated dataset detail as a list (invisibly) #' @@ -249,15 +258,16 @@ get_dataset_detail <- function(dataset, auth_token) { #' #' @examples #' \dontrun{ -#' replace_dataset_data(ds, auth_token = token, samples = c("SCPCS000001", "SCPCS000002")) +#' replace_dataset_data(ds, samples = c("SCPCS000001", "SCPCS000002")) #' } replace_dataset_data <- function( dataset, - auth_token, samples = NULL, projects = NULL, - include_bulk = FALSE + include_bulk = FALSE, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") ) { + auth_token <- resolve_auth_token(auth_token) stopifnot( "At least one of 'samples' or 'projects' must be provided" = !is.null(samples) || !is.null(projects), @@ -281,8 +291,9 @@ replace_dataset_data <- function( #' A dataset that has already been started cannot be modified. #' #' @param dataset the dataset UUID string, or a list with an `$id` element. -#' @param auth_token an authorization token obtained from [get_auth()]. #' @param email the email address to use for the download notification. +#' @param auth_token an authorization token from [get_auth()]. Defaults to the +#' `SCPCA_AUTH_TOKEN` environment variable, which [get_auth()] sets automatically. #' #' @returns the updated dataset detail as a list (invisibly) #' @@ -290,9 +301,10 @@ replace_dataset_data <- function( #' #' @examples #' \dontrun{ -#' set_dataset_email(ds, auth_token = token, email = "user@example.com") +#' set_dataset_email(ds, email = "user@example.com") #' } -set_dataset_email <- function(dataset, auth_token, email) { +set_dataset_email <- function(dataset, email, auth_token = Sys.getenv("SCPCA_AUTH_TOKEN")) { + auth_token <- resolve_auth_token(auth_token) stopifnot( "email must be a single character string" = is.character(email) && length(email) == 1 && @@ -411,13 +423,14 @@ remove_from_dataset_data <- function(existing, samples = NULL, projects = NULL) #' [replace_dataset_data()] instead. #' #' @param dataset the dataset UUID string, or a list with an `$id` element. -#' @param auth_token an authorization token obtained from [get_auth()]. #' @param samples optional character vector of ScPCA sample IDs to add or remove. #' @param projects optional character vector of ScPCA project IDs to add or #' remove; all samples from each project are included. #' @param include_bulk logical; for `add_dataset_samples()`, the `includes_bulk` #' value to use for projects that are newly added to the dataset. Existing #' projects keep their current value. Default is FALSE. +#' @param auth_token an authorization token from [get_auth()]. Defaults to the +#' `SCPCA_AUTH_TOKEN` environment variable, which [get_auth()] sets automatically. #' #' @returns the updated dataset detail as a list (invisibly) #' @@ -426,19 +439,20 @@ remove_from_dataset_data <- function(existing, samples = NULL, projects = NULL) #' #' @examples #' \dontrun{ -#' add_dataset_samples(ds, auth_token = token, samples = "SCPCS000003") -#' add_dataset_samples(ds, auth_token = token, projects = "SCPCP000002") +#' add_dataset_samples(ds, samples = "SCPCS000003") +#' add_dataset_samples(ds, projects = "SCPCP000002") #' -#' remove_dataset_samples(ds, auth_token = token, samples = "SCPCS000003") -#' remove_dataset_samples(ds, auth_token = token, projects = "SCPCP000002") +#' remove_dataset_samples(ds, samples = "SCPCS000003") +#' remove_dataset_samples(ds, projects = "SCPCP000002") #' } add_dataset_samples <- function( dataset, - auth_token, samples = NULL, projects = NULL, - include_bulk = FALSE + include_bulk = FALSE, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") ) { + auth_token <- resolve_auth_token(auth_token) stopifnot( "At least one of 'samples' or 'projects' must be provided" = !is.null(samples) || !is.null(projects), @@ -461,7 +475,13 @@ add_dataset_samples <- function( #' @rdname modify_dataset_samples #' @export -remove_dataset_samples <- function(dataset, auth_token, samples = NULL, projects = NULL) { +remove_dataset_samples <- function( + dataset, + samples = NULL, + projects = NULL, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") +) { + auth_token <- resolve_auth_token(auth_token) stopifnot( "At least one of 'samples' or 'projects' must be provided" = !is.null(samples) || !is.null(projects) diff --git a/R/downloads.R b/R/downloads.R index 9c5a7fc..e0d43fe 100644 --- a/R/downloads.R +++ b/R/downloads.R @@ -1,3 +1,27 @@ +#' Warn about a misplaced auth token in the `destination` argument +#' +#' `auth_token` was previously the second positional argument. If a caller +#' passes a token positionally it would silently land in `destination`. +#' Auth tokens are UUIDs, so a UUID-shaped `destination` almost certainly indicates +#' this mistake; warn so the caller can spot it. +#' (the download itself will fail if the token is misplaced). +#' +#' @param destination the `destination` argument to validate +#' +#' @noRd +check_destination_is_auth <- function(destination) { + if (is_uuid(destination)) { + warning( + "`destination` looks like an authorization token (a UUID), not a directory path.", + " `auth_token` is no longer the second positional argument: pass your token with `auth_token = ...`", + " or set the SCPCA_AUTH_TOKEN environment variable (see `get_auth()`).", + call. = FALSE + ) + } + invisible(destination) +} + + #' Download a sample's data files from the ScPCA Portal #' #' This function downloads the data files for a specified sample from the ScPCA Portal. @@ -19,7 +43,6 @@ #' #' #' @param sample_id The ScPCA sample ID (e.g. "SCPCS000001") -#' @param auth_token An authorization token obtained from [get_auth()] #' @param destination The path to the directory where the unzipped file directory should be saved. #' Default is "scpca_data". #' @param format The desired file format, either "sce" (SingleCellExperiment), @@ -33,6 +56,8 @@ #' If FALSE, existing files will be returned. #' Default is FALSE. #' @param quiet Whether to suppress download progress messages. Default is FALSE. +#' @param auth_token An authorization token from [get_auth()]. Defaults to the +#' `SCPCA_AUTH_TOKEN` environment variable, which [get_auth()] sets automatically. #' #' @import httr2 #' @@ -41,25 +66,26 @@ #' @export #' @examples #' \dontrun{ -#' # Get a token first -#' auth_token <- get_auth("your.email@example.com", agree = TRUE) +#' # get_auth() stores the token in SCPCA_AUTH_TOKEN, so downloads pick it up automatically +#' get_auth("your.email@example.com", agree = TRUE) #' # Then ask for a sample download -#' download_sample("SCPCS000001", auth_token, destination = "scpca_data", format = "sce") +#' download_sample("SCPCS000001", destination = "scpca_data", format = "sce") #' #' # Downloading in AnnData format -#' download_sample("SCPCS000001", auth_token, destination = "scpca_data", format = "anndata") +#' download_sample("SCPCS000001", destination = "scpca_data", format = "anndata") #' } download_sample <- function( sample_id, - auth_token, destination = "scpca_data", format = "sce", overwrite = FALSE, redownload = FALSE, - quiet = FALSE + quiet = FALSE, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") ) { + check_destination_is_auth(destination) + auth_token <- resolve_auth_token(auth_token) stopifnot( - "Authorization token must be provided" = is.character(auth_token) && nchar(auth_token) > 0, "quiet must be a logical value" = is.logical(quiet) && length(quiet) == 1 ) @@ -112,7 +138,6 @@ download_sample <- function( #' Download a project's data files from the ScPCA Portal #' #' @param project_id The ScPCA project ID (e.g. "SCPCP000001") -#' @param auth_token An authorization token obtained from [get_auth()] #' @param destination The path to the directory where the unzipped file directory should be saved. #' Default is "scpca_data". #' @param format The desired file format, either "sce" (SingleCellExperiment), @@ -131,6 +156,8 @@ download_sample <- function( #' If FALSE, existing files will be returned. #' Default is FALSE. #' @param quiet Whether to suppress download progress messages. Default is FALSE. +#' @param auth_token An authorization token from [get_auth()]. Defaults to the +#' `SCPCA_AUTH_TOKEN` environment variable, which [get_auth()] sets automatically. #' #' @importFrom stats setNames #' @@ -139,15 +166,14 @@ download_sample <- function( #' @export #' @examples #' \dontrun{ -#' # Get a token first -#' auth_token <- get_auth("your.email@example.com", agree = TRUE) -#' # Then ask for a sample download -#' download_project("SCPCS000001", auth_token, destination = "scpca_data", format = "sce") +#' # get_auth() stores the token in SCPCA_AUTH_TOKEN, so downloads pick it up automatically +#' get_auth("your.email@example.com", agree = TRUE) +#' # Then ask for a project download +#' download_project("SCPCP000001", destination = "scpca_data", format = "sce") #' #' # Downloading merged files in AnnData format #' download_project( -#' "SCPCS000001", -#' auth_token, +#' "SCPCP000001", #' destination = "scpca_data", #' format = "anndata", #' merged = TRUE @@ -155,18 +181,19 @@ download_sample <- function( #' } download_project <- function( project_id, - auth_token, destination = "scpca_data", format = "sce", merged = FALSE, include_multiplexed = NULL, overwrite = FALSE, redownload = FALSE, - quiet = FALSE + quiet = FALSE, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") ) { + check_destination_is_auth(destination) + auth_token <- resolve_auth_token(auth_token) stopifnot( "Invalid project_id." = grepl("^SCPCP\\d{6}$", project_id), - "Authorization token must be provided" = is.character(auth_token) && nchar(auth_token) > 0, "quiet must be a logical value" = is.logical(quiet) && length(quiet) == 1, "merged must be a logical value" = is.logical(merged) && length(merged) == 1, "include_multiplexed must be NULL or a logical value" = is.null(include_multiplexed) || diff --git a/R/projects.R b/R/projects.R index 23d5335..db6f0ba 100644 --- a/R/projects.R +++ b/R/projects.R @@ -159,7 +159,8 @@ get_project_samples <- function(project_id, simplify = TRUE) { #' that is part of the project. #' #' @param project_id The ScPCA project ID (e.g. "SCPCP000001") -#' @param auth_token An authorization token obtained from [get_auth()] +#' @param auth_token An authorization token from [get_auth()]. Defaults to the +#' `SCPCA_AUTH_TOKEN` environment variable, which [get_auth()] sets automatically. #' #' @returns A data frame (tibble) of library metadata for the specified project. #' @@ -172,7 +173,8 @@ get_project_samples <- function(project_id, simplify = TRUE) { #' libraries_df <- get_project_libraries("SCPCP000001", token) #' } #' -get_project_libraries <- function(project_id, auth_token) { +get_project_libraries <- function(project_id, auth_token = Sys.getenv("SCPCA_AUTH_TOKEN")) { + auth_token <- resolve_auth_token(auth_token) download_url <- get_project_metadata_url(project_id, auth_token) file_paths <- download_and_extract_file(download_url, tempfile(), overwrite = TRUE, quiet = TRUE) diff --git a/R/utils.R b/R/utils.R index ccb2459..7b00520 100644 --- a/R/utils.R +++ b/R/utils.R @@ -9,3 +9,17 @@ is_testing <- function() { identical(Sys.getenv("TESTTHAT"), "true") } + +#' Check whether a value has UUID string format +#' +#' UUIDs look like "123e4567-889b-43d2-9999-123456789abc" and are used for both +#' dataset IDs and authorization tokens in the ScPCA API. +#' +#' @param x value to test +#' +#' @return Logical indicating whether `x` is a length-1 UUID-formatted string +#' @noRd +is_uuid <- function(x) { + uuid_pattern <- "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + is.character(x) && length(x) == 1 && grepl(uuid_pattern, x, ignore.case = TRUE) +} diff --git a/README.md b/README.md index 35aa0e7..f042fbc 100644 --- a/README.md +++ b/README.md @@ -25,17 +25,18 @@ library(ScPCAr) # First, look at the terms of use view_terms() -# Get an authentication token for use with the ScPCA Portal -auth_token <- get_auth(email = "your.email@example.com", agree = TRUE) +# Get an authentication token for use with the ScPCA Portal. +# This stores the token in the `SCPCA_AUTH_TOKEN` environment variable, +# which the download functions read automatically. +get_auth(email = "your.email@example.com", agree = TRUE) # Get the sample metadata for a project sample_metadata <- get_sample_metadata(project_id = "SCPCP000001") -# Download a data for a sample +# Download data for a sample # this function returns a vector of the downloaded file paths file_paths <- download_sample( sample_id = "SCPCS000001", - auth_token = auth_token, destination = "scpca_data", format = "sce" ) diff --git a/man/create_dataset.Rd b/man/create_dataset.Rd index ea07869..82caa7f 100644 --- a/man/create_dataset.Rd +++ b/man/create_dataset.Rd @@ -5,17 +5,15 @@ \title{Create a custom dataset on the ScPCA Portal} \usage{ create_dataset( - auth_token, format = "sce", samples = NULL, projects = NULL, include_bulk = FALSE, - email = NULL + email = NULL, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") ) } \arguments{ -\item{auth_token}{an authorization token obtained from \code{\link[=get_auth]{get_auth()}}} - \item{format}{the desired file format: "sce" (SingleCellExperiment, default) or "anndata" (AnnData/H5AD). Spatial data is not a valid format option here; spatial samples are always returned in Space Ranger format.} @@ -28,6 +26,9 @@ all samples from each project are included} \item{include_bulk}{logical; whether to include bulk RNA-seq files. Default is FALSE.} \item{email}{optional email address for download notification} + +\item{auth_token}{an authorization token from \code{\link[=get_auth]{get_auth()}}. Defaults to the +\code{SCPCA_AUTH_TOKEN} environment variable, which \code{\link[=get_auth]{get_auth()}} sets automatically.} } \value{ the API response as a list (invisibly), including the dataset \verb{$id} diff --git a/man/download_project.Rd b/man/download_project.Rd index bd2e97a..d9606ab 100644 --- a/man/download_project.Rd +++ b/man/download_project.Rd @@ -6,21 +6,19 @@ \usage{ download_project( project_id, - auth_token, destination = "scpca_data", format = "sce", merged = FALSE, include_multiplexed = NULL, overwrite = FALSE, redownload = FALSE, - quiet = FALSE + quiet = FALSE, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") ) } \arguments{ \item{project_id}{The ScPCA project ID (e.g. "SCPCP000001")} -\item{auth_token}{An authorization token obtained from \code{\link[=get_auth]{get_auth()}}} - \item{destination}{The path to the directory where the unzipped file directory should be saved. Default is "scpca_data".} @@ -45,6 +43,9 @@ If FALSE, existing files will be returned. Default is FALSE.} \item{quiet}{Whether to suppress download progress messages. Default is FALSE.} + +\item{auth_token}{An authorization token from \code{\link[=get_auth]{get_auth()}}. Defaults to the +\code{SCPCA_AUTH_TOKEN} environment variable, which \code{\link[=get_auth]{get_auth()}} sets automatically.} } \value{ a vector of file paths for the downloaded files (invisibly) @@ -54,15 +55,14 @@ Download a project's data files from the ScPCA Portal } \examples{ \dontrun{ -# Get a token first -auth_token <- get_auth("your.email@example.com", agree = TRUE) -# Then ask for a sample download -download_project("SCPCS000001", auth_token, destination = "scpca_data", format = "sce") +# get_auth() stores the token in SCPCA_AUTH_TOKEN, so downloads pick it up automatically +get_auth("your.email@example.com", agree = TRUE) +# Then ask for a project download +download_project("SCPCP000001", destination = "scpca_data", format = "sce") # Downloading merged files in AnnData format download_project( - "SCPCS000001", - auth_token, + "SCPCP000001", destination = "scpca_data", format = "anndata", merged = TRUE diff --git a/man/download_sample.Rd b/man/download_sample.Rd index 0a8c089..fc12130 100644 --- a/man/download_sample.Rd +++ b/man/download_sample.Rd @@ -6,19 +6,17 @@ \usage{ download_sample( sample_id, - auth_token, destination = "scpca_data", format = "sce", overwrite = FALSE, redownload = FALSE, - quiet = FALSE + quiet = FALSE, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") ) } \arguments{ \item{sample_id}{The ScPCA sample ID (e.g. "SCPCS000001")} -\item{auth_token}{An authorization token obtained from \code{\link[=get_auth]{get_auth()}}} - \item{destination}{The path to the directory where the unzipped file directory should be saved. Default is "scpca_data".} @@ -36,6 +34,9 @@ If FALSE, existing files will be returned. Default is FALSE.} \item{quiet}{Whether to suppress download progress messages. Default is FALSE.} + +\item{auth_token}{An authorization token from \code{\link[=get_auth]{get_auth()}}. Defaults to the +\code{SCPCA_AUTH_TOKEN} environment variable, which \code{\link[=get_auth]{get_auth()}} sets automatically.} } \value{ a vector of file paths for the downloaded files (invisibly) @@ -61,12 +62,12 @@ using the \code{\link[=get_auth]{get_auth()}} function. } \examples{ \dontrun{ -# Get a token first -auth_token <- get_auth("your.email@example.com", agree = TRUE) +# get_auth() stores the token in SCPCA_AUTH_TOKEN, so downloads pick it up automatically +get_auth("your.email@example.com", agree = TRUE) # Then ask for a sample download -download_sample("SCPCS000001", auth_token, destination = "scpca_data", format = "sce") +download_sample("SCPCS000001", destination = "scpca_data", format = "sce") # Downloading in AnnData format -download_sample("SCPCS000001", auth_token, destination = "scpca_data", format = "anndata") +download_sample("SCPCS000001", destination = "scpca_data", format = "anndata") } } diff --git a/man/get_auth.Rd b/man/get_auth.Rd index 6b3e7d2..e8cd008 100644 --- a/man/get_auth.Rd +++ b/man/get_auth.Rd @@ -15,7 +15,8 @@ view_terms() \item{agree}{A logical indicating whether the user agrees to the terms of service} } \value{ -A string containing the authorization token +The authorization token string (invisibly). The token is also stored +in the \code{SCPCA_AUTH_TOKEN} environment variable. } \description{ \code{get_auth()} allows obtaining an authorization token string from the ScPCA API, @@ -24,6 +25,11 @@ after providing an email address and agreeing to the terms of use. \details{ To view the terms of use before agreeing to them, use \code{\link[=view_terms]{view_terms()}}, which opens the terms of use page in a web browser. + +The token is stored in the \code{SCPCA_AUTH_TOKEN} environment variable, which the +package's authenticated functions (e.g. \code{\link[=download_sample]{download_sample()}}) read automatically, +so you usually do not need to pass it explicitly. The token is also returned +invisibly, in case you wish to capture it. } \examples{ \dontrun{ diff --git a/man/get_project_libraries.Rd b/man/get_project_libraries.Rd index 43fbe83..17ce423 100644 --- a/man/get_project_libraries.Rd +++ b/man/get_project_libraries.Rd @@ -4,12 +4,13 @@ \alias{get_project_libraries} \title{Get metadata for all libraries in a given project} \usage{ -get_project_libraries(project_id, auth_token) +get_project_libraries(project_id, auth_token = Sys.getenv("SCPCA_AUTH_TOKEN")) } \arguments{ \item{project_id}{The ScPCA project ID (e.g. "SCPCP000001")} -\item{auth_token}{An authorization token obtained from \code{\link[=get_auth]{get_auth()}}} +\item{auth_token}{An authorization token from \code{\link[=get_auth]{get_auth()}}. Defaults to the +\code{SCPCA_AUTH_TOKEN} environment variable, which \code{\link[=get_auth]{get_auth()}} sets automatically.} } \value{ A data frame (tibble) of library metadata for the specified project. diff --git a/man/modify_dataset_samples.Rd b/man/modify_dataset_samples.Rd index 525ad3b..61604ec 100644 --- a/man/modify_dataset_samples.Rd +++ b/man/modify_dataset_samples.Rd @@ -7,19 +7,22 @@ \usage{ add_dataset_samples( dataset, - auth_token, samples = NULL, projects = NULL, - include_bulk = FALSE + include_bulk = FALSE, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") ) -remove_dataset_samples(dataset, auth_token, samples = NULL, projects = NULL) +remove_dataset_samples( + dataset, + samples = NULL, + projects = NULL, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") +) } \arguments{ \item{dataset}{the dataset UUID string, or a list with an \verb{$id} element.} -\item{auth_token}{an authorization token obtained from \code{\link[=get_auth]{get_auth()}}.} - \item{samples}{optional character vector of ScPCA sample IDs to add or remove.} \item{projects}{optional character vector of ScPCA project IDs to add or @@ -28,6 +31,9 @@ remove; all samples from each project are included.} \item{include_bulk}{logical; for \code{add_dataset_samples()}, the \code{includes_bulk} value to use for projects that are newly added to the dataset. Existing projects keep their current value. Default is FALSE.} + +\item{auth_token}{an authorization token from \code{\link[=get_auth]{get_auth()}}. Defaults to the +\code{SCPCA_AUTH_TOKEN} environment variable, which \code{\link[=get_auth]{get_auth()}} sets automatically.} } \value{ the updated dataset detail as a list (invisibly) @@ -53,10 +59,10 @@ sample (though they may be removed wholesale via \code{projects}); use } \examples{ \dontrun{ -add_dataset_samples(ds, auth_token = token, samples = "SCPCS000003") -add_dataset_samples(ds, auth_token = token, projects = "SCPCP000002") +add_dataset_samples(ds, samples = "SCPCS000003") +add_dataset_samples(ds, projects = "SCPCP000002") -remove_dataset_samples(ds, auth_token = token, samples = "SCPCS000003") -remove_dataset_samples(ds, auth_token = token, projects = "SCPCP000002") +remove_dataset_samples(ds, samples = "SCPCS000003") +remove_dataset_samples(ds, projects = "SCPCP000002") } } diff --git a/man/replace_dataset_data.Rd b/man/replace_dataset_data.Rd index 1a1d314..4f7efde 100644 --- a/man/replace_dataset_data.Rd +++ b/man/replace_dataset_data.Rd @@ -6,23 +6,24 @@ \usage{ replace_dataset_data( dataset, - auth_token, samples = NULL, projects = NULL, - include_bulk = FALSE + include_bulk = FALSE, + auth_token = Sys.getenv("SCPCA_AUTH_TOKEN") ) } \arguments{ \item{dataset}{the dataset UUID string, or a list with an \verb{$id} element.} -\item{auth_token}{an authorization token obtained from \code{\link[=get_auth]{get_auth()}}.} - \item{samples}{optional character vector of ScPCA sample IDs (e.g. "SCPCS000001").} \item{projects}{optional character vector of ScPCA project IDs (e.g. "SCPCP000001"); all samples from each project are included.} \item{include_bulk}{logical; whether to include bulk RNA-seq files. Default is FALSE.} + +\item{auth_token}{an authorization token from \code{\link[=get_auth]{get_auth()}}. Defaults to the +\code{SCPCA_AUTH_TOKEN} environment variable, which \code{\link[=get_auth]{get_auth()}} sets automatically.} } \value{ the updated dataset detail as a list (invisibly) @@ -39,6 +40,6 @@ A dataset that has already been started cannot be updated. } \examples{ \dontrun{ -replace_dataset_data(ds, auth_token = token, samples = c("SCPCS000001", "SCPCS000002")) +replace_dataset_data(ds, samples = c("SCPCS000001", "SCPCS000002")) } } diff --git a/man/resolve_auth_token.Rd b/man/resolve_auth_token.Rd new file mode 100644 index 0000000..8fec954 --- /dev/null +++ b/man/resolve_auth_token.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/auth.R +\name{resolve_auth_token} +\alias{resolve_auth_token} +\title{Resolve an authorization token, falling back to the environment} +\usage{ +resolve_auth_token(auth_token = Sys.getenv("SCPCA_AUTH_TOKEN")) +} +\arguments{ +\item{auth_token}{an authorization token string. Defaults to the +\code{SCPCA_AUTH_TOKEN} environment variable.} +} +\value{ +the resolved token as a length-1 character string +} +\description{ +Returns the supplied \code{auth_token} if it is a non-empty string. Otherwise it +falls back to the \code{SCPCA_AUTH_TOKEN} environment variable, which is set +automatically by \code{\link[=get_auth]{get_auth()}}. An informative error is raised if neither +source yields a token. +} +\keyword{internal} diff --git a/man/resolve_dataset_id.Rd b/man/resolve_dataset_id.Rd index 00cdb3d..ee7e54c 100644 --- a/man/resolve_dataset_id.Rd +++ b/man/resolve_dataset_id.Rd @@ -15,6 +15,6 @@ the dataset ID as a length-1 character string \description{ Accepts either a dataset UUID string or a list with an \verb{$id} element (such as the return value of \code{\link[=create_dataset]{create_dataset()}} or \code{\link[=get_dataset_detail]{get_dataset_detail()}}) and returns -the ID string. +the ID string, after checking that it is a valid UUID. } \keyword{internal} diff --git a/man/set_dataset_email.Rd b/man/set_dataset_email.Rd index 0f00863..cc28f59 100644 --- a/man/set_dataset_email.Rd +++ b/man/set_dataset_email.Rd @@ -4,14 +4,15 @@ \alias{set_dataset_email} \title{Set the notification email for a custom dataset} \usage{ -set_dataset_email(dataset, auth_token, email) +set_dataset_email(dataset, email, auth_token = Sys.getenv("SCPCA_AUTH_TOKEN")) } \arguments{ \item{dataset}{the dataset UUID string, or a list with an \verb{$id} element.} -\item{auth_token}{an authorization token obtained from \code{\link[=get_auth]{get_auth()}}.} - \item{email}{the email address to use for the download notification.} + +\item{auth_token}{an authorization token from \code{\link[=get_auth]{get_auth()}}. Defaults to the +\code{SCPCA_AUTH_TOKEN} environment variable, which \code{\link[=get_auth]{get_auth()}} sets automatically.} } \value{ the updated dataset detail as a list (invisibly) @@ -26,6 +27,6 @@ A dataset that has already been started cannot be modified. } \examples{ \dontrun{ -set_dataset_email(ds, auth_token = token, email = "user@example.com") +set_dataset_email(ds, email = "user@example.com") } } diff --git a/tests/testthat/ds_status/api.scpca.alexslemonade.org/v1/datasets/ds-uuid.json b/tests/testthat/ds_status/api.scpca.alexslemonade.org/v1/datasets/ds-uuid.json deleted file mode 100644 index 0ef22ec..0000000 --- a/tests/testthat/ds_status/api.scpca.alexslemonade.org/v1/datasets/ds-uuid.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "ds-uuid", - "format": "SINGLE_CELL_EXPERIMENT", - "data": { - "SCPCP000001": { - "SINGLE_CELL": ["SCPCS000001", "SCPCS000002"], - "SPATIAL": [], - "includes_bulk": false - } - }, - "email": null, - "start": false, - "is_started": false, - "is_processing": false, - "is_succeeded": false, - "is_failed": false, - "total_sample_count": 2, - "computed_file": null -} diff --git a/tests/testthat/ds_status_404/api.scpca.alexslemonade.org/v1/datasets/no-uuid.R b/tests/testthat/ds_status_404/api.scpca.alexslemonade.org/v1/datasets/no-uuid.R deleted file mode 100644 index 4592bb9..0000000 --- a/tests/testthat/ds_status_404/api.scpca.alexslemonade.org/v1/datasets/no-uuid.R +++ /dev/null @@ -1,33 +0,0 @@ -structure( - list( - method = "GET", - url = "https://api.scpca.alexslemonade.org/v1/datasets/no-uuid", - status_code = 404L, - headers = structure( - list( - Server = "nginx/1.18.0 (Ubuntu)", - Date = "Tue, 09 Sep 2025 18:05:24 GMT", - `Content-Type` = "application/json", - `Content-Length` = "23", - Connection = "keep-alive", - Vary = "Accept, Cookie, Origin", - Allow = "GET, HEAD, OPTIONS", - `X-Frame-Options` = "DENY", - `X-Content-Type-Options` = "nosniff", - `Referrer-Policy` = "same-origin" - ), - class = "httr2_headers" - ), - body = charToRaw("{\"detail\":\"Not found.\"}"), - timing = c( - redirect = 0, - namelookup = 0, - connect = 0, - pretransfer = 7.2e-05, - starttransfer = 0.040057, - total = 0.040088 - ), - cache = new.env(parent = emptyenv()) - ), - class = "httr2_response" -) diff --git a/tests/testthat/setup.R b/tests/testthat/setup.R index 1b56601..201fdf4 100644 --- a/tests/testthat/setup.R +++ b/tests/testthat/setup.R @@ -1,5 +1,9 @@ library(httptest2) +# Ensure tests never inherit a real auth token from the developer's environment. +# Blank SCPCA_AUTH_TOKEN for the duration of the test run, restoring it afterward. +withr::local_envvar(SCPCA_AUTH_TOKEN = "", .local_envir = testthat::teardown_env()) + # Helper to create a minimal httr2 JSON response for use in local_mocked_bindings json_response <- function(body_list, status = 200L) { structure( @@ -12,8 +16,14 @@ json_response <- function(body_list, status = 200L) { class = "httr2_headers" ), body = charToRaw(jsonlite::toJSON(body_list, auto_unbox = TRUE)), - timing = c(redirect = 0, namelookup = 0, connect = 0, - pretransfer = 0, starttransfer = 0, total = 0), + timing = c( + redirect = 0, + namelookup = 0, + connect = 0, + pretransfer = 0, + starttransfer = 0, + total = 0 + ), cache = new.env(parent = emptyenv()) ), class = "httr2_response" diff --git a/tests/testthat/test-auth.R b/tests/testthat/test-auth.R index 30455b0..1d5e323 100644 --- a/tests/testthat/test-auth.R +++ b/tests/testthat/test-auth.R @@ -33,10 +33,11 @@ test_that("get_auth validates input parameters", { }) test_that("get_auth accepts valid email formats", { + withr::local_envvar(SCPCA_AUTH_TOKEN = "") # Mock httr2 functions to avoid actual API calls # not using httptest2::with_mock_* because we just want to test validation here local_mocked_bindings( - req_perform = \(req) httr2::response_json() # nolint + req_perform = \(req) json_response(list(id = "mock-token-123")) ) # These should all pass validation expect_no_error(get_auth("user@example.com", TRUE)) @@ -45,6 +46,7 @@ test_that("get_auth accepts valid email formats", { }) test_that("get_auth gets a token", { + withr::local_envvar(SCPCA_AUTH_TOKEN = "") with_mock_dir("auth", { result <- get_auth("scpca@alexslemonade.org", TRUE) @@ -52,3 +54,30 @@ test_that("get_auth gets a token", { expect_equal(result, "mock-token-123") }) }) + +test_that("get_auth stores the token in the SCPCA_AUTH_TOKEN environment variable", { + withr::local_envvar(SCPCA_AUTH_TOKEN = "") + with_mock_dir("auth", { + get_auth("scpca@alexslemonade.org", TRUE) + + # the token should have been written to the environment + expect_equal(Sys.getenv("SCPCA_AUTH_TOKEN"), "mock-token-123") + }) +}) + +test_that("resolve_auth_token falls back to the environment and validates", { + # an explicit token is returned unchanged + expect_equal(resolve_auth_token("explicit-token"), "explicit-token") + + # falls back to the environment variable when no argument is supplied + withr::local_envvar(SCPCA_AUTH_TOKEN = "env-token") + expect_equal(resolve_auth_token(), "env-token") + + # an explicit token still overrides the environment value + expect_equal(resolve_auth_token("explicit-token"), "explicit-token") + + # errors when neither an argument nor the environment provides a token + withr::local_envvar(SCPCA_AUTH_TOKEN = "") + expect_error(resolve_auth_token(), "Authorization token must be provided") + expect_error(resolve_auth_token(""), "Authorization token must be provided") +}) diff --git a/tests/testthat/test-datasets.R b/tests/testthat/test-datasets.R index 255a5c6..3b87f53 100644 --- a/tests/testthat/test-datasets.R +++ b/tests/testthat/test-datasets.R @@ -1,3 +1,6 @@ +DATASET_ID <- "00000000-0000-0000-0000-000000000001" +DATASET_ID_404 <- "00000000-0000-0000-0000-000000000404" + test_that("get_ccdl_datasets returns a list of dataset objects", { with_mock_dir("ccdl_datasets", { result <- get_ccdl_datasets() @@ -319,69 +322,119 @@ test_that("create_dataset returns response invisibly and messages with dataset i expect_equal(result$id, "new-dataset-uuid") }) +test_that("create_dataset reads auth_token from the SCPCA_AUTH_TOKEN environment variable", { + withr::local_envvar(SCPCA_AUTH_TOKEN = "env-token") + local_mocked_bindings( + build_dataset_data = \(...) list(), + req_perform = \(req, ...) { + json_response(list( + id = "new-dataset-uuid", + api_key = httr2::req_get_headers(req, "reveal")$`api-key` + )) + } + ) + + # called without auth_token; the token should come from the environment + result <- suppressMessages(create_dataset(samples = "SCPCS000001", format = "sce")) + expect_equal(result$api_key, "env-token") +}) + # get_dataset_detail tests test_that("get_dataset_detail returns dataset with data and status fields", { - with_mock_dir("ds_status", { - result <- get_dataset_detail("ds-uuid", auth_token = "test-token") + local_mocked_bindings( + req_perform = \(req, ...) { + json_response(list( + id = DATASET_ID, + format = "SINGLE_CELL_EXPERIMENT", + data = list( + SCPCP000001 = list( + SINGLE_CELL = list("SCPCS000001", "SCPCS000002"), + SPATIAL = list(), + includes_bulk = FALSE + ) + ), + is_started = FALSE, + is_succeeded = FALSE, + total_sample_count = 2, + computed_file = NULL + )) + } + ) - expect_type(result, "list") - expect_equal(result$id, "ds-uuid") - expect_equal(result$format, "SINGLE_CELL_EXPERIMENT") - expect_false(result$is_started) - expect_false(result$is_succeeded) - }) + result <- get_dataset_detail(DATASET_ID, auth_token = "test-token") + + expect_type(result, "list") + expect_equal(result$id, DATASET_ID) + expect_equal(result$format, "SINGLE_CELL_EXPERIMENT") + expect_false(result$is_started) + expect_false(result$is_succeeded) }) test_that("get_dataset_detail returns data field with project and sample structure", { - with_mock_dir("ds_status", { - result <- get_dataset_detail("ds-uuid", auth_token = "test-token") - - expect_type(result$data, "list") - expect_true("SCPCP000001" %in% names(result$data)) - expect_contains( - result$data$SCPCP000001$SINGLE_CELL, - c("SCPCS000001", "SCPCS000002") - ) - }) + local_mocked_bindings( + req_perform = \(req, ...) { + json_response(list( + id = DATASET_ID, + format = "SINGLE_CELL_EXPERIMENT", + data = list( + SCPCP000001 = list( + SINGLE_CELL = list("SCPCS000001", "SCPCS000002"), + SPATIAL = list(), + includes_bulk = FALSE + ) + ) + )) + } + ) + + result <- get_dataset_detail(DATASET_ID, auth_token = "test-token") + + expect_type(result$data, "list") + expect_true("SCPCP000001" %in% names(result$data)) + expect_contains( + result$data$SCPCP000001$SINGLE_CELL, + c("SCPCS000001", "SCPCS000002") + ) }) test_that("get_dataset_detail includes api-key header when auth_token is provided", { local_mocked_bindings( req_perform = \(req, ...) { json_response(list( - id = "uuid", + id = DATASET_ID, data = list(), api_key = httr2::req_get_headers(req, "reveal")$`api-key` )) } ) - result <- get_dataset_detail("uuid", auth_token = "my-token") + result <- get_dataset_detail(DATASET_ID, auth_token = "my-token") expect_equal(result$api_key, "my-token") }) test_that("get_dataset_detail handles 404 errors correctly", { local_mocked_bindings( - check_api = function() TRUE + check_api = function() TRUE, + req_perform = \(req, ...) rlang::abort(class = "httr2_http_404", message = "Not Found") ) - with_mock_dir("ds_status_404", { - expect_error( - get_dataset_detail("no-uuid", auth_token = "test-token"), - "Dataset `no-uuid` not found." - ) - }) + expect_error( + get_dataset_detail(DATASET_ID_404, auth_token = "test-token"), + "not found" + ) }) test_that("get_dataset_detail accepts a list with $id in place of a string", { local_mocked_bindings( - req_perform = \(req, ...) json_response(list(id = "ds-uuid", data = list())) + req_perform = \(req, ...) { + json_response(list(id = DATASET_ID, data = list())) + } ) - dataset_list <- list(id = "ds-uuid", data = list()) + dataset_list <- list(id = DATASET_ID, data = list()) result <- get_dataset_detail(dataset_list, auth_token = "test-token") - expect_equal(result$id, "ds-uuid") + expect_equal(result$id, DATASET_ID) }) test_that("get_dataset_detail errors when list has no $id element", { @@ -413,8 +466,14 @@ test_that("get_ccdl_dataset_detail returns dataset fields including download_url # resolve_dataset_id tests test_that("resolve_dataset_id accepts a string and a list with $id", { - expect_equal(resolve_dataset_id("ds-uuid"), "ds-uuid") - expect_equal(resolve_dataset_id(list(id = "ds-uuid", data = list())), "ds-uuid") + expect_equal( + resolve_dataset_id(DATASET_ID), + DATASET_ID + ) + expect_equal( + resolve_dataset_id(list(id = DATASET_ID, data = list())), + DATASET_ID + ) }) test_that("resolve_dataset_id errors on invalid input", { @@ -422,11 +481,19 @@ test_that("resolve_dataset_id errors on invalid input", { expect_error(resolve_dataset_id(123), "id string or contain an \\$id element") }) +test_that("resolve_dataset_id errors when id is not a valid UUID", { + expect_error(resolve_dataset_id("not-a-uuid"), "valid UUID") + expect_error( + resolve_dataset_id(list(id = "not-a-uuid", data = list())), + "valid UUID" + ) +}) + # replace_dataset_data tests test_that("replace_dataset_data errors when neither samples nor projects are provided", { expect_error( - replace_dataset_data("ds-uuid", auth_token = "token"), + replace_dataset_data(DATASET_ID, auth_token = "token"), "At least one of 'samples' or 'projects' must be provided" ) }) @@ -450,13 +517,13 @@ test_that("replace_dataset_data PUTs a rebuilt data field without a format", { ) result <- replace_dataset_data( - "ds-uuid", + DATASET_ID, auth_token = "token", samples = "SCPCS000001" ) expect_equal(captured_req$method, "PUT") - expect_match(captured_req$url, "datasets/ds-uuid") + expect_match(captured_req$url, "datasets/00000000-0000-0000-0000-000000000001") expect_null(result$format) expect_true("SCPCP000001" %in% names(result$data)) }) @@ -472,15 +539,23 @@ test_that("set_dataset_email PUTs a new email", { } ) - result <- set_dataset_email("ds-uuid", auth_token = "token", email = "user@example.com") + result <- set_dataset_email( + DATASET_ID, + auth_token = "token", + email = "user@example.com" + ) expect_equal(captured_req$method, "PUT") - expect_match(captured_req$url, "datasets/ds-uuid") + expect_match(captured_req$url, "datasets/00000000-0000-0000-0000-000000000001") expect_equal(result$email, "user@example.com") }) test_that("set_dataset_email errors when email is not a single string", { expect_error( - set_dataset_email("ds-uuid", auth_token = "token", email = c("a@b.com", "c@d.com")), + set_dataset_email( + DATASET_ID, + auth_token = "token", + email = c("a@b.com", "c@d.com") + ), "email must be a single character string" ) }) @@ -572,7 +647,7 @@ test_that("add_dataset_samples merges new samples into existing data and PUTs", local_mocked_bindings( get_dataset_detail = \(dataset, auth_token) { list( - id = "ds-uuid", + id = DATASET_ID, data = list( SCPCP000001 = list( SINGLE_CELL = list("SCPCS000001"), @@ -597,7 +672,11 @@ test_that("add_dataset_samples merges new samples into existing data and PUTs", } ) - result <- add_dataset_samples("ds-uuid", auth_token = "token", samples = "SCPCS000002") + result <- add_dataset_samples( + DATASET_ID, + auth_token = "token", + samples = "SCPCS000002" + ) expect_equal(captured_req$method, "PUT") expect_setequal( as.character(result$data$SCPCP000001$SINGLE_CELL), @@ -610,7 +689,7 @@ test_that("remove_dataset_samples removes a project and PUTs", { local_mocked_bindings( get_dataset_detail = \(dataset, auth_token) { list( - id = "ds-uuid", + id = DATASET_ID, data = list( SCPCP000001 = list( SINGLE_CELL = list("SCPCS000001"), @@ -631,7 +710,11 @@ test_that("remove_dataset_samples removes a project and PUTs", { } ) - result <- remove_dataset_samples("ds-uuid", auth_token = "token", projects = "SCPCP000002") + result <- remove_dataset_samples( + DATASET_ID, + auth_token = "token", + projects = "SCPCP000002" + ) expect_equal(captured_req$method, "PUT") expect_equal(names(result$data), "SCPCP000001") }) diff --git a/tests/testthat/test-downloads.R b/tests/testthat/test-downloads.R index d825c87..75b1fa3 100644 --- a/tests/testthat/test-downloads.R +++ b/tests/testthat/test-downloads.R @@ -63,66 +63,80 @@ test_that("parse_download_file extracts filename correctly", { test_that("download_sample validates input parameters", { # Test missing auth token expect_error( - download_sample("SCPCS000001", ""), + download_sample("SCPCS000001", auth_token = ""), "Authorization token must be provided" ) expect_error( - download_sample("SCPCS000001", character(0)), + download_sample("SCPCS000001", auth_token = character(0)), "Authorization token must be provided" ) # Test invalid quiet parameter expect_error( - download_sample("SCPCS000001", "valid-token", quiet = "not-logical"), + download_sample("SCPCS000001", auth_token = "valid-token", quiet = "not-logical"), "quiet must be a logical value" ) expect_error( - download_sample("SCPCS000001", "valid-token", quiet = c(TRUE, FALSE)), + download_sample("SCPCS000001", auth_token = "valid-token", quiet = c(TRUE, FALSE)), "quiet must be a logical value" ) }) +test_that("check_destination_is_auth warns when destination looks like an auth token (UUID)", { + # auth_token is the last argument, so a positionally-passed token lands in destination + expect_warning( + check_destination_is_auth("123e4567-e89b-12d3-a456-426614174000"), + "looks like an authorization token" + ) + # a normal directory path does not warn + expect_no_warning(check_destination_is_auth("scpca_data")) +}) + test_that("download_project validates input parameters", { # Test invalid project_id format expect_error( - download_project("invalid", "valid-token"), + download_project("invalid", auth_token = "valid-token"), "Invalid project_id" ) expect_error( - download_project("SCPCS000001", "valid-token"), + download_project("SCPCS000001", auth_token = "valid-token"), "Invalid project_id" ) # Test missing auth token expect_error( - download_project("SCPCP000001", ""), + download_project("SCPCP000001", auth_token = ""), "Authorization token must be provided" ) # Test invalid quiet parameter expect_error( - download_project("SCPCP000001", "valid-token", quiet = "not-logical"), + download_project("SCPCP000001", auth_token = "valid-token", quiet = "not-logical"), "quiet must be a logical value" ) # Test invalid merged parameter expect_error( - download_project("SCPCP000001", "valid-token", merged = "not-logical"), + download_project("SCPCP000001", auth_token = "valid-token", merged = "not-logical"), "merged must be a logical value" ) # Test invalid include_multiplexed parameter expect_error( - download_project("SCPCP000001", "valid-token", include_multiplexed = "not-logical"), + download_project( + "SCPCP000001", + auth_token = "valid-token", + include_multiplexed = "not-logical" + ), "include_multiplexed must be NULL or a logical value" ) }) test_that("download_project validates format and merged combinations", { expect_error( - download_project("SCPCP000001", "valid-token", format = "spatial", merged = TRUE), + download_project("SCPCP000001", auth_token = "valid-token", format = "spatial", merged = TRUE), "Merged spatial files are not available" ) }) @@ -143,7 +157,7 @@ test_that("download_project uses the available dataset when include_multiplexed }, download_and_extract_file = function(url, ...) unname(url) ) - result <- download_project("SCPCP000001", "valid-token", format = "sce") + result <- download_project("SCPCP000001", auth_token = "valid-token", format = "sce") expect_match(result, "no-multi-id") }) @@ -166,7 +180,7 @@ test_that("download_project warns when include_multiplexed = TRUE but project ha expect_warning( result <- download_project( "SCPCP000001", - "valid-token", + auth_token = "valid-token", format = "sce", include_multiplexed = TRUE ), @@ -191,7 +205,7 @@ test_that("download_project uses multiplexed dataset when include_multiplexed = }, download_and_extract_file = function(url, ...) unname(url) ) - result <- download_project("SCPCP000001", "valid-token", format = "sce") + result <- download_project("SCPCP000001", auth_token = "valid-token", format = "sce") expect_match(result, "multi-id") }) @@ -213,7 +227,7 @@ test_that("download_project uses non-multiplexed dataset when include_multiplexe ) result <- download_project( "SCPCP000001", - "valid-token", + auth_token = "valid-token", format = "sce", include_multiplexed = FALSE ) @@ -231,7 +245,7 @@ test_that("download_project errors when unexpected number of datasets returned", } ) expect_error( - download_project("SCPCP000001", "valid-token", format = "sce"), + download_project("SCPCP000001", auth_token = "valid-token", format = "sce"), "Multiple pre-built datasets found" ) }) @@ -252,7 +266,7 @@ test_that("download_project downloads when a matching CCDL dataset exists", { }, download_and_extract_file = function(url, ...) c("path/to/file1.rds", "path/to/file2.rds") ) - result <- download_project("SCPCP000001", "valid-token", format = "sce") + result <- download_project("SCPCP000001", auth_token = "valid-token", format = "sce") expect_equal(result, c("path/to/file1.rds", "path/to/file2.rds")) }) @@ -262,7 +276,7 @@ test_that("download_project errors when no CCDL dataset is found", { get_ccdl_datasets = function(...) list() ) expect_error( - download_project("SCPCP000001", "valid-token", format = "sce"), + download_project("SCPCP000001", auth_token = "valid-token", format = "sce"), "SCPCP000001" ) }) @@ -273,15 +287,25 @@ test_that("download_project error mentions relevant options when none found", { get_ccdl_datasets = function(...) list() ) expect_error( - download_project("SCPCP000001", "valid-token", format = "sce", merged = TRUE), + download_project("SCPCP000001", auth_token = "valid-token", format = "sce", merged = TRUE), "merged = TRUE" ) expect_error( - download_project("SCPCP000001", "valid-token", format = "sce", include_multiplexed = FALSE), + download_project( + "SCPCP000001", + auth_token = "valid-token", + format = "sce", + include_multiplexed = FALSE + ), "include_multiplexed = FALSE" ) expect_error( - download_project("SCPCP000001", "valid-token", format = "sce", include_multiplexed = TRUE), + download_project( + "SCPCP000001", + auth_token = "valid-token", + format = "sce", + include_multiplexed = TRUE + ), "include_multiplexed = TRUE" ) }) @@ -300,7 +324,7 @@ test_that("download_project errors when no dataset has is_succeeded = TRUE", { } ) expect_error( - download_project("SCPCP000001", "valid-token", format = "sce"), + download_project("SCPCP000001", auth_token = "valid-token", format = "sce"), "No pre-built dataset found" ) })