From 5c76aa231b423d9f5b5bb43ea5ef6701d2da8004 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:06:05 +0000 Subject: [PATCH 01/10] Initial plan From 0ed009f97725d8627c1f7c6fdd22579e1773bb56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:28:18 +0000 Subject: [PATCH 02/10] feat: add header and cookie params to req_modify() and req_prepare() Agent-Logs-Url: https://github.com/api2r/nectar/sessions/5b2e2bee-e1e8-43c9-bba9-9037460b89f2 Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> --- R/aaa-shared_params.R | 4 +++ R/req_modify.R | 24 +++++++++++++++ R/req_prepare.R | 4 +++ tests/testthat/test-req_prepare.R | 51 +++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+) diff --git a/R/aaa-shared_params.R b/R/aaa-shared_params.R index f8f16ce..653f12d 100644 --- a/R/aaa-shared_params.R +++ b/R/aaa-shared_params.R @@ -21,6 +21,8 @@ #' @param body (multiple types) An object to use as the body of the request. If #' any component of the body is a path, pass it through [fs::path()] or #' otherwise give it the class "fs_path" to indicate that it is a path. +#' @param cookie (`list` or `NULL`) An optional list of cookies to set on the +#' request using [httr2::req_cookies_set()]. `NULL` elements are removed. #' @param call (`environment`) The environment from which a function was called, #' e.g. [rlang::caller_env()] (the default). The environment will be mentioned #' in error messages as the source of the error. This argument is particularly @@ -36,6 +38,8 @@ #' other than `GET` or `POST`, supply it. Case is ignored. #' @param mime_type (`length-1 character`) The mime type of any files present in #' the body. Some APIs allow you to leave this as NULL for them to guess. +#' @param header (`list` or `NULL`) An optional list of headers to add to the +#' request using [httr2::req_headers()]. `NULL` elements are removed. #' @param location (`length-1 character`) Where the API key should be passed. #' One of `"header"` (default), `"query"`, or `"cookie"`. #' @param name (`length-1 character`) The name of a package or other thing to diff --git a/R/req_modify.R b/R/req_modify.R index 693706e..5919bb9 100644 --- a/R/req_modify.R +++ b/R/req_modify.R @@ -22,6 +22,8 @@ req_modify <- function( body = NULL, mime_type = NULL, method = NULL, + header = NULL, + cookie = NULL, call = rlang::caller_env() ) { rlang::check_dots_empty() @@ -29,6 +31,8 @@ req_modify <- function( req <- .req_query_flatten(req, query) req <- .req_body_auto(req, body, mime_type, call = call) req <- .req_method_apply(req, method, call = call) + req <- .req_headers_flatten(req, header) + req <- .req_cookies_flatten(req, cookie) return(.as_nectar_request(req)) } @@ -57,6 +61,26 @@ req_modify <- function( rlang::inject(httr2::req_url_query(req, !!!query)) } +#' Add non-empty header elements to a request +#' +#' @inheritParams .shared-params +#' @inherit .shared-request return +#' @keywords internal +.req_headers_flatten <- function(req, header) { + header <- purrr::discard(header, is.null) + rlang::inject(httr2::req_headers(req, !!!header)) +} + +#' Add non-empty cookie elements to a request +#' +#' @inheritParams .shared-params +#' @inherit .shared-request return +#' @keywords internal +.req_cookies_flatten <- function(req, cookie) { + cookie <- purrr::discard(cookie, is.null) + rlang::inject(httr2::req_cookies_set(req, !!!cookie)) +} + #' Add a method if it is supplied #' #' [httr2::req_method()] errors if `method` is `NULL`, rather than using the diff --git a/R/req_prepare.R b/R/req_prepare.R index f00991e..a12d894 100644 --- a/R/req_prepare.R +++ b/R/req_prepare.R @@ -25,6 +25,8 @@ req_prepare <- function( body = NULL, mime_type = NULL, method = NULL, + header = NULL, + cookie = NULL, additional_user_agent = NULL, auth = NULL, tidy_policy = tidy_policy_body_auto(), @@ -44,6 +46,8 @@ req_prepare <- function( body = body, mime_type = mime_type, method = method, + header = header, + cookie = cookie, call = call ) auth <- .as_nectar_auth(auth, call = call) diff --git a/tests/testthat/test-req_prepare.R b/tests/testthat/test-req_prepare.R index b319b1f..d862e98 100644 --- a/tests/testthat/test-req_prepare.R +++ b/tests/testthat/test-req_prepare.R @@ -176,6 +176,57 @@ test_that("req_prepare() applies prepared auth (#81)", { ) }) +test_that("req_prepare() applies headers", { + test_result <- req_prepare( + base_url = "https://example.com", + header = list( + `X-Custom-Header` = "value1", + `X-Another-Header` = "value2" + ) + ) + expect_in("X-Custom-Header", names(test_result$headers)) + expect_in("X-Another-Header", names(test_result$headers)) + expect_identical(test_result$headers[["X-Custom-Header"]], "value1") + expect_identical(test_result$headers[["X-Another-Header"]], "value2") +}) + +test_that("req_prepare() removes NULL headers", { + test_result <- req_prepare( + base_url = "https://example.com", + header = list( + `X-Custom-Header` = "value1", + `X-Null-Header` = NULL + ) + ) + expect_in("X-Custom-Header", names(test_result$headers)) + expect_false("X-Null-Header" %in% names(test_result$headers)) +}) + +test_that("req_prepare() applies cookies", { + test_result <- req_prepare( + base_url = "https://example.com", + cookie = list( + session_id = "abc123", + user_pref = "dark_mode" + ) + ) + expect_true(grepl("session_id=abc123", test_result$options$cookie)) + expect_true(grepl("user_pref=dark_mode", test_result$options$cookie)) +}) + +test_that("req_prepare() removes NULL cookies", { + test_result <- req_prepare( + base_url = "https://example.com", + cookie = list( + session_id = "abc123", + empty_cookie = NULL + ) + ) + expect_true(grepl("session_id=abc123", test_result$options$cookie)) + expect_false(grepl("empty_cookie", test_result$options$cookie)) +}) + + test_that("req_prepare() errors for unsupported auth objects (#81)", { expect_error( req_prepare( From cb3ed5026c27a0663cb56f894d26179a6dac5837 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:29:37 +0000 Subject: [PATCH 03/10] fix: correct alphabetical ordering of header/cookie params and trailing blank line Agent-Logs-Url: https://github.com/api2r/nectar/sessions/5b2e2bee-e1e8-43c9-bba9-9037460b89f2 Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> --- R/aaa-shared_params.R | 13 ++++++------- tests/testthat/test-req_prepare.R | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/R/aaa-shared_params.R b/R/aaa-shared_params.R index 653f12d..d4cbe3e 100644 --- a/R/aaa-shared_params.R +++ b/R/aaa-shared_params.R @@ -21,8 +21,6 @@ #' @param body (multiple types) An object to use as the body of the request. If #' any component of the body is a path, pass it through [fs::path()] or #' otherwise give it the class "fs_path" to indicate that it is a path. -#' @param cookie (`list` or `NULL`) An optional list of cookies to set on the -#' request using [httr2::req_cookies_set()]. `NULL` elements are removed. #' @param call (`environment`) The environment from which a function was called, #' e.g. [rlang::caller_env()] (the default). The environment will be mentioned #' in error messages as the source of the error. This argument is particularly @@ -31,18 +29,19 @@ #' @param check_type (`length-1 logical`) Whether to check that the response has #' the expected content type. Set to `FALSE` if the response is not #' specifically tagged as the proper type. +#' @param cookie (`list` or `NULL`) An optional list of cookies to set on the +#' request using [httr2::req_cookies_set()]. `NULL` elements are removed. #' @param existing_user_agent (`length-1 character`, optional) An existing user #' agent, such as the value of `req$options$useragent` in a [httr2::request()] #' object. -#' @param method (`length-1 character`, optional) If the method is something -#' other than `GET` or `POST`, supply it. Case is ignored. -#' @param mime_type (`length-1 character`) The mime type of any files present in -#' the body. Some APIs allow you to leave this as NULL for them to guess. #' @param header (`list` or `NULL`) An optional list of headers to add to the #' request using [httr2::req_headers()]. `NULL` elements are removed. #' @param location (`length-1 character`) Where the API key should be passed. #' One of `"header"` (default), `"query"`, or `"cookie"`. -#' @param name (`length-1 character`) The name of a package or other thing to +#' @param method (`length-1 character`, optional) If the method is something +#' other than `GET` or `POST`, supply it. Case is ignored. +#' @param mime_type (`length-1 character`) The mime type of any files present in +#' the body. Some APIs allow you to leave this as NULL for them to guess.#' @param name (`length-1 character`) The name of a package or other thing to #' add to or remove from the user agent string. #' @param pagination_fn (`function`) A function that takes the previous response #' (`resp`) to generate the next request in a call to diff --git a/tests/testthat/test-req_prepare.R b/tests/testthat/test-req_prepare.R index d862e98..efc7c65 100644 --- a/tests/testthat/test-req_prepare.R +++ b/tests/testthat/test-req_prepare.R @@ -226,7 +226,6 @@ test_that("req_prepare() removes NULL cookies", { expect_false(grepl("empty_cookie", test_result$options$cookie)) }) - test_that("req_prepare() errors for unsupported auth objects (#81)", { expect_error( req_prepare( From 06f95cf5f1d00efbccf8c3c5cf9bda32a40a15d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:30:16 +0000 Subject: [PATCH 04/10] fix: add missing newline in aaa-shared_params.R Agent-Logs-Url: https://github.com/api2r/nectar/sessions/5b2e2bee-e1e8-43c9-bba9-9037460b89f2 Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> --- R/aaa-shared_params.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/aaa-shared_params.R b/R/aaa-shared_params.R index d4cbe3e..f1524c3 100644 --- a/R/aaa-shared_params.R +++ b/R/aaa-shared_params.R @@ -41,7 +41,8 @@ #' @param method (`length-1 character`, optional) If the method is something #' other than `GET` or `POST`, supply it. Case is ignored. #' @param mime_type (`length-1 character`) The mime type of any files present in -#' the body. Some APIs allow you to leave this as NULL for them to guess.#' @param name (`length-1 character`) The name of a package or other thing to +#' the body. Some APIs allow you to leave this as NULL for them to guess. +#' @param name (`length-1 character`) The name of a package or other thing to #' add to or remove from the user agent string. #' @param pagination_fn (`function`) A function that takes the previous response #' (`resp`) to generate the next request in a call to From ce4e3618d2ac3e19a4e3497750cd05d37ea57459 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 12 May 2026 13:37:55 -0500 Subject: [PATCH 05/10] air, wrap, and document --- man/dot-req_cookies_flatten.Rd | 21 +++++++++++++++++++++ man/dot-req_headers_flatten.Rd | 21 +++++++++++++++++++++ man/dot-shared-params.Rd | 12 +++++++++--- man/req_modify.Rd | 8 ++++++++ man/req_prepare.Rd | 8 ++++++++ 5 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 man/dot-req_cookies_flatten.Rd create mode 100644 man/dot-req_headers_flatten.Rd diff --git a/man/dot-req_cookies_flatten.Rd b/man/dot-req_cookies_flatten.Rd new file mode 100644 index 0000000..4871a8f --- /dev/null +++ b/man/dot-req_cookies_flatten.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/req_modify.R +\name{.req_cookies_flatten} +\alias{.req_cookies_flatten} +\title{Add non-empty cookie elements to a request} +\usage{ +.req_cookies_flatten(req, cookie) +} +\arguments{ +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} + +\item{cookie}{(\code{list} or \code{NULL}) An optional list of cookies to set on the +request using \code{\link[httr2:req_cookies_set]{httr2::req_cookies_set()}}. \code{NULL} elements are removed.} +} +\value{ +A \code{\link[httr2:request]{httr2::request()}} object with additional class \code{nectar_request}. +} +\description{ +Add non-empty cookie elements to a request +} +\keyword{internal} diff --git a/man/dot-req_headers_flatten.Rd b/man/dot-req_headers_flatten.Rd new file mode 100644 index 0000000..cd72157 --- /dev/null +++ b/man/dot-req_headers_flatten.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/req_modify.R +\name{.req_headers_flatten} +\alias{.req_headers_flatten} +\title{Add non-empty header elements to a request} +\usage{ +.req_headers_flatten(req, header) +} +\arguments{ +\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} + +\item{header}{(\code{list} or \code{NULL}) An optional list of headers to add to the +request using \code{\link[httr2:req_headers]{httr2::req_headers()}}. \code{NULL} elements are removed.} +} +\value{ +A \code{\link[httr2:request]{httr2::request()}} object with additional class \code{nectar_request}. +} +\description{ +Add non-empty header elements to a request +} +\keyword{internal} diff --git a/man/dot-shared-params.Rd b/man/dot-shared-params.Rd index c709c06..5a05cba 100644 --- a/man/dot-shared-params.Rd +++ b/man/dot-shared-params.Rd @@ -39,19 +39,25 @@ other functions.} the expected content type. Set to \code{FALSE} if the response is not specifically tagged as the proper type.} +\item{cookie}{(\code{list} or \code{NULL}) An optional list of cookies to set on the +request using \code{\link[httr2:req_cookies_set]{httr2::req_cookies_set()}}. \code{NULL} elements are removed.} + \item{existing_user_agent}{(\verb{length-1 character}, optional) An existing user agent, such as the value of \code{req$options$useragent} in a \code{\link[httr2:request]{httr2::request()}} object.} +\item{header}{(\code{list} or \code{NULL}) An optional list of headers to add to the +request using \code{\link[httr2:req_headers]{httr2::req_headers()}}. \code{NULL} elements are removed.} + +\item{location}{(\verb{length-1 character}) Where the API key should be passed. +One of \code{"header"} (default), \code{"query"}, or \code{"cookie"}.} + \item{method}{(\verb{length-1 character}, optional) If the method is something other than \code{GET} or \code{POST}, supply it. Case is ignored.} \item{mime_type}{(\verb{length-1 character}) The mime type of any files present in the body. Some APIs allow you to leave this as NULL for them to guess.} -\item{location}{(\verb{length-1 character}) Where the API key should be passed. -One of \code{"header"} (default), \code{"query"}, or \code{"cookie"}.} - \item{name}{(\verb{length-1 character}) The name of a package or other thing to add to or remove from the user agent string.} diff --git a/man/req_modify.Rd b/man/req_modify.Rd index 5d0ebde..33127d4 100644 --- a/man/req_modify.Rd +++ b/man/req_modify.Rd @@ -12,6 +12,8 @@ req_modify( body = NULL, mime_type = NULL, method = NULL, + header = NULL, + cookie = NULL, call = rlang::caller_env() ) } @@ -40,6 +42,12 @@ the body. Some APIs allow you to leave this as NULL for them to guess.} \item{method}{(\verb{length-1 character}, optional) If the method is something other than \code{GET} or \code{POST}, supply it. Case is ignored.} +\item{header}{(\code{list} or \code{NULL}) An optional list of headers to add to the +request using \code{\link[httr2:req_headers]{httr2::req_headers()}}. \code{NULL} elements are removed.} + +\item{cookie}{(\code{list} or \code{NULL}) An optional list of cookies to set on the +request using \code{\link[httr2:req_cookies_set]{httr2::req_cookies_set()}}. \code{NULL} elements are removed.} + \item{call}{(\code{environment}) The environment from which a function was called, e.g. \code{\link[rlang:caller_env]{rlang::caller_env()}} (the default). The environment will be mentioned in error messages as the source of the error. This argument is particularly diff --git a/man/req_prepare.Rd b/man/req_prepare.Rd index 4631de1..ffa79a4 100644 --- a/man/req_prepare.Rd +++ b/man/req_prepare.Rd @@ -12,6 +12,8 @@ req_prepare( body = NULL, mime_type = NULL, method = NULL, + header = NULL, + cookie = NULL, additional_user_agent = NULL, auth = NULL, tidy_policy = tidy_policy_body_auto(), @@ -46,6 +48,12 @@ the body. Some APIs allow you to leave this as NULL for them to guess.} \item{method}{(\verb{length-1 character}, optional) If the method is something other than \code{GET} or \code{POST}, supply it. Case is ignored.} +\item{header}{(\code{list} or \code{NULL}) An optional list of headers to add to the +request using \code{\link[httr2:req_headers]{httr2::req_headers()}}. \code{NULL} elements are removed.} + +\item{cookie}{(\code{list} or \code{NULL}) An optional list of cookies to set on the +request using \code{\link[httr2:req_cookies_set]{httr2::req_cookies_set()}}. \code{NULL} elements are removed.} + \item{additional_user_agent}{(\verb{length-1 character}) A string to identify where a request is coming from. We automatically include information about your package and nectar, but use this to provide additional details. From 414ee7d54c5813a3b74e793027d39cf725ef1586 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:48:15 +0000 Subject: [PATCH 06/10] refactor: remove .req_headers_flatten and pass NULL headers through to httr2 Agent-Logs-Url: https://github.com/api2r/nectar/sessions/fde4ea6e-b7ed-4973-8911-01ec29111460 Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> --- R/aaa-shared_params.R | 3 ++- R/req_modify.R | 14 +++----------- tests/testthat/test-req_prepare.R | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/R/aaa-shared_params.R b/R/aaa-shared_params.R index f1524c3..579ab85 100644 --- a/R/aaa-shared_params.R +++ b/R/aaa-shared_params.R @@ -35,7 +35,8 @@ #' agent, such as the value of `req$options$useragent` in a [httr2::request()] #' object. #' @param header (`list` or `NULL`) An optional list of headers to add to the -#' request using [httr2::req_headers()]. `NULL` elements are removed. +#' request using [httr2::req_headers()]. A `NULL` value for an individual +#' header will explicitly remove that header if it was previously set. #' @param location (`length-1 character`) Where the API key should be passed. #' One of `"header"` (default), `"query"`, or `"cookie"`. #' @param method (`length-1 character`, optional) If the method is something diff --git a/R/req_modify.R b/R/req_modify.R index 5919bb9..8c43afc 100644 --- a/R/req_modify.R +++ b/R/req_modify.R @@ -31,7 +31,9 @@ req_modify <- function( req <- .req_query_flatten(req, query) req <- .req_body_auto(req, body, mime_type, call = call) req <- .req_method_apply(req, method, call = call) - req <- .req_headers_flatten(req, header) + if (length(header)) { + req <- rlang::inject(httr2::req_headers(req, !!!header)) + } req <- .req_cookies_flatten(req, cookie) return(.as_nectar_request(req)) } @@ -61,16 +63,6 @@ req_modify <- function( rlang::inject(httr2::req_url_query(req, !!!query)) } -#' Add non-empty header elements to a request -#' -#' @inheritParams .shared-params -#' @inherit .shared-request return -#' @keywords internal -.req_headers_flatten <- function(req, header) { - header <- purrr::discard(header, is.null) - rlang::inject(httr2::req_headers(req, !!!header)) -} - #' Add non-empty cookie elements to a request #' #' @inheritParams .shared-params diff --git a/tests/testthat/test-req_prepare.R b/tests/testthat/test-req_prepare.R index efc7c65..5e8b7e9 100644 --- a/tests/testthat/test-req_prepare.R +++ b/tests/testthat/test-req_prepare.R @@ -190,7 +190,7 @@ test_that("req_prepare() applies headers", { expect_identical(test_result$headers[["X-Another-Header"]], "value2") }) -test_that("req_prepare() removes NULL headers", { +test_that("req_prepare() uses NULL headers to remove previously-set headers", { test_result <- req_prepare( base_url = "https://example.com", header = list( From adb68bd33d32384838ae8bf7c5fc2a7ab391f841 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 12 May 2026 13:57:26 -0500 Subject: [PATCH 07/10] Document --- man/dot-req_headers_flatten.Rd | 21 --------------------- man/dot-shared-params.Rd | 3 ++- man/req_modify.Rd | 3 ++- man/req_prepare.Rd | 3 ++- 4 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 man/dot-req_headers_flatten.Rd diff --git a/man/dot-req_headers_flatten.Rd b/man/dot-req_headers_flatten.Rd deleted file mode 100644 index cd72157..0000000 --- a/man/dot-req_headers_flatten.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/req_modify.R -\name{.req_headers_flatten} -\alias{.req_headers_flatten} -\title{Add non-empty header elements to a request} -\usage{ -.req_headers_flatten(req, header) -} -\arguments{ -\item{req}{(\code{httr2_request}) A \code{\link[httr2:request]{httr2::request()}} object.} - -\item{header}{(\code{list} or \code{NULL}) An optional list of headers to add to the -request using \code{\link[httr2:req_headers]{httr2::req_headers()}}. \code{NULL} elements are removed.} -} -\value{ -A \code{\link[httr2:request]{httr2::request()}} object with additional class \code{nectar_request}. -} -\description{ -Add non-empty header elements to a request -} -\keyword{internal} diff --git a/man/dot-shared-params.Rd b/man/dot-shared-params.Rd index 5a05cba..7275819 100644 --- a/man/dot-shared-params.Rd +++ b/man/dot-shared-params.Rd @@ -47,7 +47,8 @@ agent, such as the value of \code{req$options$useragent} in a \code{\link[httr2: object.} \item{header}{(\code{list} or \code{NULL}) An optional list of headers to add to the -request using \code{\link[httr2:req_headers]{httr2::req_headers()}}. \code{NULL} elements are removed.} +request using \code{\link[httr2:req_headers]{httr2::req_headers()}}. A \code{NULL} value for an individual +header will explicitly remove that header if it was previously set.} \item{location}{(\verb{length-1 character}) Where the API key should be passed. One of \code{"header"} (default), \code{"query"}, or \code{"cookie"}.} diff --git a/man/req_modify.Rd b/man/req_modify.Rd index 33127d4..713eb25 100644 --- a/man/req_modify.Rd +++ b/man/req_modify.Rd @@ -43,7 +43,8 @@ the body. Some APIs allow you to leave this as NULL for them to guess.} other than \code{GET} or \code{POST}, supply it. Case is ignored.} \item{header}{(\code{list} or \code{NULL}) An optional list of headers to add to the -request using \code{\link[httr2:req_headers]{httr2::req_headers()}}. \code{NULL} elements are removed.} +request using \code{\link[httr2:req_headers]{httr2::req_headers()}}. A \code{NULL} value for an individual +header will explicitly remove that header if it was previously set.} \item{cookie}{(\code{list} or \code{NULL}) An optional list of cookies to set on the request using \code{\link[httr2:req_cookies_set]{httr2::req_cookies_set()}}. \code{NULL} elements are removed.} diff --git a/man/req_prepare.Rd b/man/req_prepare.Rd index ffa79a4..569f493 100644 --- a/man/req_prepare.Rd +++ b/man/req_prepare.Rd @@ -49,7 +49,8 @@ the body. Some APIs allow you to leave this as NULL for them to guess.} other than \code{GET} or \code{POST}, supply it. Case is ignored.} \item{header}{(\code{list} or \code{NULL}) An optional list of headers to add to the -request using \code{\link[httr2:req_headers]{httr2::req_headers()}}. \code{NULL} elements are removed.} +request using \code{\link[httr2:req_headers]{httr2::req_headers()}}. A \code{NULL} value for an individual +header will explicitly remove that header if it was previously set.} \item{cookie}{(\code{list} or \code{NULL}) An optional list of cookies to set on the request using \code{\link[httr2:req_cookies_set]{httr2::req_cookies_set()}}. \code{NULL} elements are removed.} From 7adc677046dac230058c352e0644faa1eab66e14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 19:34:53 +0000 Subject: [PATCH 08/10] test: add test-req_modify.R with 100% coverage for R/req_modify.R Agent-Logs-Url: https://github.com/api2r/nectar/sessions/d58f3771-2488-4613-8fe3-6ae28c9adf88 Co-authored-by: jonthegeek <33983824+jonthegeek@users.noreply.github.com> --- tests/testthat/_snaps/req_modify.md | 25 ++++ tests/testthat/test-req_modify.R | 190 ++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 tests/testthat/_snaps/req_modify.md create mode 100644 tests/testthat/test-req_modify.R diff --git a/tests/testthat/_snaps/req_modify.md b/tests/testthat/_snaps/req_modify.md new file mode 100644 index 0000000..1b857aa --- /dev/null +++ b/tests/testthat/_snaps/req_modify.md @@ -0,0 +1,25 @@ +# req_modify() handles bodies with paths + + Code + test_result <- req_modify(req_base, body = list(foo = "bar", baz = fs::path( + test_path("fixtures", "img-test.png")))) + test_result$body + Output + $data + $data$foo + Form data of length 5 (type: application/json) + + $data$baz + Form file: img-test.png + + + $type + [1] "multipart" + + $content_type + NULL + + $params + list() + + diff --git a/tests/testthat/test-req_modify.R b/tests/testthat/test-req_modify.R new file mode 100644 index 0000000..b91ebc8 --- /dev/null +++ b/tests/testthat/test-req_modify.R @@ -0,0 +1,190 @@ +# req_modify() ----------------------------------------------------------------- + +test_that("req_modify() returns an unmodified request when no args are given", { + req_base <- req_init("https://example.com") + test_result <- req_modify(req_base) + expect_s3_class(test_result, "nectar_request") + expect_identical(test_result$url, "https://example.com/") +}) + +test_that("req_modify() errors for unexpected dots", { + req_base <- req_init("https://example.com") + expect_error(req_modify(req_base, unexpected = "arg")) +}) + +test_that("req_modify() deals with paths", { + req_base <- req_init("https://example.com") + test_result <- req_modify(req_base, path = "foo/bar") + expect_identical(test_result$url, "https://example.com/foo/bar") + + test_result <- req_modify( + req_base, + path = list("foo/{bar}", bar = "baz") + ) + expect_identical(test_result$url, "https://example.com/foo/baz") +}) + +test_that("req_modify() uses query parameters", { + req_base <- req_init("https://example.com") + test_result <- req_modify( + req_base, + query = list(foo = "bar", baz = "qux") + ) + expect_identical( + url_normalize(test_result$url), + "https://example.com/?foo=bar&baz=qux" + ) +}) + +test_that("req_modify() uses the .multi arg", { + req_base <- req_init("https://example.com") + test_result <- req_modify( + req_base, + query = list( + foo = "bar", + baz = c("qux", "quux"), + .multi = "comma" + ) + ) + expect_identical( + url_normalize(test_result$url), + "https://example.com/?foo=bar&baz=qux%2Cquux" + ) +}) + +test_that("req_modify() removes empty query parameters", { + req_base <- req_init("https://example.com") + test_result <- req_modify( + req_base, + query = list(foo = NULL, bar = "baz") + ) + expect_identical( + url_normalize(test_result$url), + "https://example.com/?bar=baz" + ) +}) + +test_that("req_modify() uses body parameters", { + req_base <- req_init("https://example.com") + test_result <- req_modify( + req_base, + body = list(foo = "bar", baz = "qux") + ) + expect_identical( + test_result$body$data, + list(foo = "bar", baz = "qux") + ) +}) + +test_that("req_modify() handles bodies with paths", { + req_base <- req_init("https://example.com") + expect_snapshot({ + test_result <- req_modify( + req_base, + body = list( + foo = "bar", + baz = fs::path(test_path("fixtures", "img-test.png")) + ) + ) + test_result$body + }) + expect_identical(test_result$body$type, "multipart") +}) + +test_that("req_modify() applies methods", { + req_base <- req_init("https://example.com") + test_result <- req_modify(req_base, method = "PATCH") + expect_identical(test_result$method, "PATCH") + + test_result <- req_modify(req_base) + expect_null(test_result$method) + + test_result <- req_modify(req_base, body = list(a = 1)) + expect_null(test_result$method) +}) + +test_that("req_modify() applies headers", { + req_base <- req_init("https://example.com") + test_result <- req_modify( + req_base, + header = list( + `X-Custom-Header` = "value1", + `X-Another-Header` = "value2" + ) + ) + expect_in("X-Custom-Header", names(test_result$headers)) + expect_in("X-Another-Header", names(test_result$headers)) + expect_identical(test_result$headers[["X-Custom-Header"]], "value1") + expect_identical(test_result$headers[["X-Another-Header"]], "value2") +}) + +test_that("req_modify() uses NULL headers to remove previously-set headers", { + req_base <- httr2::req_headers( + req_init("https://example.com"), + `X-Remove-Me` = "old-value" + ) + test_result <- req_modify( + req_base, + header = list( + `X-Custom-Header` = "value1", + `X-Remove-Me` = NULL + ) + ) + expect_in("X-Custom-Header", names(test_result$headers)) + expect_false("X-Remove-Me" %in% names(test_result$headers)) +}) + +test_that("req_modify() applies cookies", { + req_base <- req_init("https://example.com") + test_result <- req_modify( + req_base, + cookie = list(session_id = "abc123", user_pref = "dark_mode") + ) + expect_true(grepl("session_id=abc123", test_result$options$cookie)) + expect_true(grepl("user_pref=dark_mode", test_result$options$cookie)) +}) + +test_that("req_modify() removes NULL cookies", { + req_base <- req_init("https://example.com") + test_result <- req_modify( + req_base, + cookie = list(session_id = "abc123", empty_cookie = NULL) + ) + expect_true(grepl("session_id=abc123", test_result$options$cookie)) + expect_false(grepl("empty_cookie", test_result$options$cookie)) +}) + +# .prepare_body() -------------------------------------------------------------- + +test_that(".prepare_body() returns empty list for NULL body", { + result <- .prepare_body(NULL) + expect_length(result, 0) +}) + +test_that(".prepare_body() assigns json class for non-path body", { + result <- .prepare_body(list(foo = "bar")) + expect_s3_class(result, "json") +}) + +test_that(".prepare_body() assigns multipart class when body contains fs_path", { + result <- .prepare_body( + list( + foo = "bar", + baz = fs::path(test_path("fixtures", "img-test.png")) + ) + ) + expect_s3_class(result, "multipart") +}) + +# .prepare_body_part() --------------------------------------------------------- + +test_that(".prepare_body_part() wraps fs_path as form_file", { + path <- fs::path(test_path("fixtures", "img-test.png")) + result <- .prepare_body_part(path) + expect_s3_class(result, "form_file") +}) + +test_that(".prepare_body_part() wraps non-path as form_data", { + result <- .prepare_body_part(list(x = 1)) + expect_s3_class(result, "form_data") +}) From f502e195a788e2af7d982819fa830009d7832832 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 12 May 2026 14:55:19 -0500 Subject: [PATCH 09/10] Clean up `expect_error` --- tests/testthat/_snaps/req_prepare.md | 22 ++++++++++++++++++++++ tests/testthat/test-req_modify.R | 7 ++++++- tests/testthat/test-req_prepare.R | 8 ++++---- tests/testthat/test-resp_body_csv.R | 6 ++++-- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/tests/testthat/_snaps/req_prepare.md b/tests/testthat/_snaps/req_prepare.md index 3c81f53..20fc196 100644 --- a/tests/testthat/_snaps/req_prepare.md +++ b/tests/testthat/_snaps/req_prepare.md @@ -23,6 +23,28 @@ list() +# req_prepare() errors for unsupported tidy policy objects (#86) + + Code + (expect_pkg_error_classes(req_prepare(base_url = "https://example.com", + tidy_policy = "not_tidy_policy"), "nectar", "unsupported_tidy_policy_class")) + Output + + Error: + ! `not_tidy_policy` must be `NULL` or a . + x `not_tidy_policy` is a string. + +# req_prepare() errors for unsupported auth objects (#81) + + Code + (expect_pkg_error_classes(req_prepare(base_url = "https://example.com", auth = "not_auth"), + "nectar", "unsupported_auth_class")) + Output + + Error: + ! `not_auth` must be `NULL` or a . + x `not_auth` is a string. + # .as_nectar_request() fails gracefully for non-reqs Code diff --git a/tests/testthat/test-req_modify.R b/tests/testthat/test-req_modify.R index b91ebc8..1f9a2af 100644 --- a/tests/testthat/test-req_modify.R +++ b/tests/testthat/test-req_modify.R @@ -9,7 +9,12 @@ test_that("req_modify() returns an unmodified request when no args are given", { test_that("req_modify() errors for unexpected dots", { req_base <- req_init("https://example.com") - expect_error(req_modify(req_base, unexpected = "arg")) + expect_error( + { + req_modify(req_base, unexpected = "arg") + }, + class = "rlib_error_dots_nonempty" + ) }) test_that("req_modify() deals with paths", { diff --git a/tests/testthat/test-req_prepare.R b/tests/testthat/test-req_prepare.R index 5e8b7e9..c1b1fe0 100644 --- a/tests/testthat/test-req_prepare.R +++ b/tests/testthat/test-req_prepare.R @@ -156,12 +156,12 @@ test_that("req_prepare() applies prepared tidying (#86)", { }) test_that("req_prepare() errors for unsupported tidy policy objects (#86)", { - expect_error( + expect_nectar_error_snapshot( req_prepare( base_url = "https://example.com", tidy_policy = "not_tidy_policy" ), - class = "nectar-error-unsupported_tidy_policy_class" + "unsupported_tidy_policy_class" ) }) @@ -227,12 +227,12 @@ test_that("req_prepare() removes NULL cookies", { }) test_that("req_prepare() errors for unsupported auth objects (#81)", { - expect_error( + expect_nectar_error_snapshot( req_prepare( base_url = "https://example.com", auth = "not_auth" ), - class = "nectar-error-unsupported_auth_class" + "unsupported_auth_class" ) }) diff --git a/tests/testthat/test-resp_body_csv.R b/tests/testthat/test-resp_body_csv.R index 86c1155..4a9a2b1 100644 --- a/tests/testthat/test-resp_body_csv.R +++ b/tests/testthat/test-resp_body_csv.R @@ -16,7 +16,8 @@ test_that("resp_body_csv fails gracefully for bad data (#40)", { ) expect_error( resp_body_csv(resp), - "Unexpected content type" + "Unexpected content type", + class = "rlang_error" ) }) @@ -38,6 +39,7 @@ test_that("resp_body_tsv fails gracefully for bad data (#40)", { ) expect_error( resp_body_tsv(resp), - "Unexpected content type" + "Unexpected content type", + class = "rlang_error" ) }) From 7b442a884458ec4c1b638de6758458e156caa530 Mon Sep 17 00:00:00 2001 From: Jon Harmon Date: Tue, 12 May 2026 14:57:18 -0500 Subject: [PATCH 10/10] Tag tests to issues --- tests/testthat/test-req_modify.R | 8 ++++---- tests/testthat/test-req_prepare.R | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/testthat/test-req_modify.R b/tests/testthat/test-req_modify.R index 1f9a2af..4d7b919 100644 --- a/tests/testthat/test-req_modify.R +++ b/tests/testthat/test-req_modify.R @@ -108,7 +108,7 @@ test_that("req_modify() applies methods", { expect_null(test_result$method) }) -test_that("req_modify() applies headers", { +test_that("req_modify() applies headers (#92)", { req_base <- req_init("https://example.com") test_result <- req_modify( req_base, @@ -123,7 +123,7 @@ test_that("req_modify() applies headers", { expect_identical(test_result$headers[["X-Another-Header"]], "value2") }) -test_that("req_modify() uses NULL headers to remove previously-set headers", { +test_that("req_modify() uses NULL headers to remove previously-set headers (#92)", { req_base <- httr2::req_headers( req_init("https://example.com"), `X-Remove-Me` = "old-value" @@ -139,7 +139,7 @@ test_that("req_modify() uses NULL headers to remove previously-set headers", { expect_false("X-Remove-Me" %in% names(test_result$headers)) }) -test_that("req_modify() applies cookies", { +test_that("req_modify() applies cookies (#92)", { req_base <- req_init("https://example.com") test_result <- req_modify( req_base, @@ -149,7 +149,7 @@ test_that("req_modify() applies cookies", { expect_true(grepl("user_pref=dark_mode", test_result$options$cookie)) }) -test_that("req_modify() removes NULL cookies", { +test_that("req_modify() removes NULL cookies (#92)", { req_base <- req_init("https://example.com") test_result <- req_modify( req_base, diff --git a/tests/testthat/test-req_prepare.R b/tests/testthat/test-req_prepare.R index c1b1fe0..c29c14a 100644 --- a/tests/testthat/test-req_prepare.R +++ b/tests/testthat/test-req_prepare.R @@ -176,7 +176,7 @@ test_that("req_prepare() applies prepared auth (#81)", { ) }) -test_that("req_prepare() applies headers", { +test_that("req_prepare() applies headers (#92)", { test_result <- req_prepare( base_url = "https://example.com", header = list( @@ -190,7 +190,7 @@ test_that("req_prepare() applies headers", { expect_identical(test_result$headers[["X-Another-Header"]], "value2") }) -test_that("req_prepare() uses NULL headers to remove previously-set headers", { +test_that("req_prepare() uses NULL headers to remove previously-set headers (#92)", { test_result <- req_prepare( base_url = "https://example.com", header = list( @@ -202,7 +202,7 @@ test_that("req_prepare() uses NULL headers to remove previously-set headers", { expect_false("X-Null-Header" %in% names(test_result$headers)) }) -test_that("req_prepare() applies cookies", { +test_that("req_prepare() applies cookies (#92)", { test_result <- req_prepare( base_url = "https://example.com", cookie = list( @@ -214,7 +214,7 @@ test_that("req_prepare() applies cookies", { expect_true(grepl("user_pref=dark_mode", test_result$options$cookie)) }) -test_that("req_prepare() removes NULL cookies", { +test_that("req_prepare() removes NULL cookies (#92)", { test_result <- req_prepare( base_url = "https://example.com", cookie = list(