tests/testthat/test-cookies.R

context("Cookies")

skip_if_no_cookie_support <- function() {
  skip_if_not_installed("sodium")
  skip_if_not_installed("base64enc")
}

test_that("cookies are parsed", {

  cookies <- parseCookies("spaced=cookie%2C%20here; another=2")
  expect_equal(names(cookies), c("spaced", "another"))
  expect_equal(cookies$spaced, "cookie, here")
  expect_equal(cookies$another, "2")

  cookies <- parseCookies("a=zxcv=asdf; missingVal=; b=qwer=ttyui")
  expect_equal(names(cookies), c("a", "missingVal", "b"))
  expect_equal(cookies$a, "zxcv=asdf")
  expect_equal(cookies$missingVal, "")
  expect_equal(cookies$b, "qwer=ttyui")

})

test_that("missing cookies are an empty list", {
  cookies <- parseCookies("")

  expect_equal(cookies, list())
})

cookieReq <- function(cookieStr) {
  req <- new.env()
  req$HTTP_COOKIE <- cookieStr
  cookieFilter(req)
  req
}

test_that("the cookies list is set", {
  req <- cookieReq("abc=123")
  expect_equal(req$cookies$abc, "123")
})

test_that("missing cookie values are empty string", {
  req <- cookieReq("abc=")
  expect_equal(req$cookies$abc, "")
})

test_that("cookies can convert to string", {
  testthat::skip_on_cran()

  expect_equal(cookieToStr("abc", 123), "abc=123")
  expect_equal(cookieToStr("complex", "string with spaces"), "complex=string%20with%20spaces")
  expect_equal(cookieToStr("complex2", "forbidden:,%/"), "complex2=forbidden%3A%2C%25%2F")
  expect_equal(cookieToStr("abc", 123, path="/somepath"), "abc=123; Path=/somepath")
  expect_equal(cookieToStr("abc", 123, http=TRUE, secure=TRUE), "abc=123; HttpOnly; Secure")
  expect_equal(cookieToStr("abc", 123, http=TRUE, secure=TRUE, same_site="None"), "abc=123; HttpOnly; Secure; SameSite=None")

  now <- force(Sys.time())
  cookieToStr_ <- function(expiration, ...) {
    cookieToStr("abc", 123, expiration = expiration, ..., now = now)
  }
  cookie_match <- function(expirationStr, expiresSec) {
    # difftime is exclusive, so the Max-Age may be off by one on positive time diffs.
    #   Using a regex from 0 to 9 incase a slow machine is encountered
    # match from 0 to 9 seconds
    paste0("abc=123; Expires= ", expirationStr, "; Max-Age= ", expiresSec)
  }
  expect_cookie <- function(expiresSec) {
    expires <- now + expiresSec
    expyStr <- format(expires, format="%a, %e %b %Y %T", tz="GMT", usetz=TRUE)

    # When given as a number of seconds
    expect_equal(cookieToStr_(expiresSec), cookie_match(expyStr, expiresSec), label = "Raw seconds expiration cookie")

    # When given as a POSIXct
    expect_equal(cookieToStr_(expires), cookie_match(expyStr, expiresSec), label = "POSIXct expiration cookie")
  }

  # Test date in the future
  expect_cookie(9)

  # Works with a negative number of seconds
  expect_cookie(-8)
})

test_that("remove cookie string works", {
  expect_equal(
    removeCookieStr("asdf"),
    "asdf=; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
  )
  expect_equal(
    removeCookieStr("asdf", path = "/"),
    "asdf=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
  )
  expect_equal(
    removeCookieStr("asdf", http = TRUE),
    "asdf=; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
  )
  expect_equal(
    removeCookieStr("asdf", secure = TRUE),
    "asdf=; Secure; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
  )
  expect_equal(
    removeCookieStr("asdf", path = "/", http = TRUE, secure = TRUE),
    "asdf=; Path=/; HttpOnly; Secure; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
  )
  expect_equal(
    removeCookieStr("asdf", path = "/", http = TRUE, secure = TRUE, same_site = "None"),
    "asdf=; Path=/; HttpOnly; Secure; SameSite=None; Expires=Thu, 01 Jan 1970 00:00:00 GMT"
  )
})


