tests/testthat/test-util_scopes.R

fake_gh_error = function(status, message, doc = NULL, url = NULL, headers = list()) {
  body = character()
  if (!is.null(url))
    body = c(body, x = paste0("URL not found: <", url, ">"))
  if (!is.null(doc))
    body = c(body, i = paste0("Read more at <", doc, ">"))

  content = list(message = message, status = as.character(status))
  if (!is.null(doc))
    content[["documentation_url"]] = doc

  e = structure(
    list(
      message = paste0("GitHub API error (", status, "):  ", message),
      body = body,
      response_headers = headers,
      response_content = content
    ),
    class = c("github_error", paste0("http_error_", status), "rlang_error", "error", "condition")
  )

  list(result = NULL, error = e)
}

fake_token = function(chr) {
  paste0("ghp_", strrep(chr, 36))
}

test_that("parse_scopes() splits header values", {
  expect_equal(parse_scopes(NULL), character())
  expect_equal(parse_scopes(""), character())
  expect_equal(parse_scopes("repo"), "repo")
  expect_equal(parse_scopes("gist, read:org, repo, workflow"), c("gist", "read:org", "repo", "workflow"))
})

test_that("expand_scopes() includes implied scopes", {
  expect_equal(expand_scopes(character()), character())
  expect_true(all(c("admin:org", "write:org", "read:org") %in% expand_scopes("admin:org")))
  expect_true("read:org" %in% expand_scopes("write:org"))
  expect_false("write:org" %in% expand_scopes("read:org"))
  expect_true(all(c("repo", "public_repo") %in% expand_scopes("repo")))
  expect_equal(expand_scopes("workflow"), "workflow")
})

test_that("ghclass_scopes includes independently required classic scopes", {
  expect_setequal(
    names(ghclass_scopes),
    c("repo", "admin:org", "workflow", "notifications", "delete_repo")
  )
})

test_that("token_type() recognizes token prefixes", {
  expect_equal(token_type("ghp_abc"), "classic personal access token")
  expect_equal(token_type("github_pat_abc"), "fine-grained personal access token")
  expect_equal(token_type("gho_abc"), "OAuth access token")
  expect_equal(token_type("ghu_abc"), "GitHub App user access token")
  expect_equal(token_type("ghs_abc"), "GitHub App installation access token")
  expect_equal(token_type("0123456789abcdef"), "unknown")
})

test_that("token types unsupported by repository subscriptions are identified", {
  expect_setequal(
    repo_subscription_unsupported_token_types,
    c(
      "fine-grained personal access token",
      "GitHub App user access token",
      "GitHub App installation access token"
    )
  )
  expect_false("classic personal access token" %in% repo_subscription_unsupported_token_types)
  expect_false("OAuth access token" %in% repo_subscription_unsupported_token_types)
})

test_that("token_source() reports where a token came from", {
  withr::with_envvar(c(GITHUB_PAT = fake_token("a"), GITHUB_TOKEN = NA), {
    expect_equal(
      token_source(fake_token("a"), supplied = FALSE),
      "GITHUB_PAT environment variable"
    )
    expect_equal(token_source(fake_token("a"), supplied = TRUE), "supplied directly")
    expect_equal(token_source(fake_token("b")), "supplied directly")
  })

  withr::with_envvar(c(GITHUB_PAT = NA, GITHUB_TOKEN = fake_token("a")), {
    expect_equal(
      token_source(fake_token("a"), supplied = FALSE),
      "GITHUB_TOKEN environment variable"
    )
  })
})

test_that("missing_scope_hint() compares token and endpoint scopes", {
  expect_null(missing_scope_hint(NULL))
  expect_null(missing_scope_hint(list("x-accepted-oauth-scopes" = "admin:org")))
  expect_null(missing_scope_hint(list("x-oauth-scopes" = "repo", "x-accepted-oauth-scopes" = "")))
  expect_null(missing_scope_hint(list("x-oauth-scopes" = "admin:org", "x-accepted-oauth-scopes" = "read:org")))
  expect_null(missing_scope_hint(list(
    "x-oauth-scopes" = "admin:org, repo",
    "x-accepted-oauth-scopes" = "admin:org, read:org, repo, user, write:org"
  )))

  expect_equal(
    missing_scope_hint(list("x-oauth-scopes" = "gist, read:org, repo, workflow", "x-accepted-oauth-scopes" = "admin:org")),
    "admin:org (token has gist, read:org, repo, workflow)"
  )
  expect_equal(
    missing_scope_hint(list("x-oauth-scopes" = "gist", "x-accepted-oauth-scopes" = "admin:org, read:org")),
    "one of admin:org, read:org (token has gist)"
  )
  expect_equal(
    missing_scope_hint(list("x-oauth-scopes" = "", "x-accepted-oauth-scopes" = "repo")),
    "repo (token has none)"
  )
})

