Skip to content

bundler: Fix Bundler::Fetcher for PQC support, adding integration connection tests#9637

Open
junaruga wants to merge 3 commits into
ruby:masterfrom
junaruga:wip/bundler-add-pqc-integration-tests
Open

bundler: Fix Bundler::Fetcher for PQC support, adding integration connection tests#9637
junaruga wants to merge 3 commits into
ruby:masterfrom
junaruga:wip/bundler-add-pqc-integration-tests

Conversation

@junaruga

@junaruga junaruga commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

This PR is related to #9543. The first commit is the same with #9615. I want to see the #9615 is reviewed and merged. After that, I will rebase this PR on the latest master branch. The second commit is this PR's commit.

Created spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb adding non-PQC and PQC server/client connection integration tests.

As "Bundler::Fetcher local SSL server #connection PQC connects with client cert auth" failed with the following error due to hardcoded OpenSSL::PKey::RSA.new in Bundler::Fetcher#connection, fixed it to support ML-DSA ssl_client_cert.

$ bin/rspec spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb
...
Failures:

  1) Bundler::Fetcher local SSL server #connection PQC connects with client cert auth
     Failure/Error: fetcher = Bundler::Fetcher.new(remote)

     OpenSSL::PKey::PKeyError:
       incorrect pkey type: UNDEF
     # /home/jaruga/.local/ruby-4.1.0-debug-3ef48ef9c8-openssl-4.1.0-7194354488/lib/ruby/4.1.0+1/openssl/pkey.rb:394:in 'OpenSSL::PKey::RSA#initialize'
     # /home/jaruga/.local/ruby-4.1.0-debug-3ef48ef9c8-openssl-4.1.0-7194354488/lib/ruby/4.1.0+1/openssl/pkey.rb:394:in 'Class#new'
     # /home/jaruga/.local/ruby-4.1.0-debug-3ef48ef9c8-openssl-4.1.0-7194354488/lib/ruby/4.1.0+1/openssl/pkey.rb:394:in 'OpenSSL::PKey::RSA.new'
     # ./bundler/lib/bundler/fetcher.rb:321:in 'Bundler::Fetcher#connection'
     # ./bundler/lib/bundler/fetcher.rb:140:in 'Bundler::Fetcher#initialize'
     # ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:69:in 'RSpec::ExampleGroups::BundlerFetcherLocalSSLServer#fetch_path'
     # ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:60:in 'block (4 levels) in <top (required)>'
...

Created test/rubygems/local_ssl_server_utilities.rb to manage utility methods
called by RubyGems test-unit and Bundler rspec tests.

Co-Authored-By: Claude noreply@anthropic.com

Files

spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb

I aligned the file structure with test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb. I also referred to spec/bundler/fetcher/gem_remote_fetcher_spec.rb.

test/rubygems/local_ssl_server_utilities.rb

For the file's naming, I referred to test/rubygems/multifactor_auth_utilities.rb, there are following files as support files, I think test/rubygems/*_utilities.rb file is suitable in this case in my opinion.

test/rubygems/helper.rb
test/rubygems/utilities.rb
test/rubygems/multifactor_auth_utilities.rb
test/rubygems/mock_gem_ui.rb

What was the end-user or developer problem that led to this PR?

Bundler::Fetcher#connection doesn't work with ML-DSA ssl_client_cert due to hardcoded OpenSSL::PKey::RSA.new.

What is your fix for the problem, implemented in this PR?

Fixed the Bundler::Fetcher#connection to support ML-DSA ssl_client_cert.
Added integration HTTPS server/client connection tests.

Make sure the following tasks are checked

@junaruga

Copy link
Copy Markdown
Member Author

@junaruga junaruga marked this pull request as draft June 23, 2026 19:03
@junaruga

Copy link
Copy Markdown
Member Author

CI ruby-core cases failed.

I am testing by adding the 3rd commit.

@junaruga junaruga force-pushed the wip/bundler-add-pqc-integration-tests branch from 0284301 to e578fea Compare June 23, 2026 20:09
@junaruga

junaruga commented Jun 23, 2026

Copy link
Copy Markdown
Member Author

CI ruby-core cases failed.

I fixed the ruby-core cases.