test_that("asCookieKey conforms entropy", {
  skip_if_no_cookie_support()


  secretFromStr <- function(val, count) {
    rep(val, count) %>%
      paste0(collapse = "")
  }

  expect_cookie_key <- function(key) {
    expect_type(key, "raw")
    expect_length(key, 32)
  }
  expect_invalid_cookie <- function(secret) {
    expect_error({
      asCookieKey(secret)
    }, "Illegal cookie")
  }
  expect_legacy_cookie <- function(secret) {
    expect_warning({
      cookie <- asCookieKey(secret)
    }, "Legacy cookie secret")
    ret <- expect_cookie_key(cookie)
    invisible(ret)
  }

  expect_warning({
    expect_null(asCookieKey(NULL))
  }, "Cookies will not be encrypted")

  # not char
  expect_invalid_cookie(42)
  expect_invalid_cookie(sodium::random(31))
  expect_invalid_cookie(sodium::random(100))

  # legacy cookie
  # convert non hexadecimal to hexadecimal
  expect_legacy_cookie(secretFromStr("a", 63))
  expect_legacy_cookie(secretFromStr("a", 65))
  expect_legacy_cookie(secretFromStr("/", 64))

  # Used as 64 digit hex bin
  ## lower case a
  char <- secretFromStr("a", 64)
  key <- asCookieKey(char)
  expect_cookie_key(key)
  expect_equal(key, sodium::hex2bin(char))

  ## upper case a
  char <- secretFromStr("A", 64)
  key <- asCookieKey(char)
  expect_cookie_key(key)
  expect_equal(key, sodium::hex2bin(char))

  ## hex char input
  randomRaw <- sodium::random(32) %>% sodium::bin2hex()
  key <- asCookieKey(randomRaw)
  expect_cookie_key(key)
  expect_equal(sodium::bin2hex(key), randomRaw)
})


test_that("cookie encryption works", {
  skip_if_no_cookie_support()

  # check that you can't encode a NULL value
  expect_equal(encodeCookie(NULL, NULL), "")
  expect_equal(encodeCookie(NULL, asCookieKey(random_cookie_key())), "")

  xVals <- list(
    list(),
    "",
    list(a = 4, b = 3),
    rep("1234567890", 100) %>% paste0(collapse = "")
  )
  keys <- list(
    NULL, # no key
    asCookieKey(random_cookie_key()), # random key
    asCookieKey(random_cookie_key()) # different random key
  )

  for (key in keys) {
    for (x in xVals) {
      encrypted <- encodeCookie(x, key)
      encryptedStr <- cookieToStr("cookieVal", encrypted)

      encryptedParsed <- parseCookies(encryptedStr)
      maybeX <- decodeCookie(encryptedParsed$cookieVal, key)
      expect_equal(x, maybeX)
    }
  }

})

test_that("cookie encyption fails smoothly", {
  skip_if_no_cookie_support()

  x <- list(x = 4, y = 5)

  # garbage in
  garbage <- x %>%
    serialize(NULL) %>%
    sodium::hash() %>% # make "garbage"
    base64enc::base64encode()

  # garbage in, no key
  expect_error({
    decodeCookie(garbage, NULL)
  }) # error from jsonlite::parse_json()
  # garbage in, key
  expect_error({
    decodeCookie(garbage, asCookieKey(random_cookie_key()))
  }, "Could not separate")

  infoList <- list(
    # different cookies
    list(
      a = asCookieKey(random_cookie_key()),
      b = asCookieKey(random_cookie_key()),
      error = "Failed to decrypt"
    ),
    # not encrypted, try to decrypt
    list(
      a = NULL,
      b = asCookieKey(random_cookie_key()),
      error = "Could not separate"
    ),
    # encrypted, no decryption
    list(
      a = asCookieKey(random_cookie_key()),
      b = NULL
      # error from jsonlite::parse_json()
    )
  )

  for (info in infoList) {
    keyA <- info$a
    keyB <- info$b
    err <- info$error

    expect_error({
      encrypted <- encodeCookie(x, keyA)
      encryptedStr <- cookieToStr("cookieVal", encrypted)

      encryptedParsed <- parseCookies(encryptedStr)
      maybeX <- decodeCookie(encryptedParsed$cookieVal, keyB)
    }, err)
  }

})


test_that("large cookie size makes warning", {
  skip_if_no_cookie_support()

  largeObj <- rbind(iris, iris)
  encrypted <- encodeCookie(largeObj, NULL)
  expect_warning({
    cookieToStr("cookieVal", encrypted)
  }, "Cookie being saved is too large")
})

Try the plumber package in your browser

Any scripts or data that you put into this service are public.

plumber documentation built on Sept. 7, 2022, 1:05 a.m.