test_that("error_msg() extracts details from gh errors", {
  res = fake_gh_error(
    403, "You must be an org admin.", doc = "https://docs.github.com/x",
    headers = list("x-oauth-scopes" = "repo", "x-accepted-oauth-scopes" = "admin:org")
  )
  msg = error_msg(res)
  expect_equal(as.vector(msg), "GitHub API error (403): You must be an org admin.")
  expect_null(attr(msg, "msg"))
  expect_equal(attr(msg, "doc"), "https://docs.github.com/x")
  expect_null(attr(msg, "404"))
  expect_equal(attr(msg, "scopes"), "admin:org (token has repo)")

  res = fake_gh_error(
    404, "Not Found", doc = "https://docs.github.com/y", url = "https://api.github.com/orgs/nope",
    headers = list("x-oauth-scopes" = "repo", "x-accepted-oauth-scopes" = "admin:org, repo")
  )
  msg = error_msg(res)
  expect_equal(as.vector(msg), "GitHub API error (404): Not Found")
  expect_equal(attr(msg, "404"), "https://api.github.com/orgs/nope")
  expect_null(attr(msg, "scopes"))
})

test_that("error_msg() only suggests scope mismatches for authorization-like errors", {
  headers = list("x-oauth-scopes" = "repo", "x-accepted-oauth-scopes" = "admin:org")

  not_found = error_msg(fake_gh_error(404, "Not Found", headers = headers))
  expect_equal(attr(not_found, "scopes"), "admin:org (token has repo)")

  server_error = error_msg(fake_gh_error(500, "Internal Server Error", headers = headers))
  expect_null(attr(server_error, "scopes"))
})

test_that("error_msg() parses legacy multiline gh messages", {
  e = structure(
    list(message = paste0(
      "GitHub API error (404): Not Found\n",
      "  Message: Not Found\n",
      "  URL not found: https://api.github.com/orgs/nope\n",
      "  Read more at https://docs.github.com/y"
    )),
    class = c("github_error", "http_error_404", "error", "condition")
  )
  msg = error_msg(list(result = NULL, error = e))
  expect_equal(as.vector(msg), "GitHub API error (404): Not Found")
  expect_null(attr(msg, "msg"))
  expect_equal(attr(msg, "doc"), "https://docs.github.com/y")
  expect_equal(attr(msg, "404"), "https://api.github.com/orgs/nope")
  expect_null(attr(msg, "scopes"))
})

test_that("error_msg() passes through non-API errors", {
  res = purrr::safely(stop)("boom")
  expect_equal(error_msg(res), "boom")
  expect_equal(error_bullets(res), c(x = "boom"))
})

test_that("error_msg_tree() and error_bullets() include the details", {
  local_reproducible_output()

  res = fake_gh_error(
    403, "Forbidden.", doc = "https://docs.github.com/x",
    headers = list("x-oauth-scopes" = "repo", "x-accepted-oauth-scopes" = "admin:org")
  )

  tree = error_msg_tree(error_msg(res))
  expect_true(any(grepl("GitHub API error (403): Forbidden.", tree, fixed = TRUE)))
  expect_true(any(grepl("API docs: https://docs.github.com/x", tree, fixed = TRUE)))
  expect_true(any(grepl("Possible missing scope: admin:org (token has repo)", tree, fixed = TRUE)))

  bullets = error_bullets(res)
  expect_equal(names(bullets), c("x", "i", "i"))
  expect_equal(bullets[["x"]], "GitHub API error (403): Forbidden.")
  expect_true(any(grepl("API docs: {.url https://docs.github.com/x}", bullets, fixed = TRUE)))
  expect_true(any(grepl("Possible missing scope: admin:org (token has repo)", bullets, fixed = TRUE)))
})

test_that("error_bullets() escapes braces for cli", {
  res = fake_gh_error(422, "Bad {thing}")
  bullets = error_bullets(res)
  expect_equal(bullets[["x"]], "GitHub API error (422): Bad {{thing}}")
  expect_equal(cli::format_inline(bullets[["x"]]), "GitHub API error (422): Bad {thing}")
})

Try the ghclass package in your browser

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

ghclass documentation built on Aug. 31, 2026, 5:08 p.m.