In CI Bundler on macOS ruby-4.0, ruby-3.2 cases, the following non-PQC/PQC tests failed. Investigating.

https://github.com/ruby/rubygems/actions/runs/28050419021/job/83039282611?pr=9637#step:12:109

Failures:

  1) Bundler::Fetcher local SSL server #connection PQC connects
     Failure/Error: expect(response.code).to eq("200")

       expected: #<Encoding:UTF-8> "200"
            got: #<Encoding:ASCII-8BIT> "403"

       (compared using ==)
     # ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:49:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'

  2) Bundler::Fetcher local SSL server #connection PQC connects with client cert auth
     Failure/Error: expect(response.code).to eq("200")

       expected: #<Encoding:UTF-8> "200"
            got: #<Encoding:ASCII-8BIT> "403"

       (compared using ==)
     # ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:61:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'

  3) Bundler::Fetcher local SSL server #connection non-PQC connects with client cert auth
     Failure/Error: expect(response.code).to eq("200")

       expected: #<Encoding:UTF-8> "200"
            got: #<Encoding:ASCII-8BIT> "403"

       (compared using ==)
     # ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:35:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'

  4) Bundler::Fetcher local SSL server #connection non-PQC connects
     Failure/Error: expect(response.code).to eq("200")

       expected: #<Encoding:UTF-8> "200"
            got: #<Encoding:ASCII-8BIT> "403"

       (compared using ==)
     # ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:24:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'

Finished in 18 minutes 55 seconds (files took 0.49493 seconds to load)
3859 examples, 4 failures, 23 pending

Failed examples:

rspec ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:44 # Bundler::Fetcher local SSL server #connection PQC connects
rspec ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:52 # Bundler::Fetcher local SSL server #connection PQC connects with client cert auth
rspec ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:27 # Bundler::Fetcher local SSL server #connection non-PQC connects with client cert auth
rspec ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:19 # Bundler::Fetcher local SSL server #connection non-PQC connects

Error: Process completed with exit code 1.

@junaruga junaruga force-pushed the wip/bundler-add-pqc-integration-tests branch 5 times, most recently from eca36c3 to c245713 Compare June 24, 2026 17:41
@junaruga junaruga marked this pull request as ready for review June 25, 2026 12:50
@junaruga

Copy link
Copy Markdown
Member Author

@hsbt To investigate the above issue that the testing SSL server returning HTTP status 403, I ran the CI on my fork repository with the following change to check the details.

diff --git a/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb b/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb
index 91f0200558..5ded6c0fec 100644
--- a/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb
+++ b/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb
@@ -69,7 +69,13 @@ def fetch_path(uri)
     fetcher = Bundler::Fetcher.new(remote)

     connection = fetcher.send(:connection)
-    connection.request(uri)
+    response = connection.request(uri)
+    unless response.code == "200"
+      warn "DEBUG: uri=#{uri} code=#{response.code} body=#{response.body.inspect}"
+      warn "DEBUG: HTTP_PROXY=#{ENV["HTTP_PROXY"].inspect} HTTPS_PROXY=#{ENV["HTTPS_PROXY"].inspect}"
+      warn "DEBUG: http_proxy=#{ENV["http_proxy"].inspect} https_proxy=#{ENV["https_proxy"].inspect}"
+    end
+    response
   end

   def skip_unless_support_pqc

However, I couldn't find the failures on CI so far when running the CI 2 times. The logs are below.

I also see the CI on this PR is failing on Bunder on macOS-intel (ruby-4.0) cases. But I don't think the failures are related to this PR.

https://github.com/ruby/rubygems/actions/runs/28117909971/job/83262168621?pr=9637

rspec ./spec/install/path_spec.rb:167 # bundle install when BUNDLE_PATH or the global path config is set re-installs gems whose extensions have been deleted

So, could you review this PR? When the above issues happen again, I will investigate again.

rspec ./spec/commands/pristine_spec.rb:234 # bundle pristine when a build config exists for one of the gems applies the config when installing the gem

@junaruga junaruga force-pushed the wip/bundler-add-pqc-integration-tests branch from c245713 to defe1d5 Compare June 25, 2026 23:25
@junaruga junaruga marked this pull request as draft June 25, 2026 23:57
@junaruga

Copy link
Copy Markdown
Member Author

