From 85b75108b47f6d7588b363a1033433eb9a618053 Mon Sep 17 00:00:00 2001 From: Colin Gillespie Date: Fri, 16 Oct 2026 11:28:55 +0100 Subject: [PATCH 1/5] chore: Test every header summary offline Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QVzHxqR2weizVuCKLw3fZm --- tests/testthat/helper-header_summary.R | 4 ++ tests/testthat/test-check.R | 19 +++-- tests/testthat/test-check_server_headers.R | 16 +++-- tests/testthat/test-check_status_code.R | 11 +-- tests/testthat/test-header_summary.R | 80 ++++++++++++++++++++++ 5 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 tests/testthat/helper-header_summary.R diff --git a/tests/testthat/helper-header_summary.R b/tests/testthat/helper-header_summary.R new file mode 100644 index 0000000..1acf4f6 --- /dev/null +++ b/tests/testthat/helper-header_summary.R @@ -0,0 +1,4 @@ +# Summarise a header value as get_response_headers() would pass it +summarise_header = function(value, header) { + header_summary(structure(value, class = header)) +} diff --git a/tests/testthat/test-check.R b/tests/testthat/test-check.R index 7ab3780..219482e 100644 --- a/tests/testthat/test-check.R +++ b/tests/testthat/test-check.R @@ -1,11 +1,10 @@ -test_that("Testing check", { - x = check("jumpingrivers.com") - expect_true(any(grepl(200, x$status_codes))) - expect_equal(ncol(x$headers), 6) - expect_gt(nrow(x$headers), 18) - - x = check("https://report-uri.com/") - expect_equal(x$status_codes, 200) - expect_equal(ncol(x$headers), 6) - expect_gt(nrow(x$headers), 18) +test_that("check() returns status codes and a header summary", { + skip_if_offline() + x = suppressMessages(check("jumpingrivers.com")) + expect_equal(x$status_codes[length(x$status_codes)], 200) + expect_named( + x$headers, + c("header", "status", "message", "value", "documentation", "primary_header") + ) + expect_true(all(c("ssl", "ssl-redirection", "strict-transport-security") %in% x$headers$header)) }) diff --git a/tests/testthat/test-check_server_headers.R b/tests/testthat/test-check_server_headers.R index 6e5259a..8af7e30 100644 --- a/tests/testthat/test-check_server_headers.R +++ b/tests/testthat/test-check_server_headers.R @@ -1,9 +1,15 @@ test_that("Is redirected works", { - res = httr::GET("neverssl.com") - expect_false(is_redirected(res)) - res = httr::GET("google.com") - expect_true(is_redirected(res)) - expect_true(is.data.frame(check("http://httpforever.com/")$headers)) + response = function(status, version = "HTTP/1.1") { + list(all_headers = purrr::map2(status, version, \(s, v) list(status = s, version = v))) + } + expect_false(is_redirected(response(200))) + expect_true(is_redirected(response(c(301, 200)))) + expect_true(is_redirected(response(200, "HTTP/2"))) +}) + +test_that("check() copes with a site that has no https", { + skip_if_offline() + expect_true(is.data.frame(suppressMessages(check("http://httpforever.com/"))$headers)) }) fake_response = function(type, body) { diff --git a/tests/testthat/test-check_status_code.R b/tests/testthat/test-check_status_code.R index 2fcb03a..1476188 100644 --- a/tests/testthat/test-check_status_code.R +++ b/tests/testthat/test-check_status_code.R @@ -1,6 +1,7 @@ -test_that("Test Status codes", { - status_codes = check_status_code("jumpingrivers.com") - expect_true(any(grepl(200, status_codes))) - status_codes = check_status_code("jumpingrivers.com/bad-page") - expect_true(any(grepl(404, status_codes))) +test_that("Status codes follow each redirect", { + skip_if_offline() + status_codes = suppressMessages(check_status_code("jumpingrivers.com")) + expect_equal(status_codes[length(status_codes)], 200) + status_codes = suppressMessages(check_status_code("jumpingrivers.com/bad-page")) + expect_equal(status_codes[length(status_codes)], 404) }) diff --git a/tests/testthat/test-header_summary.R b/tests/testthat/test-header_summary.R index 3ec832c..2e0313b 100644 --- a/tests/testthat/test-header_summary.R +++ b/tests/testthat/test-header_summary.R @@ -31,3 +31,83 @@ test_that("Header values are compared case-insensitively", { expect_equal(xss("1;mode=block")$status, "OK") expect_equal(xss("1")$status, "WARN") }) + +test_that("Unknown headers are reported but not judged", { + out = summarise_header("abc", "x-made-up") + expect_equal(out$header, "x-made-up") + expect_equal(out$status, "UNKNOWN") + expect_equal(out$value, "abc") +}) + +test_that("The scheme must be https", { + expect_equal(summarise_header("https", "scheme")$status, "OK") + expect_equal(summarise_header("http", "scheme")$status, "WARN") +}) + +test_that("Content-Type needs a charset", { + expect_equal(summarise_header("text/html; charset=utf-8", "content-type")$status, "OK") + expect_equal(summarise_header("text/html", "content-type")$status, "WARN") +}) + +test_that("Policy headers are noted when present", { + policies = c( + "report-to", + "nel", + "content-security-policy", + "content-security-policy-report-only", + "permissions-policy" + ) + for (policy in policies) { + out = summarise_header("some-policy", policy) + expect_equal(out$status, "OK", label = policy) + expect_equal(out$value, "some-policy", label = policy) + } + expect_equal(summarise_header("*", "access-control-allow-origin")$status, "NOTE") + expect_match(summarise_header("nginx", "server")$message, "nginx") +}) + +test_that("X-Frame-Options allows deny or sameorigin only", { + expect_equal(summarise_header("DENY", "x-frame-options")$status, "OK") + expect_equal(summarise_header("sameorigin", "x-frame-options")$status, "OK") + allow = summarise_header("allow-from https://example.com", "x-frame-options") + expect_equal(allow$status, "WARN") + expect_match(allow$message, "Other domains") + expect_match(summarise_header("yes", "x-frame-options")$message, "not found") +}) + +test_that("X-Permitted-Cross-Domain-Policies needs a known value", { + expect_equal(summarise_header("None", "x-permitted-cross-domain-policies")$status, "OK") + expect_equal(summarise_header("everything", "x-permitted-cross-domain-policies")$status, "WARN") +}) + +test_that("Headers that leak or are deprecated get a WARN", { + expect_equal(summarise_header("PHP/8.2", "x-powered-by")$status, "WARN") + expect_match(summarise_header("pin-sha256=x", "public-key-pins")$message, "deprecated") + expect_match(summarise_header("max-age=86400", "expect-ct")$message, "deprecated") +}) + +test_that("Cookies must all use the Secure flag", { + expect_equal(summarise_header(logical(0), "cookies")$message, "No cookies detected") + expect_equal(summarise_header(c(TRUE, TRUE), "cookies")$status, "OK") + expect_equal(summarise_header(c(TRUE, FALSE), "cookies")$status, "WARN") +}) + +test_that("Redirection is reported", { + expect_equal(summarise_header(TRUE, "redirection")$status, "OK") + expect_equal(summarise_header(FALSE, "redirection")$status, "WARN") +}) + +test_that("Subresource Integrity is checked on script tags", { + sri = function(value) summarise_header(value, "subresource-integrity") + expect_equal(sri(list())$status, "OK") + + hashed = c( + src = "https://cdn.example.com/a.js", + integrity = "sha384-x", + crossorigin = "anonymous" + ) + expect_equal(sri(list(hashed))$status, "OK") + + no_crossorigin = c(src = "https://cdn.example.com/b.js", integrity = "sha384-y") + expect_equal(sri(list(hashed, no_crossorigin))$status, "WARN") +}) From 1e75108d36b3f374abb0148eb07f676cddc9a2b8 Mon Sep 17 00:00:00 2001 From: Colin Gillespie Date: Fri, 16 Oct 2026 11:28:55 +0100 Subject: [PATCH 2/5] ci: Run R CMD check on GitHub Actions Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QVzHxqR2weizVuCKLw3fZm --- .Rbuildignore | 1 + .github/.gitignore | 1 + .github/workflows/R-CMD-check.yaml | 50 ++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 .github/.gitignore create mode 100644 .github/workflows/R-CMD-check.yaml diff --git a/.Rbuildignore b/.Rbuildignore index dc73d1e..2f32a2e 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -3,3 +3,4 @@ ^\.lintr$ ^air\.toml$ ^LICENSE\.md$ +^\.github$ diff --git a/.github/.gitignore b/.github/.gitignore new file mode 100644 index 0000000..2d19fc7 --- /dev/null +++ b/.github/.gitignore @@ -0,0 +1 @@ +*.html diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml new file mode 100644 index 0000000..73bfb21 --- /dev/null +++ b/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,50 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + push: + branches: [main, master] + pull_request: + +name: R-CMD-check.yaml + +permissions: read-all + +jobs: + R-CMD-check: + runs-on: ${{ matrix.config.os }} + + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + + strategy: + fail-fast: false + matrix: + config: + - {os: macos-latest, r: 'release'} + - {os: windows-latest, r: 'release'} + - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + - {os: ubuntu-latest, r: 'release'} + - {os: ubuntu-latest, r: 'oldrel-1'} + + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + R_KEEP_PKG_SOURCE: yes + + steps: + - uses: actions/checkout@v6 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ matrix.config.r }} + http-user-agent: ${{ matrix.config.http-user-agent }} + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::rcmdcheck + needs: check + + - uses: r-lib/actions/check-r-package@v2 + with: + upload-snapshots: true + build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' From 652eace3ef7cdbb1244aa6d4cd394588eb914ddb Mon Sep 17 00:00:00 2001 From: Colin Gillespie Date: Fri, 16 Oct 2026 11:28:56 +0100 Subject: [PATCH 3/5] chore: Bump version to 0.1.16 --- DESCRIPTION | 2 +- NEWS.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 5ffdec9..35579c1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: serverHeaders Title: Server Header Summary -Version: 0.1.15 +Version: 0.1.16 Authors@R: person("Jumping", "Rivers", , "info@jumpingrivers.com", role = c("aut", "cre")) Description: Checks and summarises the server headers for a given URL. diff --git a/NEWS.md b/NEWS.md index 5a9aa52..09d1b72 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +# serverHeaders 0.1.16 + +## Bug fixes and minor improvements + +* Every `header_summary()` method now has offline tests, and live tests skip when offline. +* R CMD check runs on GitHub Actions for pushes to main and pull requests. + # serverHeaders 0.1.15 ## Bug fixes and minor improvements From 071b44563202c1c7d55cc8ab9d8eb8d78fa53ddb Mon Sep 17 00:00:00 2001 From: Colin Gillespie Date: Fri, 16 Oct 2026 11:36:15 +0100 Subject: [PATCH 4/5] chore: Test the no-HTTPS SSL check offline Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QVzHxqR2weizVuCKLw3fZm --- tests/testthat/test-check_ssl_functionality.R | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test-check_ssl_functionality.R b/tests/testthat/test-check_ssl_functionality.R index d49073f..874bdd3 100644 --- a/tests/testthat/test-check_ssl_functionality.R +++ b/tests/testthat/test-check_ssl_functionality.R @@ -12,9 +12,12 @@ test_that("SSL errors report the real error and keep the value", { }) test_that("Sites without an SSL certificate get a WARN", { - skip_if_offline() - # neverssl.com never serves https - ssl = suppressMessages(check_ssl("neverssl.com")) + # What curl reports for a site that doesn't serve https + local_mocked_bindings( + HEAD = function(...) stop("SSL connect error"), + .package = "httr" + ) + ssl = suppressMessages(check_ssl("no-https.example.com")) expect_equal(ssl$status, "WARN") expect_equal(ssl$message, "SSL incorrect") expect_false(is.na(ssl$value)) From c43ff02ea7a0f40169ace1652b1cadc698f5b612 Mon Sep 17 00:00:00 2001 From: Colin Gillespie Date: Fri, 16 Oct 2026 12:01:42 +0100 Subject: [PATCH 5/5] ci: Skip live-site tests on CI Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QVzHxqR2weizVuCKLw3fZm --- NEWS.md | 2 +- tests/testthat/test-check.R | 1 + tests/testthat/test-check_server_headers.R | 1 + tests/testthat/test-check_status_code.R | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 09d1b72..b1c8aa2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,7 @@ ## Bug fixes and minor improvements -* Every `header_summary()` method now has offline tests, and live tests skip when offline. +* Every `header_summary()` method now has offline tests, and live tests skip when offline or on CI. * R CMD check runs on GitHub Actions for pushes to main and pull requests. # serverHeaders 0.1.15 diff --git a/tests/testthat/test-check.R b/tests/testthat/test-check.R index 219482e..233c1cf 100644 --- a/tests/testthat/test-check.R +++ b/tests/testthat/test-check.R @@ -1,5 +1,6 @@ test_that("check() returns status codes and a header summary", { skip_if_offline() + skip_on_ci() x = suppressMessages(check("jumpingrivers.com")) expect_equal(x$status_codes[length(x$status_codes)], 200) expect_named( diff --git a/tests/testthat/test-check_server_headers.R b/tests/testthat/test-check_server_headers.R index 8af7e30..1488f47 100644 --- a/tests/testthat/test-check_server_headers.R +++ b/tests/testthat/test-check_server_headers.R @@ -9,6 +9,7 @@ test_that("Is redirected works", { test_that("check() copes with a site that has no https", { skip_if_offline() + skip_on_ci() expect_true(is.data.frame(suppressMessages(check("http://httpforever.com/"))$headers)) }) diff --git a/tests/testthat/test-check_status_code.R b/tests/testthat/test-check_status_code.R index 1476188..9667f59 100644 --- a/tests/testthat/test-check_status_code.R +++ b/tests/testthat/test-check_status_code.R @@ -1,5 +1,6 @@ test_that("Status codes follow each redirect", { skip_if_offline() + skip_on_ci() status_codes = suppressMessages(check_status_code("jumpingrivers.com")) expect_equal(status_codes[length(status_codes)], 200) status_codes = suppressMessages(check_status_code("jumpingrivers.com/bad-page"))