diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0da33f714e..f1afa9fd22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,10 +30,20 @@ jobs: os: 'ubuntu-latest' steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} - uses: julia-actions/julia-buildpkg@latest + - name: Generate a token that has read access to https://github.com/JuliaRegistries/user-blocklist-mock-for-testing + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.APP_ID_TEST_APP_USER_BLOCKLIST_MOCK }} + private-key: ${{ secrets.APP_PRIVATE_KEY_TEST_APP_USER_BLOCKLIST_MOCK }} + owner: JuliaRegistries + repositories: user-blocklist-mock-for-testing - name: Run the package tests run: | import Pkg @@ -47,7 +57,7 @@ jobs: Pkg.test(; allow_reresolve, coverage) end env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} shell: julia --color=yes --project {0} - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 @@ -59,6 +69,8 @@ jobs: contents: write # needed to be able to push to the gh-pages branch steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: julia-actions/setup-julia@v2 with: version: '1' diff --git a/run/config.commentbot.toml b/run/config.commentbot.toml index 50532aef27..8e494a102e 100644 --- a/run/config.commentbot.toml +++ b/run/config.commentbot.toml @@ -42,6 +42,21 @@ check_private_membership = false # If set to true Registrator will allow priv # Additional registries to look into for `[deps]` and `[compat]` registry_deps = ["https://github.com/JuliaRegistries/General"] +# -- Blocklist configuration (optional) -- +# Block specific users from using Registrator. The blocklist is fetched from a +# GitHub repo (can be private) containing a TOML file with blocked user entries. +# The github token above (commentbot.github.token) is used to authenticate when +# fetching the blocklist, so it must have read access to the blocklist repo. +# Use scripts/lookup_user_id.sh to find a user's immutable platform ID. +# +# blocklist_repo = "JuliaRegistries/user-blocklist" # owner/repo on GitHub +# blocklist_file = "blocklist.toml" # path within the repo +# blocklist_cache_ttl = 300 # seconds between refreshes +# blocklist_token = "" # optional: a GitHub PAT with read access to the + # blocklist repo. If not set, commentbot.github.token + # is used instead (it must then have read access to + # the blocklist repo). + [commentbot.github] user = "" # A GitHub user ID, can be same as that set for [regservice] email = "" # The email associated with the above user, this is diff --git a/run/config.web.toml b/run/config.web.toml index 51857fc38a..ee5bf86f3a 100644 --- a/run/config.web.toml +++ b/run/config.web.toml @@ -22,6 +22,21 @@ stop_file = "stopwebui" # allow_private = false # enable_logging = true +# -- Blocklist configuration (optional) -- +# Block specific users from using Registrator. The blocklist is fetched from a +# GitHub repo (can be private) containing a TOML file with blocked user entries. +# The github token below (web.github.token) is used to authenticate when +# fetching the blocklist, so it must have read access to the blocklist repo. +# Use scripts/lookup_user_id.sh to find a user's immutable platform ID. +# +# blocklist_repo = "JuliaRegistries/user-blocklist" # owner/repo on GitHub +# blocklist_file = "blocklist.toml" # path within the repo +# blocklist_cache_ttl = 300 # seconds between refreshes +# blocklist_token = "" # optional: a GitHub PAT with read access to the + # blocklist repo. If not set, web.github.token is + # used instead (it must then have read access to + # the blocklist repo). + [web.github] token = "" client_id = "" diff --git a/scripts/lookup_user_id.sh b/scripts/lookup_user_id.sh new file mode 100755 index 0000000000..396aa5537d --- /dev/null +++ b/scripts/lookup_user_id.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# +# Look up a user or organization's immutable platform ID for use in the +# Registrator blocklist. Works for both users and organizations. +# +# Usage: +# ./lookup_user_id.sh # defaults to github +# ./lookup_user_id.sh github +# ./lookup_user_id.sh gitlab +# ./lookup_user_id.sh bitbucket +# +# Requires: curl, grep, sed +# +# For private GitHub repos or to avoid rate limits, set GITHUB_TOKEN: +# GITHUB_TOKEN=ghp_... ./lookup_user_id.sh +# +# Note: On GitHub, the same endpoint works for both users and organizations, +# since they share the same ID namespace. + +set -euo pipefail + +usage() { + echo "Usage: $0 [github|gitlab|bitbucket]" + exit 1 +} + +[ $# -lt 1 ] && usage + +USERNAME="$1" +PROVIDER="${2:-github}" + +case "$PROVIDER" in + github) + AUTH_HEADER="" + if [ -n "${GITHUB_TOKEN:-}" ]; then + AUTH_HEADER="Authorization: token $GITHUB_TOKEN" + fi + + RESPONSE=$(curl -s ${AUTH_HEADER:+-H "$AUTH_HEADER"} \ + "https://api.github.com/users/${USERNAME}") + + ID=$(echo "$RESPONSE" | grep '"id":' | head -1 | sed 's/[^0-9]//g') + + if [ -z "$ID" ]; then + echo "Error: Could not find GitHub user '$USERNAME'" >&2 + echo "$RESPONSE" >&2 + exit 1 + fi + + echo "Provider: GitHub" + echo "Username: $USERNAME" + echo "ID: $ID" + echo "" + echo "Blocklist entry:" + echo "" + echo "[[blocked]]" + echo "provider = \"github\"" + echo "id = $ID" + echo "username = \"$USERNAME\"" + echo "reason = \"\"" + ;; + + gitlab) + RESPONSE=$(curl -s "https://gitlab.com/api/v4/users?username=${USERNAME}") + + ID=$(echo "$RESPONSE" | grep -o '"id":[0-9]*' | head -1 | sed 's/"id"://') + + if [ -z "$ID" ]; then + echo "Error: Could not find GitLab user '$USERNAME'" >&2 + echo "$RESPONSE" >&2 + exit 1 + fi + + echo "Provider: GitLab" + echo "Username: $USERNAME" + echo "ID: $ID" + echo "" + echo "Blocklist entry:" + echo "" + echo "[[blocked]]" + echo "provider = \"gitlab\"" + echo "id = $ID" + echo "username = \"$USERNAME\"" + echo "reason = \"\"" + ;; + + bitbucket) + RESPONSE=$(curl -s "https://api.bitbucket.org/2.0/users/${USERNAME}") + + UUID=$(echo "$RESPONSE" | grep -o '"uuid": *"[^"]*"' | head -1 | sed 's/"uuid": *"//;s/"//') + + if [ -z "$UUID" ]; then + echo "Error: Could not find Bitbucket user '$USERNAME'" >&2 + echo "$RESPONSE" >&2 + exit 1 + fi + + echo "Provider: Bitbucket" + echo "Username: $USERNAME" + echo "UUID: $UUID" + echo "" + echo "Blocklist entry:" + echo "" + echo "[[blocked]]" + echo "provider = \"bitbucket\"" + echo "id = \"$UUID\"" + echo "username = \"$USERNAME\"" + echo "reason = \"\"" + ;; + + *) + echo "Error: Unknown provider '$PROVIDER'. Use github, gitlab, or bitbucket." >&2 + exit 1 + ;; +esac diff --git a/src/Registrator.jl b/src/Registrator.jl index c2f39291c5..12581f02ab 100644 --- a/src/Registrator.jl +++ b/src/Registrator.jl @@ -43,6 +43,7 @@ RegistryTools.register(regp::RegisterParams) = RegistryTools.register(regp.packa include("pull_request.jl") include("Messaging.jl") +include("blocklist.jl") include("RegService.jl") include("commentbot/CommentBot.jl") include("webui/WebUI.jl") diff --git a/src/blocklist.jl b/src/blocklist.jl new file mode 100644 index 0000000000..eb73d4e957 --- /dev/null +++ b/src/blocklist.jl @@ -0,0 +1,143 @@ +module Blocklist + +using HTTP +using JSON +using Base64 +using Dates +using Logging +import Pkg: TOML + +export is_blocked, load_blocklist! + +# Maps provider name (e.g. "github") to Set of blocked ID strings. +const BLOCKED_IDS = Dict{String, Set{String}}() +const BLOCKLIST_LOCK = ReentrantLock() +const LAST_FETCH = Ref{DateTime}(DateTime(0)) + +""" + load_blocklist!(config::Dict) + +Fetch the blocklist from the configured GitHub repo and update the in-memory cache. +The blocklist file is expected to be TOML-formatted with a `[[blocked]]` array of tables, +each having an `id` field (the immutable platform ID) and a `provider` field. + +Entries can block individual users OR organizations/repo owners. GitHub users and orgs +share the same ID namespace, so an org ID works the same way as a user ID. Registrator +checks both the requesting user's ID and the repository owner's ID against the blocklist. + +Example blocklist.toml: + + # To find a GitHub user or org ID: curl https://api.github.com/users/NAME + # To find a GitLab user's ID: curl https://gitlab.com/api/v4/users?username=NAME + # To find a GitLab group's ID: curl https://gitlab.com/api/v4/groups/NAME + # To find a Bitbucket user's UUID: curl https://api.bitbucket.org/2.0/users/NAME + # + # Or use: scripts/lookup_user_id.sh NAME [github|gitlab|bitbucket] + + # Block a user + [[blocked]] + provider = "github" + id = 12345678 + username = "spammer1" # for human reference only + reason = "AI-generated spam" + + # Block an organization (prevents registration of any repo owned by this org) + [[blocked]] + provider = "github" + id = 87654321 + username = "spam-org" + reason = "organization used for spam packages" + +Falls back silently (fail-open) if the repo is unreachable or the file is malformed. +""" +function load_blocklist!(config::Dict) + repo = get(config, "blocklist_repo", "") + isempty(repo) && return + file = get(config, "blocklist_file", "blocklist.toml") + # Use a dedicated blocklist token if provided, otherwise fall back to the + # main GitHub token. The token must have read access to the blocklist repo. + token = get(config, "blocklist_token", "") + if isempty(token) + token = get(get(config, "github", Dict()), "token", "") + end + isempty(token) && return + + # Fetch outside the lock so that slow/stalled network requests don't block + # all is_blocked() callers. Only hold the lock for the in-memory swap. + new_blocked = nothing + try + headers = [ + "Authorization" => "Bearer $token", + "Accept" => "application/vnd.github.v3+json", + "User-Agent" => "Registrator.jl", + ] + url = "https://api.github.com/repos/$repo/contents/$file" + resp = HTTP.get(url; headers=headers, status_exception=false) + if resp.status != 200 + @warn "Failed to fetch blocklist" status=resp.status repo=repo file=file + return + end + data = JSON.parse(String(resp.body)) + content = String(base64decode(replace(get(data, "content", ""), "\n" => ""))) + toml = TOML.parse(content) + new_blocked = Dict{String, Set{String}}() + for entry in get(toml, "blocked", []) + id = get(entry, "id", nothing) + provider = get(entry, "provider", nothing) + (id === nothing || provider === nothing) && continue + provider_key = lowercase(string(provider)) + ids = get!(Set{String}, new_blocked, provider_key) + push!(ids, string(id)) + end + catch ex + # Log only the exception type and message, not the full exception object, + # because HTTP errors may include request headers containing the auth token. + @warn "Failed to load blocklist, allowing all users" error=sprint(showerror, ex) + return + end + + lock(BLOCKLIST_LOCK) do + empty!(BLOCKED_IDS) + merge!(BLOCKED_IDS, new_blocked) + LAST_FETCH[] = now(UTC) + end + total = sum(length, values(new_blocked); init=0) + @info "Blocklist loaded" count=total +end + +function maybe_refresh!(config::Dict) + repo = get(config, "blocklist_repo", "") + isempty(repo) && return + ttl = get(config, "blocklist_cache_ttl", 300) + if (now(UTC) - LAST_FETCH[]).value / 1000 > ttl + load_blocklist!(config) + end +end + +""" + is_blocked(provider::AbstractString, user_id, config::Dict) -> Bool + +Check whether a user ID on the given provider is on the blocklist. +`provider` should be `"github"`, `"gitlab"`, or `"bitbucket"`. +The `user_id` can be any type (integer, string, UUID) — it is converted to +a string for comparison. Refreshes the cached blocklist if the TTL has expired. +Returns `false` (fail-open) on any error or if the blocklist is not configured. +""" +function is_blocked(provider::AbstractString, user_id, config::Dict) + repo = get(config, "blocklist_repo", "") + isempty(repo) && return false + + maybe_refresh!(config) + + provider_key = lowercase(provider) + id_str = string(user_id) + blocked = lock(BLOCKLIST_LOCK) do + id_str in get(BLOCKED_IDS, provider_key, Set{String}()) + end + if blocked + @info "Blocked user attempted registration" provider=provider_key user_id=id_str + end + return blocked +end + +end # module diff --git a/src/commentbot/CommentBot.jl b/src/commentbot/CommentBot.jl index e9adb431a3..f50a647979 100644 --- a/src/commentbot/CommentBot.jl +++ b/src/commentbot/CommentBot.jl @@ -16,6 +16,8 @@ import RegistryTools: RegBranch, Project import Base: string using ..Messaging import ..RegisterParams +using ..Blocklist: is_blocked, load_blocklist! +import ..Blocklist include("trigger_types.jl") include("parse_comment.jl") @@ -274,6 +276,7 @@ function main(config::AbstractString=isempty(ARGS) ? "config.toml" : first(ARGS) end zsock = RequestSocket(get(CONFIG, "backend_port", 5555)) + Blocklist.load_blocklist!(CONFIG) @info("Starting server...") t1 = @async request_processor(zsock) t2 = @async status_monitor(CONFIG["stop_file"], event_queue, httpsock) diff --git a/src/commentbot/github_utils.jl b/src/commentbot/github_utils.jl index 3cbfc0665b..fa4d9436d5 100644 --- a/src/commentbot/github_utils.jl +++ b/src/commentbot/github_utils.jl @@ -154,6 +154,20 @@ function get_user_login(payload::Dict{<:AbstractString}) end end +function get_user_id(payload::Dict{<:AbstractString}) + if haskey(payload, "comment") + return payload["comment"]["user"]["id"] + elseif haskey(payload, "issue") + return payload["issue"]["user"]["id"] + elseif haskey(payload, "pull_request") + return payload["pull_request"]["user"]["id"] + else + error("Don't know how to get user id") + end +end + +get_repo_owner_id(payload::Dict{<:AbstractString}) = payload["repository"]["owner"]["id"] + function get_body(payload::Dict{<:AbstractString}) if haskey(payload, "comment") return payload["comment"]["body"] diff --git a/src/commentbot/param_types.jl b/src/commentbot/param_types.jl index e2494e9756..61b36fc92b 100644 --- a/src/commentbot/param_types.jl +++ b/src/commentbot/param_types.jl @@ -39,7 +39,15 @@ struct RequestParams{T<:RequestTrigger} subdir = get(action_kwargs, :subdir, "") target = get(action_kwargs, :target, nothing) - if evt.payload["repository"]["private"] && get(CONFIG, "disable_private_registrations", true) + if is_blocked("github", get_user_id(evt.payload), CONFIG) + err = "**Register Failed**\n$(mention(user)), you are not allowed to use Registrator." + @debug(err) + report_error = true + elseif is_blocked("github", get_repo_owner_id(evt.payload), CONFIG) + err = "**Register Failed**\nThe owner of this repository is not allowed to use Registrator." + @debug(err) + report_error = true + elseif evt.payload["repository"]["private"] && get(CONFIG, "disable_private_registrations", true) err = "Private package registration request received, ignoring" @debug(err) elseif action_name == "register" diff --git a/src/webui/WebUI.jl b/src/webui/WebUI.jl index 3d4851ee17..e4ace76283 100644 --- a/src/webui/WebUI.jl +++ b/src/webui/WebUI.jl @@ -18,6 +18,8 @@ using URIs using ..Messaging import ..RegisterParams +using ..Blocklist: is_blocked, load_blocklist! +import ..Blocklist # currently used only in routes/bitbucket.jl struct Route{Forge, Service} end @@ -266,6 +268,7 @@ function main(config::AbstractString=isempty(ARGS) ? "config.toml" : first(ARGS) end end + Blocklist.load_blocklist!(CONFIG) init_providers() init_registry() diff --git a/src/webui/gitutils.jl b/src/webui/gitutils.jl index 4c4b8e73d0..013deffa62 100644 --- a/src/webui/gitutils.jl +++ b/src/webui/gitutils.jl @@ -73,6 +73,29 @@ end is_success(res::AuthSuccess) = true is_success(res::AuthFailure) = false +# Extract the immutable user ID and provider name for blocklist checking. +# GitHub and GitLab use numeric IDs; Bitbucket uses a UUID string. +get_user_id(u::User{GitHub.User}) = u.user.id +get_user_id(u::User{GitLab.User}) = u.user.id +get_user_id(u::User{Bitbucket.User}) = u.user.uuid +get_user_id(u::User) = nothing + +get_provider_name(::User{GitHub.User}) = "github" +get_provider_name(::User{GitLab.User}) = "gitlab" +get_provider_name(::User{Bitbucket.User}) = "bitbucket" +get_provider_name(::User) = nothing + +# Extract the immutable owner/org ID from a repository for blocklist checking. +get_repo_owner_id(repo::GitHub.Repo) = repo.owner.id +get_repo_owner_id(repo::GitLab.Project) = repo.namespace.id +get_repo_owner_id(repo::Bitbucket.Repo) = repo.workspace.uuid +get_repo_owner_id(::Any) = nothing + +get_repo_provider_name(::GitHub.Repo) = "github" +get_repo_provider_name(::GitLab.Project) = "gitlab" +get_repo_provider_name(::Bitbucket.Repo) = "bitbucket" +get_repo_provider_name(::Any) = nothing + # Check for a user's authorization to release a package. # The criteria is simply whether the user is a collaborator for user-owned repos, # or whether they're an organization member or collaborator for organization-owned repos. @@ -286,7 +309,7 @@ function withpasswd(func, url::URI) mktemp() do path, io # base64 encode now and decode while printing out in shell to avoid shell injection encoded_user = base64encode(user) - encoded_passwd = base64encode(passwd) + encoded_passwd = base64encode(passwd) print(io, """ #!/bin/sh case "\$1" in diff --git a/src/webui/routes/register.jl b/src/webui/routes/register.jl index 1f57ee272d..c4d9318971 100644 --- a/src/webui/routes/register.jl +++ b/src/webui/routes/register.jl @@ -27,10 +27,25 @@ function register(r::HTTP.Request) notes = get(form, "notes", "") subdir = get(form, "subdir", "") + # Check blocklist before proceeding. + uid = get_user_id(u) + prov = get_provider_name(u) + if uid !== nothing && prov !== nothing && is_blocked(prov, uid, CONFIG) + return json(403; error="You are not allowed to use Registrator.") + end + # Get the repo, then check for authorization. owner, name = splitrepo(package) repo = getrepo(u.forge, owner, name) repo === nothing && return json(400; error="Repository was not found") + + # Check if the repo owner/org is blocked. + owner_id = get_repo_owner_id(repo) + repo_prov = get_repo_provider_name(repo) + if owner_id !== nothing && repo_prov !== nothing && is_blocked(repo_prov, owner_id, CONFIG) + return json(403; error="The owner of this repository is not allowed to use Registrator.") + end + auth_result = isauthorized(u, repo) if !is_success(auth_result) return json(400; error="Unauthorized to release this package. Reason: $(auth_result.reason)") diff --git a/test/blocklist.jl b/test/blocklist.jl new file mode 100644 index 0000000000..4404dd8781 --- /dev/null +++ b/test/blocklist.jl @@ -0,0 +1,143 @@ +using Registrator.Blocklist: is_blocked, load_blocklist!, BLOCKED_IDS, LAST_FETCH, BLOCKLIST_LOCK +using Registrator.WebUI: get_repo_owner_id, get_repo_provider_name +using Dates + +@testset "Blocklist" begin + @testset "disabled when no blocklist_repo configured" begin + config = Dict{String,Any}() + @test !is_blocked("github", 12345, config) + + config["blocklist_repo"] = "" + @test !is_blocked("github", 12345, config) + end + + @testset "is_blocked checks against correct provider" begin + config = Dict{String,Any}( + "blocklist_repo" => "JuliaRegistries/RegistratorBlocklist", + "blocklist_cache_ttl" => 99999, + "github" => Dict("token" => "fake"), + ) + lock(BLOCKLIST_LOCK) do + empty!(BLOCKED_IDS) + BLOCKED_IDS["github"] = Set(["12345", "67890"]) + BLOCKED_IDS["gitlab"] = Set(["11111"]) + BLOCKED_IDS["bitbucket"] = Set(["{abc-uuid}"]) + end + LAST_FETCH[] = now(Dates.UTC) + + # GitHub IDs only match github provider + @test is_blocked("github", 12345, config) + @test is_blocked("github", "12345", config) + @test is_blocked("github", 67890, config) + @test !is_blocked("github", 99999, config) + @test !is_blocked("gitlab", 12345, config) # same ID, wrong provider + @test !is_blocked("bitbucket", 12345, config) + + # GitLab IDs only match gitlab provider + @test is_blocked("gitlab", 11111, config) + @test !is_blocked("github", 11111, config) + + # Bitbucket UUIDs only match bitbucket provider + @test is_blocked("bitbucket", "{abc-uuid}", config) + @test !is_blocked("github", "{abc-uuid}", config) + + # Case-insensitive provider matching + @test is_blocked("GitHub", 12345, config) + @test is_blocked("GITLAB", 11111, config) + end + + @testset "org/owner IDs use the same mechanism as user IDs" begin + config = Dict{String,Any}( + "blocklist_repo" => "JuliaRegistries/RegistratorBlocklist", + "blocklist_cache_ttl" => 99999, + "github" => Dict("token" => "fake"), + ) + # Simulate blocking an org (GitHub org ID 743164 = JuliaLang) + lock(BLOCKLIST_LOCK) do + empty!(BLOCKED_IDS) + BLOCKED_IDS["github"] = Set(["743164"]) + end + LAST_FETCH[] = now(Dates.UTC) + + # The org ID is checked via the same is_blocked function + @test is_blocked("github", 743164, config) + # A user ID that isn't blocked should still pass + @test !is_blocked("github", 999999, config) + end + + @testset "get_repo_owner_id helpers" begin + # Verify that the helpers return the expected types for use with is_blocked. + # We can't easily construct full repo objects without API calls, + # but we can verify the fallback returns nothing. + @test get_repo_owner_id("not a repo") === nothing + @test get_repo_provider_name("not a repo") === nothing + end + + @testset "load_blocklist! is no-op without config" begin + config = Dict{String,Any}() + lock(BLOCKLIST_LOCK) do + empty!(BLOCKED_IDS) + end + load_blocklist!(config) + lock(BLOCKLIST_LOCK) do + @test isempty(BLOCKED_IDS) + end + + # No token + config["blocklist_repo"] = "Org/Repo" + load_blocklist!(config) + lock(BLOCKLIST_LOCK) do + @test isempty(BLOCKED_IDS) + end + end + + @testset "live fetch from JuliaRegistries/user-blocklist-test-for-mocking" begin + token = get(ENV, "GITHUB_TOKEN", "") + if isempty(token) + @warn "Skipping live blocklist test: GITHUB_TOKEN not set" + @test_skip false + else + # Clear any prior state + lock(BLOCKLIST_LOCK) do + empty!(BLOCKED_IDS) + end + LAST_FETCH[] = DateTime(0) + + config = Dict{String,Any}( + "blocklist_repo" => "JuliaRegistries/user-blocklist-mock-for-testing", + "blocklist_file" => "banlist.toml", + "github" => Dict("token" => token), + ) + + # Wrap in try/catch so a malformed response never leaks raw content + # (IDs, usernames, or tokens) into test output. + try + load_blocklist!(config) + catch ex + @error "load_blocklist! threw unexpectedly" exception_type=typeof(ex) + @test false # fail without printing exception details + end + + # Verify the blocklist loaded at least one entry + count = lock(BLOCKLIST_LOCK) do + sum(length, values(BLOCKED_IDS); init=0) + end + @test count > 0 + + # DilumAluthgeBot (GitHub user ID 43731525) should be blocked. + @test is_blocked("github", 43731525, config) + + # A user that is definitely not on the blocklist + @test !is_blocked("github", 1, config) + + # Cross-provider: same ID on a different provider should not match + @test !is_blocked("gitlab", 43731525, config) + end + + # Clean up global state so other tests aren't affected + lock(BLOCKLIST_LOCK) do + empty!(BLOCKED_IDS) + end + LAST_FETCH[] = DateTime(0) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index e4a4b54217..600a3c5844 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -69,6 +69,10 @@ function mock_provider!() end @testset "Registrator" begin + @testset "blocklist" begin + include("blocklist.jl") + end + @testset "server" begin include("server.jl") end