The HTTP 403 issue appeared again after rebasing on the latest master branch. Let me investigate.

@junaruga junaruga force-pushed the wip/bundler-add-pqc-integration-tests branch 3 times, most recently from 3231648 to 40bd7b3 Compare June 26, 2026 22:24
@junaruga

junaruga commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

Maybe I found the cause of the HTTP status 403 issue and fixed it. I debugged with the following debug log on my fork repository.

diff --git a/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb b/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb
index 91f0200558..8403f6cc1e 100644
--- a/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb
+++ b/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb
@@ -69,7 +69,17 @@ def fetch_path(uri)
     fetcher = Bundler::Fetcher.new(remote)

     connection = fetcher.send(:connection)
-    connection.request(uri)
+    response = connection.request(uri)
+    unless response.code == "200"
+      warn "DEBUG: uri=#{uri} host=#{uri.host} code=#{response.code} body=#{response.body.inspect}"
+      warn "DEBUG: Gem::Net::HTTP=#{Gem::Net::HTTP}"
+      warn "DEBUG: Artifice defined=#{defined?(Artifice)}"
+      if defined?(Endpoint)
+        permitted = Endpoint.host_authorization[:permitted_hosts]
+        warn "DEBUG: Endpoint permitted_hosts=#{permitted.inspect}"
+      end
+    end
+    response
   end

   def skip_unless_support_pqc
diff --git a/spec/support/artifice/helpers/rack_request.rb b/spec/support/artifice/helpers/rack_request.rb
index 05ff034463..d4920fc775 100644
--- a/spec/support/artifice/helpers/rack_request.rb
+++ b/spec/support/artifice/helpers/rack_request.rb
@@ -45,6 +45,10 @@ def connect
       #   this method will yield the Gem::Net::HTTPResponse to
       #   it after the body is read.
       def request(req, body = nil, &block)
+        if addr_port.include?("localhost")
+          warn "DEBUG: Artifice::Net::HTTP#request called with endpoint=#{self.class.endpoint} addr_port=#{addr_port}"
+          warn "DEBUG: caller=\n#{caller.join("\n")}"
+        end
         rack_request = RackRequest.new(self.class.endpoint)

         req.each_header do |header, value|

And the log is below. The full log is here.

