diff --git a/tests/testthat/apps/content/data.txt b/tests/testthat/apps/content/data.txt new file mode 100644 index 00000000..f5c71047 --- /dev/null +++ b/tests/testthat/apps/content/data.txt @@ -0,0 +1,2 @@ +This is a text file. + diff --git a/tests/testthat/apps/content/index.html b/tests/testthat/apps/content/index.html new file mode 100644 index 00000000..17b21214 --- /dev/null +++ b/tests/testthat/apps/content/index.html @@ -0,0 +1,3 @@ +This is the index file! + +Here's a UTF-8 emoji: 😀 diff --git a/tests/testthat/apps/content/mtcars.csv b/tests/testthat/apps/content/mtcars.csv new file mode 100644 index 00000000..2abcf283 --- /dev/null +++ b/tests/testthat/apps/content/mtcars.csv @@ -0,0 +1,33 @@ +"mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb" +21,6,160,110,3.9,2.62,16.46,0,1,4,4 +21,6,160,110,3.9,2.875,17.02,0,1,4,4 +22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 +21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 +18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 +18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 +14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 +24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 +22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 +19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 +17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 +16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 +17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 +15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 +10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 +10.4,8,460,215,3,5.424,17.82,0,0,3,4 +14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 +32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 +30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 +33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 +21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 +15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 +15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 +13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 +19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 +27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 +26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 +30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 +15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 +19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 +15,8,301,335,3.54,3.57,14.6,0,1,5,8 +21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 diff --git a/tests/testthat/apps/content/subdir/index.html b/tests/testthat/apps/content/subdir/index.html new file mode 100644 index 00000000..b09907cd --- /dev/null +++ b/tests/testthat/apps/content/subdir/index.html @@ -0,0 +1 @@ +This is the index file for content/subdir/ diff --git a/tests/testthat/apps/content_1/index.html b/tests/testthat/apps/content_1/index.html new file mode 100644 index 00000000..10acc96e --- /dev/null +++ b/tests/testthat/apps/content_1/index.html @@ -0,0 +1 @@ +Index file for content1. diff --git a/tests/testthat/helper-app.R b/tests/testthat/helper-app.R new file mode 100644 index 00000000..3333ace7 --- /dev/null +++ b/tests/testthat/helper-app.R @@ -0,0 +1,130 @@ +library(curl) +library(promises) + + +random_open_port <- function(min = 3000, max = 9000, n = 20) { + # Unsafe port list from shiny::runApp() + valid_ports <- setdiff(min:max, c(3659, 4045, 6000, 6665:6669, 6697)) + + # Try up to n ports + for (port in sample(valid_ports, n)) { + handle <- NULL + + # Check if port is open + tryCatch( + handle <- httpuv::startServer("127.0.0.1", port, list()), + error = function(e) { } + ) + if (!is.null(handle)) { + httpuv::stopServer(handle) + return(port) + } + } + + stop("Cannot find an available port") +} + + +curl_fetch_async <- function(url, pool = NULL, data = NULL, handle = new_handle()) { + p <- promises::promise(function(resolve, reject) { + curl_fetch_multi(url, done = resolve, fail = reject, pool = pool, data = data, handle = handle) + }) + + finished <- FALSE + poll <- function() { + if (!finished) { + multi_run(timeout = 0, poll = TRUE, pool = pool) + later::later(poll, 0.01) + } + } + poll() + + p %>% finally(function() { + finished <<- TRUE + }) +} + + +# A way of sending an HTTP request using a socketConnection. This isn't as +# reliable as using curl, so we'll use it only when curl can't do what we want. +http_request_con_async <- function(request, host, port) { + resolve_fun <- NULL + reject_fun <- NULL + con <- NULL + + p <- promises::promise(function(resolve, reject) { + resolve_fun <<- resolve + reject_fun <<- reject + con <<- socketConnection(host, port) + writeLines(c(request, ""), con) + }) + + result <- NULL + # finished <- FALSE + poll <- function() { + result <<- readLines(con) + if (length(result) > 0) { + resolve_fun(result) + } else { + later::later(poll, 0.01) + } + } + poll() + + p %>% finally(function() { + close(con) + }) +} + + +wait_for_it <- function() { + while (!later::loop_empty()) { + later::run_now() + } +} + + +# Block until the promise is resolved/rejected. If resolved, return the value. +# If rejected, throw (yes throw, not return) the error. +extract <- function(promise) { + promise_value <- NULL + error <- NULL + promise %...>% + (function(value) promise_value <<- value) %...!% + (function(reason) error <<- reason) + + wait_for_it() + if (!is.null(error)) + stop(error) + else + promise_value +} + + +# Make an HTTP request using curl. +fetch <- function(url, handle = new_handle()) { + p <- curl_fetch_async(url, handle = handle) + extract(p) +} + +# Make an HTTP request using a socketConnection. Not as robust as fetch(), so +# we'll use this only when necessary. +http_request_con <- function(request, host, port) { + p <- http_request_con_async(request, host, port) + extract(p) +} + + +local_url <- function(path, port) { + stopifnot(grepl("^/", path)) + paste0("http://127.0.0.1:", port, path) +} + +parse_http_date <- function(x) { + strptime(x, format = "%a, %d %b %Y %H:%M:%S GMT", tz = "GMT") +} + +raw_file_content <- function(filename) { + size <- file.info(filename)$size + readBin(filename, "raw", n = size) +} diff --git a/tests/testthat/test-app.R b/tests/testthat/test-app.R new file mode 100644 index 00000000..30829536 --- /dev/null +++ b/tests/testthat/test-app.R @@ -0,0 +1,46 @@ +context("basic") + +test_that("Basic functionality", { + s1 <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 200L, + headers = list('Content-Type' = 'text/html'), + body = "server 1" + ) + } + ) + ) + expect_equal(length(listServers()), 1) + + s2 <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 200L, + headers = list('Content-Type' = 'text/html'), + body = "server 2" + ) + } + ) + ) + expect_equal(length(listServers()), 2) + + r1 <- fetch(local_url("/", s1$getPort())) + r2 <- fetch(local_url("/", s2$getPort())) + + expect_equal(r1$status_code, 200) + expect_equal(r2$status_code, 200) + + expect_identical(rawToChar(r1$content), "server 1") + expect_identical(rawToChar(r2$content), "server 2") + + expect_identical(parse_headers_list(r1$headers)$`content-type`, "text/html") + expect_identical(parse_headers_list(r1$headers)$`content-length`, "8") + + s1$stop() + expect_equal(length(listServers()), 1) + stopAllServers() + expect_equal(length(listServers()), 0) +}) diff --git a/tests/testthat/test-static-paths.R b/tests/testthat/test-static-paths.R new file mode 100644 index 00000000..9d0bda46 --- /dev/null +++ b/tests/testthat/test-static-paths.R @@ -0,0 +1,541 @@ +context("static") + +index_file_content <- raw_file_content(test_path("apps/content/index.html")) +subdir_index_file_content <- raw_file_content(test_path("apps/content/subdir/index.html")) +index_file_1_content <- raw_file_content(test_path("apps/content_1/index.html")) + +test_that("Basic static file serving", { + s <- startServer("127.0.0.1", random_open_port(), + list( + staticPaths = list( + # Testing out various leading and trailing slashes + "/" = test_path("apps/content"), + "/1" = test_path("apps/content"), + "/2/" = test_path("apps/content/"), + "3" = test_path("apps/content"), + "4/" = test_path("apps/content/") + ), + staticPathOptions = staticPathOptions( + headers = list("Test-Code-Path" = "C++") + ) + ) + ) + on.exit(s$stop()) + + # Fetch index.html + r <- fetch(local_url("/", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) + + # index.html for subdirectory + r <- fetch(local_url("/subdir", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, subdir_index_file_content) + + h <- parse_headers_list(r$headers) + expect_equal(as.integer(h$`content-length`), length(index_file_content)) + expect_equal(as.integer(h$`content-length`), length(r$content)) + expect_identical(h$`content-type`, "text/html; charset=utf-8") + expect_identical(h$`test-code-path`, "C++") + # Check that response time is within 1 minute of now. (Possible DST problems?) + expect_true(abs(as.numeric(parse_http_date(h$date)) - as.numeric(Sys.time())) < 60) + + + # Testing index for other paths + r1 <- fetch(local_url("/1", s$getPort())) + h1 <- parse_headers_list(r1$headers) + expect_identical(r$content, r1$content) + expect_identical(h$`content-length`, h1$`content-length`) + expect_identical(h$`content-type`, h1$`content-type`) + + r2 <- fetch(local_url("/1/", s$getPort())) + h2 <- parse_headers_list(r2$headers) + expect_identical(r$content, r2$content) + expect_identical(h$`content-length`, h2$`content-length`) + expect_identical(h$`content-type`, h2$`content-type`) + + r3 <- fetch(local_url("/1/index.html", s$getPort())) + h3 <- parse_headers_list(r3$headers) + expect_identical(r$content, r3$content) + expect_identical(h$`content-length`, h3$`content-length`) + expect_identical(h$`content-type`, h3$`content-type`) + + # Missing file (404) + r <- fetch(local_url("/foo", s$getPort())) + h <- parse_headers_list(r$headers) + expect_identical(rawToChar(r$content), "404 Not Found\n") + expect_equal(h$`content-length`, "14") + + # MIME types for other files + r <- fetch(local_url("/mtcars.csv", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(h$`content-type`, "text/csv") + + r <- fetch(local_url("/data.txt", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(h$`content-type`, "text/plain") +}) + + +test_that("Missing file fallthrough", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + return(list( + status = 404, + headers = list("Test-Code-Path" = "R"), + body = paste0("404 file not found: ", req$PATH_INFO) + )) + }, + staticPaths = list( + # Testing out various leading and trailing slashes + "/" = staticPath( + test_path("apps/content"), + indexhtml = FALSE, + fallthrough = TRUE + ) + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 404) + expect_identical(h$`test-code-path`, "R") + expect_identical(rawToChar(r$content), "404 file not found: /") +}) + + +test_that("Longer paths override shorter ones", { + s <- startServer("127.0.0.1", random_open_port(), + list( + staticPaths = list( + # Testing out various leading and trailing slashes + "/" = test_path("apps/content"), + "/a" = staticPath( + test_path("apps/content"), + indexhtml = FALSE + ), + "/a/b" = staticPath( + test_path("apps/content"), + indexhtml = NULL + ), + "/a/b/c" = staticPath( + test_path("apps/content"), + indexhtml = TRUE + ) + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) + + r <- fetch(local_url("/a/", s$getPort())) + expect_equal(r$status_code, 404) + + # When NULL, option values are not inherited from the parent dir, "/a"; + # they're inherited from the overall options for the app. + r <- fetch(local_url("/a/b", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) + + r <- fetch(local_url("/a/b/c", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) +}) + + +test_that("Options and option inheritance", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + return(list( + status = 404, + headers = list("Test-Code-Path" = "R"), + body = paste0("404 file not found: ", req$PATH_INFO) + )) + }, + staticPaths = list( + "/default" = staticPath(test_path("apps/content")), + # This path overrides options + "/override" = staticPath( + test_path("apps/content"), + indexhtml = FALSE, + fallthrough = TRUE, + html_charset = "ISO-8859-1", + headers = list("Test-Code-Path" = "C++2") + ), + # This path unsets some options + "/unset" = staticPath( + test_path("apps/content"), + html_charset = character(), + headers = list() + ) + ), + staticPathOptions = staticPathOptions( + indexhtml = TRUE, + fallthrough = FALSE, + headers = list("Test-Code-Path" = "C++") + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/default", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`content-type`, "text/html; charset=utf-8") + expect_identical(h$`test-code-path`, "C++") + expect_identical(r$content, index_file_content) + + r <- fetch(local_url("/override", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 404) + expect_identical(h$`test-code-path`, "R") + expect_identical(rawToChar(r$content), "404 file not found: /override") + + r <- fetch(local_url("/override/index.html", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`test-code-path`, "C++2") + expect_identical(h$`content-type`, "text/html; charset=ISO-8859-1") + + r <- fetch(local_url("/unset", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_false("test-code-path" %in% names(h)) + expect_identical(h$`content-type`, "text/html") + expect_identical(r$content, index_file_content) + + r <- fetch(local_url("/unset/index.html", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_false("test-code-path" %in% names(h)) + expect_identical(h$`content-type`, "text/html") + expect_identical(r$content, index_file_content) +}) + + +test_that("Header validation", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + if (!identical(req$HTTP_TEST_VALIDATION, "aaa")) { + return(list( + status = 403, + headers = list("Test-Code-Path" = "R"), + body = "403 Forbidden\n" + )) + } + return(list( + status = 200, + headers = list("Test-Code-Path" = "R"), + body = "200 OK\n" + )) + }, + staticPaths = list( + "/default" = staticPath(test_path("apps/content")), + # This path overrides validation + "/override" = staticPath( + test_path("apps/content"), + validation = c('"Test-Validation-1" == "bbb"') + ), + # This path unsets validation + "/unset" = staticPath( + test_path("apps/content"), + validation = character() + ), + # Fall through to R + "/fallthrough" = staticPath( + test_path("apps/content"), + fallthrough = TRUE + ) + ), + staticPathOptions = staticPathOptions( + headers = list("Test-Code-Path" = "C++"), + validation = c('"Test-Validation" == "aaa"') + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/default", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 403) + # This header doesn't get set. Should it? + expect_false("test-code-path" %in% names(h)) + expect_identical(rawToChar(r$content), "403 Forbidden\n") + + r <- fetch(local_url("/default", s$getPort()), + handle_setheaders(new_handle(), "test-validation" = "aaa")) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`test-code-path`, "C++") + expect_identical(r$content, index_file_content) + + # Check case insensitive + r <- fetch(local_url("/default", s$getPort()), + handle_setheaders(new_handle(), "tesT-ValidatioN" = "aaa")) + expect_equal(r$status_code, 200) + + r <- fetch(local_url("/unset", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`test-code-path`, "C++") + expect_identical(r$content, index_file_content) + + # When fallthrough=TRUE, the header validation is still checked before falling + # through to the R code path. + r <- fetch(local_url("/fallthrough/missingfile", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 403) + # This header doesn't get set. Should it? + expect_false("test-code-path" %in% names(h)) + expect_identical(rawToChar(r$content), "403 Forbidden\n") + + r <- fetch(local_url("/fallthrough/missingfile", s$getPort()), + handle_setheaders(new_handle(), "test-validation" = "aaa")) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`test-code-path`, "R") + expect_identical(rawToChar(r$content), "200 OK\n") +}) + + +test_that("Dynamically changing paths", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 500, + headers = list("Test-Code-Path" = "R"), + body = "500 Internal Server Error\n" + ) + }, + staticPaths = list( + "/static" = test_path("apps/content") + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) + + # Replace with different static path and options + s$setStaticPath( + "/static" = staticPath( + test_path("apps/content_1"), + indexhtml = FALSE + ) + ) + + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 404) + + r <- fetch(local_url("/static/index.html", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_1_content) + + # Remove static path + s$removeStaticPath("/static") + + expect_equal(length(s$getStaticPaths()), 0) + + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 500) + h <- parse_headers_list(r$headers) + expect_identical(h$`test-code-path`, "R") + expect_identical(rawToChar(r$content), "500 Internal Server Error\n") + + # Add static path + s$setStaticPath( + "/static_new" = test_path("apps/content") + ) + r <- fetch(local_url("/static_new", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) +}) + + +test_that("Dynamically changing options", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 500, + headers = list("Test-Code-Path" = "R"), + body = "500 Internal Server Error\n" + ) + }, + staticPaths = list( + "/static" = test_path("apps/content") + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 200) + + s$setStaticPathOption(indexhtml = FALSE) + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 404) + + s$setStaticPathOption(fallthrough = TRUE) + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 500) + + s$setStaticPathOption( + indexhtml = TRUE, + headers = list("Test-Headers" = "aaa"), + validation = c('"Test-Validation" == "aaa"') + ) + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 403) + r <- fetch(local_url("/static", s$getPort()), + handle_setheaders(new_handle(), "test-validation" = "aaa")) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`test-headers`, "aaa") + + # Unset some options + s$setStaticPathOption( + headers = list(), + validation = character() + ) + r <- fetch(local_url("/static", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_false("test-headers" %in% h) +}) + + +test_that("Escaped characters in paths", { + # Need to create files with weird names + static_dir <- tempfile("httpuv_test") + dir.create(static_dir) + cat("This is file content.\n", file = file.path(static_dir, "file with space.txt")) + on.exit(unlink(static_dir, recursive = TRUE)) + + + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 500, + headers = list("Test-Code-Path" = "R"), + body = "500 Internal Server Error\n" + ) + }, + staticPaths = list( + "/static" = static_dir + ) + ) + ) + on.exit(s$stop(), add = TRUE) + + r <- fetch(local_url("/static/file%20with%20space.txt", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(rawToChar(r$content), "This is file content.\n") +}) + + +test_that("Paths with ..", { + # TODO: Figure out how to send a request with .. + + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 404, + headers = list("Test-Code-Path" = "R"), + body = "404 Not Found\n" + ) + }, + staticPaths = list( + "/static" = test_path("apps/content") + ) + ) + ) + on.exit(s$stop()) + + res <- http_request_con("GET /", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 404 Not Found") + expect_true(any(grepl("^Test-Code-Path: R$", res, ignore.case = TRUE))) + + res <- http_request_con("GET /static", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 200 OK") + + # The presence of a ".." path segment results in a 400. + res <- http_request_con("GET /static/..", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 400 Bad Request") + + res <- http_request_con("GET /static/../", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 400 Bad Request") + + res <- http_request_con("GET /static/../static", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 400 Bad Request") + + # ".." is valid as part of a path segment (but we'll get 404's since the files + # don't actually exist). + res <- http_request_con("GET /static/..foo", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 404 Not Found") + expect_false(any(grepl("^Test-Code-Path: R$", res, ignore.case = TRUE))) + + res <- http_request_con("GET /static/foo..", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 404 Not Found") + expect_false(any(grepl("^Test-Code-Path: R$", res, ignore.case = TRUE))) + + res <- http_request_con("GET /static/foo../", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 404 Not Found") + expect_false(any(grepl("^Test-Code-Path: R$", res, ignore.case = TRUE))) +}) + + +test_that("HEAD, POST, PUT requests", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 404, + headers = list("Test-Code-Path" = "R"), + body = "404 Not Found\n" + ) + }, + staticPaths = list( + "/static" = test_path("apps/content") + ) + ) + ) + on.exit(s$stop()) + + # The GET results, for comparison to HEAD. + r_get <- fetch(local_url("/static", s$getPort())) + h_get <- parse_headers_list(r_get$headers) + + # HEAD is OK. + # Note the weird interface for a HEAD request: + # https://github.com/jeroen/curl/issues/24 + r <- fetch(local_url("/static", s$getPort()), new_handle(nobody = TRUE)) + expect_equal(r$status_code, 200) + expect_true(length(r$content) == 0) # No message body for HEAD + h <- parse_headers_list(r$headers) + # Headers should match GET request, except for date. + expect_identical(h[setdiff(names(h), "date")], h_get[setdiff(names(h_get), "date")]) + + # POST and PUT are not OK + r <- fetch(local_url("/static", s$getPort()), + handle_setopt(new_handle(), customrequest = "POST")) + expect_equal(r$status_code, 400) + + r <- fetch(local_url("/static", s$getPort()), + handle_setopt(new_handle(), customrequest = "PUT")) + expect_equal(r$status_code, 400) +}) + diff --git a/tests/testthat/test-traffic.R b/tests/testthat/test-traffic.R index 232a0dba..984e3dc4 100644 --- a/tests/testthat/test-traffic.R +++ b/tests/testthat/test-traffic.R @@ -11,29 +11,6 @@ skip_if_not_possible <- function() { } } -random_open_port <- function(min = 3000, max = 9000, n = 20) { - # Unsafe port list from shiny::runApp() - valid_ports <- setdiff(min:max, c(3659, 4045, 6000, 6665:6669, 6697)) - - # Try up to n ports - for (port in sample(valid_ports, n)) { - handle <- NULL - - # Check if port is open - tryCatch( - handle <- httpuv::startServer("127.0.0.1", port, list()), - error = function(e) { } - ) - if (!is.null(handle)) { - httpuv::stopServer(handle) - return(port) - } - } - - stop("Cannot find an available port") -} - - parse_ab_output <- function(p) { text <- readLines(p$get_output_file())