DEBUG: Artifice::Net::HTTP#request called with endpoint=CompactIndexAPI addr_port=localhost:41071
DEBUG: caller=
/home/runner/work/rubygems/rubygems/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb:930:in 'block in Gem::Net::HTTP::Persistent#request'
/home/runner/work/rubygems/rubygems/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb:651:in 'Gem::Net::HTTP::Persistent#connection_for'
/home/runner/work/rubygems/rubygems/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb:924:in 'Gem::Net::HTTP::Persistent#request'
/home/runner/work/rubygems/rubygems/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:72:in 'RSpec::ExampleGroups::BundlerFetcherLocalSSLServer#fetch_path'
/home/runner/work/rubygems/rubygems/spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:34:in 'block (4 levels) in <top (required)>'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:263:in 'BasicObject#instance_exec'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:263:in 'block in RSpec::Core::Example#run'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:511:in 'block in RSpec::Core::Example#with_around_and_singleton_context_hooks'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:468:in 'block in RSpec::Core::Example#with_around_example_hooks'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:486:in 'block in RSpec::Core::Hooks::HookCollections#run'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:626:in 'block in RSpec::Core::Hooks::HookCollections#run_around_example_hooks_for'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:352:in 'RSpec::Core::Example::Procsy#call'
/home/runner/work/rubygems/rubygems/spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
/home/runner/work/rubygems/rubygems/lib/bundler/ui/shell.rb:177:in 'Bundler::UI::Shell#with_level'
/home/runner/work/rubygems/rubygems/lib/bundler/ui/shell.rb:122:in 'Bundler::UI::Shell#silence'
/home/runner/work/rubygems/rubygems/spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
/home/runner/work/rubygems/rubygems/spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
/home/runner/work/rubygems/rubygems/spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
/home/runner/work/rubygems/rubygems/spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
/home/runner/work/rubygems/rubygems/spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:457:in 'BasicObject#instance_exec'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:457:in 'RSpec::Core::Example#instance_exec'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:390:in 'RSpec::Core::Hooks::AroundHook#execute_with'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:628:in 'block (2 levels) in RSpec::Core::Hooks::HookCollections#run_around_example_hooks_for'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:352:in 'RSpec::Core::Example::Procsy#call'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:629:in 'RSpec::Core::Hooks::HookCollections#run_around_example_hooks_for'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:486:in 'RSpec::Core::Hooks::HookCollections#run'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:468:in 'RSpec::Core::Example#with_around_example_hooks'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:511:in 'RSpec::Core::Example#with_around_and_singleton_context_hooks'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:259:in 'RSpec::Core::Example#run'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:653:in 'block in RSpec::Core::ExampleGroup.run_examples'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:649:in 'Array#map'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:649:in 'RSpec::Core::ExampleGroup.run_examples'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:614:in 'RSpec::Core::ExampleGroup.run'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:615:in 'block in RSpec::Core::ExampleGroup.run'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:615:in 'Array#map'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:615:in 'RSpec::Core::ExampleGroup.run'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:615:in 'block in RSpec::Core::ExampleGroup.run'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:615:in 'Array#map'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:615:in 'RSpec::Core::ExampleGroup.run'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:121:in 'block (3 levels) in RSpec::Core::Runner#run_specs'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:121:in 'Array#map'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:121:in 'block (2 levels) in RSpec::Core::Runner#run_specs'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/configuration.rb:2097:in 'RSpec::Core::Configuration#with_suite_hooks'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:116:in 'block in RSpec::Core::Runner#run_specs'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/reporter.rb:74:in 'RSpec::Core::Reporter#report'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:115:in 'RSpec::Core::Runner#run_specs'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:89:in 'RSpec::Core::Runner#run'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:71:in 'RSpec::Core::Runner.run'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:45:in 'RSpec::Core::Runner.invoke'
/opt/hostedtoolcache/Ruby/4.0.5/x64/lib/ruby/gems/4.0.0/gems/rspec-core-3.13.6/exe/rspec:4:in '<top (required)>'
/home/runner/work/rubygems/rubygems/lib/rubygems.rb:306:in 'Kernel#load'
/home/runner/work/rubygems/rubygems/lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'
/opt/hostedtoolcache/Ruby/4.0.5/x64/bin/rspec:25:in '<main>'
DEBUG: uri=https://localhost:41071/yaml host=localhost code=403 body="Host not permitted"
DEBUG: Gem::Net::HTTP=Artifice::Net::HTTP
DEBUG: Artifice defined=constant
DEBUG: Endpoint permitted_hosts=[".example.org", ".local", ".mirror", ".repo", ".repo1", ".repo2", ".repo3", ".repo4", ".rubygems.org", ".security", ".source", ".test", "127.0.0.1"]

Added localhost to Artifice Endpoint's host_authorization - permitted_hosts in spec/support/artifice/helpers/endpoint.rb.

diff --git a/spec/support/artifice/helpers/endpoint.rb b/spec/support/artifice/helpers/endpoint.rb
index 9590611dfe..55e2110245 100644
--- a/spec/support/artifice/helpers/endpoint.rb
+++ b/spec/support/artifice/helpers/endpoint.rb
@@ -27,7 +27,7 @@ def self.all_requests

   set :raise_errors, true
   set :show_exceptions, false
-  set :host_authorization, permitted_hosts: [".example.org", ".local", ".mirror", ".repo", ".repo1", ".repo2", ".repo3", ".repo4", ".rubygems.org", ".security", ".source", ".test", "127.0.0.1"]
+  set :host_authorization, permitted_hosts: [".example.org", ".local", ".mirror", ".repo", ".repo1", ".repo2", ".repo3", ".repo4", ".rubygems.org", ".security", ".source", ".test", "127.0.0.1", "localhost"]

   def call!(*)
     super.tap do

Because spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb#fetch_path
connects to localhost, and Artifice::Net::HTTP is used instead of
Gem::Net::HTTP, and Artifice::Net::HTTP#request
(spec/support/artifice/helpers/rack_request.rb#request) is called with
endpoint=CompactIndexAPI extending Endpoint, and Endpoint's host_authorization - permitted_hosts is used. Without adding localhost to the permitted_hosts, the client connection to https://localhost:<port>/yaml sometimes fails with HTTP status
(response.code): 403, and response.body: "Host not permitted".

I cannot explain why the issue sometimes happens rather than always.

@junaruga junaruga force-pushed the wip/bundler-add-pqc-integration-tests branch from 40bd7b3 to 1d1474f Compare June 26, 2026 22:27
@junaruga junaruga marked this pull request as ready for review June 26, 2026 22:28
@junaruga junaruga force-pushed the wip/bundler-add-pqc-integration-tests branch 2 times, most recently from cf3cec0 to e5c62af Compare June 29, 2026 11:37
@junaruga

Copy link
Copy Markdown
Member Author

After rebasing now, I see new failures with HTTP status 404 Not Found. Let me fix the issue.

https://github.com/ruby/rubygems/actions/runs/28369192398/job/84042446245?pr=9637#step:12:122

Failures:

  1) Bundler::Fetcher local SSL server #connection non-PQC connects with client cert auth
     Failure/Error: expect(response.code).to eq("200")

       expected: #<Encoding:UTF-8> "200"
            got: #<Encoding:ASCII-8BIT> "404"

       (compared using ==)
     # ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:35:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'

  2) Bundler::Fetcher local SSL server #connection non-PQC connects
     Failure/Error: expect(response.code).to eq("200")

       expected: #<Encoding:UTF-8> "200"
            got: #<Encoding:ASCII-8BIT> "404"

       (compared using ==)
     # ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:24:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'

Finished in 18 minutes 28 seconds (files took 0.57338 seconds to load)
3884 examples, 2 failures, 26 pending

@junaruga junaruga marked this pull request as draft June 29, 2026 12:23
junaruga and others added 2 commits June 29, 2026 16:51
…nection tests

Create spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb
adding non-PQC and PQC server/client connection integration tests.
As "Bundler::Fetcher local SSL server #connection PQC connects with client cert
auth" failed with the following error due to hardcoded `OpenSSL::PKey::RSA.new` in
`Bundler::Fetcher#connection`, fixed it to support ML-DSA ssl_client_cert.

```
$ bin/rspec spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb
...
Failures:

  1) Bundler::Fetcher local SSL server #connection PQC connects with client cert auth
     Failure/Error: fetcher = Bundler::Fetcher.new(remote)

     OpenSSL::PKey::PKeyError:
       incorrect pkey type: UNDEF
     # /home/jaruga/.local/ruby-4.1.0-debug-3ef48ef9c8-openssl-4.1.0-7194354488/lib/ruby/4.1.0+1/openssl/pkey.rb:394:in 'OpenSSL::PKey::RSA#initialize'
     # /home/jaruga/.local/ruby-4.1.0-debug-3ef48ef9c8-openssl-4.1.0-7194354488/lib/ruby/4.1.0+1/openssl/pkey.rb:394:in 'Class#new'
     # /home/jaruga/.local/ruby-4.1.0-debug-3ef48ef9c8-openssl-4.1.0-7194354488/lib/ruby/4.1.0+1/openssl/pkey.rb:394:in 'OpenSSL::PKey::RSA.new'
     # ./bundler/lib/bundler/fetcher.rb:321:in 'Bundler::Fetcher#connection'
     # ./bundler/lib/bundler/fetcher.rb:140:in 'Bundler::Fetcher#initialize'
     # ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:69:in 'RSpec::ExampleGroups::BundlerFetcherLocalSSLServer#fetch_path'
     # ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:60:in 'block (4 levels) in <top (required)>'
...
```

Create test/rubygems/local_ssl_server_utilities.rb to manage utility methods
called by RubyGems test-unit and Bundler rspec tests.

Co-Authored-By: Claude <noreply@anthropic.com>
The hardcoded Gem::Net::HTTP in Artifice.deactivate, is actually
the replaced Artifice::Net::HTTP. This doesn't restore the original
Gem::Net::HTTP.

Restore the saved original Gem::Net::HTTP in Artifice.deactivate

This issue caused the tests sometimes failed in
spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb
using Artifice::Net::HTTP unintentionally. Because localhost is not included in
spec/support/artifice/helpers/endpoint.rb - permitted_hosts caused HTTP status 403
(response.body: "Host not permitted"). But Gem::Net::HTTP should be used in the
tests instead of Artifice::Net::HTTP. Possibly this issue happens when
Artifice.deactivate is called in other tests such as spec/commands/ssl_spec.rb.
That's why this issue sometimes happened rather than always.

```
  1) Bundler::Fetcher local SSL server #connection PQC connects
     Failure/Error: expect(response.code).to eq("200")

       expected: #<Encoding:UTF-8> "200"
            got: #<Encoding:ASCII-8BIT> "403"

       (compared using ==)
     # ./spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb:49:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'
```

spec/commands/ssl_spec.rb
```
...
  after(:each) do
    ...
    Artifice.deactivate
    ...
  end
...
```

Co-Authored-By: Claude <noreply@anthropic.com>
@junaruga junaruga force-pushed the wip/bundler-add-pqc-integration-tests branch from e5c62af to 339165f Compare June 29, 2026 16:11
@junaruga

junaruga commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

OK. Maybe I found and fixed the issue on the 2nd commit. The issue was in Artifice.deactivate. Artifice::Net::HTTP was unintentionally used in the tests of spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb. Because other tests that was executed before the tests in gem_remote_fetcher_local_ssl_server_spec.rb, didn't deactivate the Artifice::Net::HTTP, restoring it to Gem::Net::HTTP. Below is the debug log to prove this situation. You see Gem::Net::HTTP is still Artifice::Net::HTTP after calling Artifice.deactivate.

diff --git a/spec/commands/ssl_spec.rb b/spec/commands/ssl_spec.rb
index 4220731b69..f1fbc7faa4 100644
--- a/spec/commands/ssl_spec.rb
+++ b/spec/commands/ssl_spec.rb
@@ -28,6 +28,7 @@
   after(:each) do
     Bundler.ui = @previous_ui
     Artifice.deactivate
+    warn "DEBUG: ssl_spec.rb after(:each) Gem::Net::HTTP=#{Gem::Net::HTTP} after deactivate"
     Gem::Request::ConnectionPools.client = @previous_client
   end
$ bin/rspec spec/commands/ssl_spec.rb -e "fails due to a bundler and rubygems connection error"
Run options: include {full_description: /fails\ due\ to\ a\ bundler\ and\ rubygems\ connection\ error/}

Randomized with seed 65498
DEBUG: ssl_spec.rb after(:each) Gem::Net::HTTP=Artifice::Net::HTTP after deactivate
.

Finished in 1.85 seconds (files took 0.47634 seconds to load)
1 example, 0 failures

Randomized with seed 65498

So, I don't need to add localhost to permitted_hosts in spec/support/artifice/helpers/endpoint.rb, because the spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb doesn't use the Artifice::Net::HTTP. I reverted the change adding the localhost to permitted_hosts.

@junaruga junaruga marked this pull request as ready for review June 29, 2026 16:18
@junaruga

Copy link
Copy Markdown
Member Author

The following new test failures shows maybe the actual Gem::Net::HTTP is used instead of Artifice::Net::HTTP. Maybe this issue appeared because I fixed Artifice.deactivate. Let me fix the failures.

https://github.com/ruby/rubygems/actions/runs/28386154046/job/84101204150?pr=9637


  1) Bundler::ParallelInstaller connect to make jobserver takes all available slots
     Failure/Error: raise HTTPError, e.message

     Bundler::HTTPError:
       Could not reach host gem.repo2. Check your network connection and try again.
     # ./spec/bundler/installer/parallel_installer_spec.rb:151:in 'block (4 levels) in <top (required)>'
     # ./spec/bundler/installer/parallel_installer_spec.rb:221:in 'RSpec::ExampleGroups::BundlerParallelInstaller::ConnectToMakeJobserver#redefine_build_jobs'
     # ./spec/bundler/installer/parallel_installer_spec.rb:150:in 'block (3 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'
     # ------------------
     # --- Caused by: ---
     # Bundler::HTTPError:
     #   Could not reach host gem.repo2. Check your network connection and try again.
     #   ./spec/bundler/installer/parallel_installer_spec.rb:151:in 'block (4 levels) in <top (required)>'

  2) Bundler::ParallelInstaller connect to make jobserver release the job slots
     Failure/Error: raise HTTPError, e.message

     Bundler::HTTPError:
       Could not reach host gem.repo2. Check your network connection and try again.
     # ./spec/bundler/installer/parallel_installer_spec.rb:189:in 'block (3 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'
     # ------------------
     # --- Caused by: ---
     # Bundler::HTTPError:
     #   Could not reach host gem.repo2. Check your network connection and try again.
     #   ./spec/bundler/installer/parallel_installer_spec.rb:189:in 'block (3 levels) in <top (required)>'

  3) Bundler::ParallelInstaller connect to make jobserver fallback to non parallel when no slots are available
     Failure/Error: raise HTTPError, e.message

     Bundler::HTTPError:
       Could not reach host gem.repo2. Check your network connection and try again.
     # ./spec/bundler/installer/parallel_installer_spec.rb:162:in 'block (4 levels) in <top (required)>'
     # ./spec/bundler/installer/parallel_installer_spec.rb:221:in 'RSpec::ExampleGroups::BundlerParallelInstaller::ConnectToMakeJobserver#redefine_build_jobs'
     # ./spec/bundler/installer/parallel_installer_spec.rb:161:in 'block (3 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'
     # ------------------
     # --- Caused by: ---
     # Bundler::HTTPError:
     #   Could not reach host gem.repo2. Check your network connection and try again.
     #   ./spec/bundler/installer/parallel_installer_spec.rb:162:in 'block (4 levels) in <top (required)>'

  4) Bundler::ParallelInstaller priority queue queues native extensions in priority
     Failure/Error: raise HTTPError, e.message

     Bundler::HTTPError:
       Could not reach host gem.repo2. Check your network connection and try again.
     # ./spec/bundler/installer/parallel_installer_spec.rb:59:in 'block (3 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'
     # ------------------
     # --- Caused by: ---
     # Bundler::HTTPError:
     #   Could not reach host gem.repo2. Check your network connection and try again.
     #   ./spec/bundler/installer/parallel_installer_spec.rb:59:in 'block (3 levels) in <top (required)>'

Finished in 17 minutes 3 seconds (files took 0.91218 seconds to load)
3860 examples, 4 failures, 25 pending

This commit fixes the following failures.
The `require "support/artifice/compact_index"` is not enough
because the file is not loaded when it is called second time.

```
  1) Bundler::ParallelInstaller connect to make jobserver takes all available slots
     Failure/Error: raise HTTPError, e.message

     Bundler::HTTPError:
       Could not reach host gem.repo2. Check your network connection and try again.
     # ./spec/bundler/installer/parallel_installer_spec.rb:151:in 'block (4 levels) in <top (required)>'
     # ./spec/bundler/installer/parallel_installer_spec.rb:221:in 'RSpec::ExampleGroups::BundlerParallelInstaller::ConnectToMakeJobserver#redefine_build_jobs'
     # ./spec/bundler/installer/parallel_installer_spec.rb:150:in 'block (3 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (4 levels) in <top (required)>'
     # ./spec/spec_helper.rb:164:in 'block (3 levels) in <top (required)>'
     # ./spec/support/helpers.rb:414:in 'block in Spec::Helpers#with_gem_path_as'
     # ./spec/support/helpers.rb:428:in 'Spec::Helpers#without_env_side_effects'
     # ./spec/support/helpers.rb:409:in 'Spec::Helpers#with_gem_path_as'
     # ./spec/spec_helper.rb:163:in 'block (2 levels) in <top (required)>'
     # ./lib/rubygems.rb:306:in 'Kernel#load'
     # ./lib/rubygems.rb:306:in 'Gem.activate_and_load_bin_path'
     # ------------------
     # --- Caused by: ---
     # Bundler::HTTPError:
     #   Could not reach host gem.repo2. Check your network connection and try again.
     #   ./spec/bundler/installer/parallel_installer_spec.rb:151:in 'block (4 levels) in <top (required)>'
...
```

Co-Authored-By: Claude <noreply@anthropic.com>
@junaruga

Copy link
Copy Markdown
Member Author

@hsbt I fixed the Artifice.deactivate issue to fix the 1st commits' test failures, as a 2nd commit, and fixed above Bundler::ParallelInstaller test failures appeared by the 2nd commit, as a 3rd commit. This PR is ready for review. Thanks for your review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants