diff --git a/common.mk b/common.mk index fcf64566f5c1e4..204312cc8f4796 100644 --- a/common.mk +++ b/common.mk @@ -1050,8 +1050,8 @@ $(ENC_MK): $(srcdir)/enc/make_encmake.rb $(srcdir)/enc/Makefile.in $(srcdir)/enc PHONY: -{$(VPATH)}parse.c: {$(VPATH)}parse.y {$(VPATH)}id.h -{$(VPATH)}parse.h: {$(VPATH)}parse.c +parse.c: {$(VPATH)}parse.y {$(VPATH)}id.h +parse.h: {$(VPATH)}parse.c {$(srcdir)}.y.c: $(ECHO) generating $@ @@ -1310,9 +1310,7 @@ $(MAINOBJ): $(srcdir)/$(MAINSRC) $(ECHO) compiling $(srcdir)/$(MAINSRC) $(Q) $(CC) $(MAINCPPFLAGS) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/$(MAINSRC) -{$(VPATH)}probes.dmyh: {$(srcdir)}probes.d $(tooldir)/gen_dummy_probes.rb - -probes.dmyh: +probes.dmyh: {$(srcdir)}probes.d $(tooldir)/gen_dummy_probes.rb $(BASERUBY) $(tooldir)/gen_dummy_probes.rb $(srcdir)/probes.d > $@ probes.h: {$(VPATH)}probes.$(DTRACE_EXT) $(srcdir)/vm_opts.h diff --git a/file.c b/file.c index 7193bc28dd99f2..cd206a692bb1cf 100644 --- a/file.c +++ b/file.c @@ -6038,14 +6038,14 @@ rb_file_join(long argc, VALUE *args) } /* * call-seq: - * File.join(*objects) -> new_string + * File.join(*components) -> string * - * Returns a new string formed by joining the given string-converted +objects+ + * Returns a new string formed by joining the given string +components+ * with character '/': * - * File.join # => "" - * File.join('foo') # => "foo" - * File.join('foo', 'bar', 'baz') # => "foo/bar/baz" + * File.join # => "" + * File.join('foo') # => "foo" + * File.join(*%w[bar baz bat]) # => "bar/baz/bat" * */ diff --git a/lib/bundler/bundler.gemspec b/lib/bundler/bundler.gemspec index 62ac4ba3331463..346d156693e018 100644 --- a/lib/bundler/bundler.gemspec +++ b/lib/bundler/bundler.gemspec @@ -12,12 +12,15 @@ Gem::Specification.new do |s| s.version = Bundler::VERSION s.license = "MIT" s.authors = [ - "André Arko", "Samuel Giddins", "Colby Swandale", "Hiroshi Shibata", - "David Rodríguez", "Grey Baker", "Stephanie Morillo", "Chris Morris", "James Wen", "Tim Moore", - "André Medeiros", "Jessica Lynn Suttles", "Terence Lee", "Carl Lerche", - "Yehuda Katz" + "Yehuda Katz", "Carl Lerche", "André Arko", "Terence Lee", "Tim Moore", + "Jessica Lynn Suttles", "Hiroshi SHIBATA", "André Medeiros", "Samuel Giddins", "David Rodríguez", + "James Wen", "Chris Morris", "Colby Swandale", "Grey Baker", "Stephanie Morillo" + ] + s.email = [ + "wycats@gmail.com", "me@carllerche.com", "andre@arko.net", "hone02@gmail.com", "tmoore@incrementalism.net", + "jlsuttles@gmail.com", "hsbt@ruby-lang.org", "me@andremedeiros.info", "segiddins@segiddins.me", "deivid.rodriguez@riseup.net", + "jrw2175@columbia.edu", "chrismo@clabs.org", "colby@rubygems.org", "greysteil@gmail.com", "" ] - s.email = ["team@bundler.io"] s.homepage = "https://bundler.io" s.summary = "The best way to manage your application's dependencies" s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably" diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb index 5d0485ee558ef0..9be06aac47c0ea 100644 --- a/lib/bundler/endpoint_specification.rb +++ b/lib/bundler/endpoint_specification.rb @@ -193,11 +193,16 @@ def parse_metadata(data) TIME_ZONE_SUFFIX = /(?:Z|z|[+-]\d{2}(?::?\d{2})?)\z/ private_constant :TIME_ZONE_SUFFIX + # See Gem::Cooldown::FOUR_DIGIT_YEAR. + FOUR_DIGIT_YEAR = /\A\d{4}-/ + private_constant :FOUR_DIGIT_YEAR + # A timestamp without a time zone offset is read as UTC, because reading # it as local time would shift the cooldown window by the environment's - # offset. Unparsable values become nil so the cooldown fails open. + # offset. Unparsable values and years outside four digits become nil so + # the cooldown fails open. def parse_created_at(value) - return unless value.is_a?(String) + return unless value.is_a?(String) && value.match?(FOUR_DIGIT_YEAR) require "time" begin diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index 6258308e3b6dbb..9ba67433f4210d 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -432,10 +432,12 @@ def fetch def validate_spec(_spec); end def load_gemspec(file) - dirname = Pathname.new(file).dirname - SharedHelpers.chdir(dirname.to_s) do - stub = Gem::StubSpecification.gemspec_stub(file, install_path.parent, install_path.parent) - stub.full_gem_path = dirname.expand_path(root).to_s + # Expand the path before the chdir below, since resolving it inside the + # block would base it on the gemspec directory instead of `root`. + gemspec_path = Pathname.new(file).expand_path(root) + SharedHelpers.chdir(gemspec_path.dirname.to_s) do + stub = Gem::StubSpecification.gemspec_stub(gemspec_path.to_s, install_path.parent, install_path.parent) + stub.full_gem_path = gemspec_path.dirname.to_s StubSpecification.from_stub(stub) end end diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index 42f1e9dd71ffb2..eba61c02386c76 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -430,7 +430,7 @@ def verify(reference) def configured_uri @configured_uri ||= if /https?:/.match?(uri) remote = Gem::URI(uri) - config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host] + config_auth = Bundler.settings.credentials_for(remote) remote.userinfo ||= config_auth remote.to_s else diff --git a/lib/rubygems/compact_index_client/cache.rb b/lib/rubygems/compact_index_client/cache.rb index 3bfd3bfe397d4d..61cfd62aa22806 100644 --- a/lib/rubygems/compact_index_client/cache.rb +++ b/lib/rubygems/compact_index_client/cache.rb @@ -79,7 +79,7 @@ def info_etag_path(name) # validated, so refuse anything that would escape the cache # directory when used as a path component. def validate_name!(name) - return if File.basename(name) == name + return unless name.empty? || name.include?("\0") || name == "." || name == ".." || File.basename(name) != name raise Gem::Exception, "malformed gem name: #{name.inspect}" end diff --git a/lib/rubygems/compact_index_client/http_fetcher.rb b/lib/rubygems/compact_index_client/http_fetcher.rb index 5ba5ee1147820c..87040972912f18 100644 --- a/lib/rubygems/compact_index_client/http_fetcher.rb +++ b/lib/rubygems/compact_index_client/http_fetcher.rb @@ -29,7 +29,13 @@ def fetch(uri, headers, redirects_remaining) response = request(uri, headers) case response - when Gem::Net::HTTPSuccess, Gem::Net::HTTPNotModified + when Gem::Net::HTTPNotModified + response + when Gem::Net::HTTPSuccess + # The callers write the body into the cache, so a body-less success + # such as 204 would truncate the cached file. + raise bad_response(response, uri) unless response.class.body_permitted? + response when Gem::Net::HTTPMovedPermanently, Gem::Net::HTTPFound, Gem::Net::HTTPSeeOther, Gem::Net::HTTPTemporaryRedirect, Gem::Net::HTTPPermanentRedirect @@ -42,15 +48,20 @@ def fetch(uri, headers, redirects_remaining) if https?(uri) && !https?(redirect) raise Gem::RemoteFetcher::FetchError.new("redirecting to non-https resource: #{Gem::Uri.redact(redirect)}", uri) end + # An absolute Location on the same host drops the credentials that a + # relative one would have kept. + redirect.userinfo = uri.userinfo if redirect.host == uri.host && !redirect.userinfo fetch(redirect, headers, redirects_remaining - 1) when Gem::Net::HTTPRangeNotSatisfiable - raise Gem::RemoteFetcher::FetchError.new("bad response #{response.message} #{response.code}", uri) unless headers.key?("Range") + raise bad_response(response, uri) unless headers.key?("Range") - # The local cache is longer than the remote file, refetch it whole. - fetch(uri, headers.except("Range"), redirects_remaining) + # The local cache is longer than the remote file, refetch it whole. A + # matching ETag would otherwise turn the retry into a 304 and keep the + # oversized cache. + fetch(uri, headers.except("Range", "If-None-Match"), redirects_remaining) else - raise Gem::RemoteFetcher::FetchError.new("bad response #{response.message} #{response.code}", uri) + raise bad_response(response, uri) end end @@ -65,6 +76,11 @@ def request(uri, headers) raise Gem::RemoteFetcher::FetchError.new("#{e.class}: #{e}", uri) end + def bad_response(response, uri) + detail = response["X-Error-Message"] || response.message + Gem::RemoteFetcher::FetchError.new("bad response #{detail} #{response.code}", uri) + end + def https?(uri) uri.scheme == "https" end diff --git a/lib/rubygems/cooldown.rb b/lib/rubygems/cooldown.rb index f44958a7bc4d4b..9af06f38202c7b 100644 --- a/lib/rubygems/cooldown.rb +++ b/lib/rubygems/cooldown.rb @@ -94,14 +94,21 @@ def self.output_skipped_summary(entries) TIME_ZONE_SUFFIX = /(?:Z|z|[+-]\d{2}(?::?\d{2})?)\z/ # :nodoc: private_constant :TIME_ZONE_SUFFIX + # Matches the four-digit year an ISO 8601 timestamp starts with. + # Time.iso8601 also accepts a year of any length, and one far enough + # away overflows the Float arithmetic behind #remaining_days. + FOUR_DIGIT_YEAR = /\A\d{4}-/ # :nodoc: + private_constant :FOUR_DIGIT_YEAR + ## # Parses a +created_at+ timestamp from the compact index. A timestamp # without a time zone offset is read as UTC, because reading it as local # time would shift the cooldown window by the environment's offset. - # Returns nil for anything unparsable, so the cooldown fails open. + # Returns nil for anything unparsable, including a year outside four + # digits, so the cooldown fails open. def self.parse_created_at(value) - return unless value.is_a?(String) + return unless value.is_a?(String) && value.match?(FOUR_DIGIT_YEAR) require "time" begin diff --git a/pathname_builtin.rb b/pathname_builtin.rb index b5309a88f78555..79264a18882f06 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -955,16 +955,25 @@ def plus(path1, path2) # :nodoc: end private :plus + # :markup: markdown + # # call-seq: - # join(*objects) -> new_pathname + # join(*components) -> self or new_pathname + # + # With no arguments, returns `self`. # - # Joins the string-converted given +objects+ to the string path in +self+; + # With arguments, joins the given `components` to the string path in `self` + # with character `'/'`; # returns a new pathname containing the joined string: # - # Pathname('foo').join # => # - # Pathname('foo').join('bar') # => # - # Pathname('foo').join('bar', 'baz') # => # - # Pathname('foo').join(Pathname('bar')) # => # + # ```ruby + # pn = Pathname('foo') # => # + # # String arguments. + # pn.join('bar') # => # + # pn.join(*%w[bar baz bat]) # => # + # # Pathname arguments. + # pn.join(Pathname('bar'), Pathname('baz')) # => # + # ``` # def join(*args) return self if args.empty? diff --git a/spec/bundler/bundler/endpoint_specification_spec.rb b/spec/bundler/bundler/endpoint_specification_spec.rb index 229ea34dda66f0..207db2fcbf3823 100644 --- a/spec/bundler/bundler/endpoint_specification_spec.rb +++ b/spec/bundler/bundler/endpoint_specification_spec.rb @@ -97,6 +97,14 @@ def with_tz(tz) end end + context "when created_at has a year that overflows Float arithmetic" do + let(:metadata) { { "created_at" => ["#{"9" * 400}-01-01T00:00:00Z"] } } + + it "leaves created_at as nil" do + expect(subject.created_at).to be_nil + end + end + context "when the metadata has an empty checksum value" do let(:metadata) { { "checksum" => [] } } diff --git a/spec/bundler/bundler/resolver/cooldown_spec.rb b/spec/bundler/bundler/resolver/cooldown_spec.rb index 37ec158cba4fcf..52862a87d9d53e 100644 --- a/spec/bundler/bundler/resolver/cooldown_spec.rb +++ b/spec/bundler/bundler/resolver/cooldown_spec.rb @@ -73,6 +73,16 @@ def spec(created_at:, remote:, name: "myrack", version: "1.0.0") end end + context "when created_at has a year that overflows Float arithmetic" do + it "keeps the spec like one without created_at" do + metadata = { "created_at" => ["#{"9" * 400}-01-01T00:00:00Z"] } + s = Bundler::EndpointSpecification.new("myrack", "1.0.0", Gem::Platform::RUBY, nil, [], metadata) + s.remote = remote(cooldown: 7) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + context "when the remote has no cooldown" do it "keeps every spec" do s = spec(created_at: now - 3600, remote: remote(cooldown: nil)) diff --git a/spec/bundler/bundler/source/git/git_proxy_spec.rb b/spec/bundler/bundler/source/git/git_proxy_spec.rb index 760819c7e51ff4..f47e8f57997031 100644 --- a/spec/bundler/bundler/source/git/git_proxy_spec.rb +++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require "rubygems/credential_store" +require_relative "../../../support/fake_credential_backend" + RSpec.describe Bundler::Source::Git::GitProxy do let(:path) { Pathname("path") } let(:uri) { "https://github.com/ruby/rubygems.git" } @@ -98,6 +101,36 @@ end end + context "with credentials in the credential store" do + let(:fake_store) { Gem::CredentialStore.new(backend: FakeCredentialBackend.new) } + + before do + Gem::CredentialStore.instance = fake_store + fake_store.set(Bundler::Settings.key_for("github.com"), "u:p") + end + + after { Gem::CredentialStore.reset! } + + it "adds username and password from the store to URI for host" do + Bundler.settings.temporary("credential_store" => "true") do + expect(Bundler.settings["github.com"]).to be_nil + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", "https://u:p@github.com/ruby/rubygems.git", path.to_s], nil).and_return(["", "", clone_result]) + subject.checkout + end + end + + it "keeps original userinfo" do + Bundler.settings.temporary("credential_store" => "true") do + original = "https://orig:info@github.com/ruby/rubygems.git" + git_proxy = described_class.new(Pathname("path"), original, options) + allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0") + expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", original, path.to_s], nil).and_return(["", "", clone_result]) + git_proxy.checkout + end + end + end + describe "filtering credentials out of command output" do let(:secret) { "s3cr3tp4ss" } let(:credentialed_uri) { "https://user:#{secret}@github.com/ruby/rubygems.git" } diff --git a/spec/bundler/bundler/source/git_spec.rb b/spec/bundler/bundler/source/git_spec.rb index 59b145ef17adba..00f09320fc64d5 100644 --- a/spec/bundler/bundler/source/git_spec.rb +++ b/spec/bundler/bundler/source/git_spec.rb @@ -149,4 +149,30 @@ expect(::Bundler::FileUtils).to have_received(:rm_rf).once end end + + describe "#load_gemspec" do + let(:options) do + { "uri" => uri, "revision" => "123abc" } + end + + before do + allow(Bundler).to receive(:root).and_return(tmp) + allow(subject).to receive(:install_path).and_return(tmp("install/bar-123abc")) + + create_file(tmp("bar/bar.gemspec"), <<~GEMSPEC) + Gem::Specification.new do |s| + s.name = "bar" + s.version = "1.0" + end + GEMSPEC + end + + it "resolves a relative path against the root, not the gemspec directory" do + spec = Dir.chdir(tmp) { subject.send(:load_gemspec, "bar/bar.gemspec") } + + expect(spec.name).to eq("bar") + expect(spec.loaded_from).to eq(tmp("bar/bar.gemspec").to_s) + expect(spec.full_gem_path).to eq(tmp("bar").to_s) + end + end end diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index b45d50b1f2e1bc..6991725aa364af 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -424,6 +424,22 @@ expect(the_bundle).to include_gems("ripe_gem 2.0.0") end + it "treats a created_at with a year that overflows Float arithmetic as unknown" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown_bad_created_at" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + expect(out).not_to include("skipped by the cooldown setting") + + bundle "outdated --cooldown 7", artifice: "compact_index_cooldown_bad_created_at", raise_on_error: false + + expect(out).not_to include("cooldown") + end + it "annotates in-cooldown versions in bundle outdated table output" do gemfile <<-G source "https://gem.repo3" diff --git a/spec/bundler/support/artifice/compact_index_cooldown_bad_created_at.rb b/spec/bundler/support/artifice/compact_index_cooldown_bad_created_at.rb new file mode 100644 index 00000000000000..3379a9683a65ef --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_cooldown_bad_created_at.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index_cooldown" + +# Serves every version with a created_at year that Time.iso8601 accepts but +# whose distance from now overflows Float. +class CompactIndexCooldownBadCreatedAt < CompactIndexCooldownAPI + helpers do + def build_gem_version(spec, deps, checksum) + CompactIndex::GemVersionV2.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s, "#{"9" * 400}-01-01T00:00:00Z") + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexCooldownBadCreatedAt) diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb index a99afbfef74e7f..01786bea58cb89 100644 --- a/test/rubygems/test_gem_commands_install_command.rb +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -788,6 +788,25 @@ def test_execute_remote_cooldown_explicit_version_error assert_match "--cooldown 0", @ui.error end + def test_execute_remote_cooldown_unparsable_created_at_fails_open + util_setup_cooldown_repo created_at: { + "a-1" => util_cooldown_time(30), + "a-2" => "#{"9" * 400}-01-01T00:00:00Z", + } + + @cmd.options[:cooldown] = 7 + @cmd.options[:args] = %w[a] + + use_ui @ui do + assert_raise Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.execute + end + end + + assert_equal %w[a-2], @cmd.installed_specs.map(&:full_name) + refute_match "skipped by the cooldown setting", @ui.output + end + def test_execute_remote_cooldown_missing_created_at_fails_open util_setup_cooldown_repo diff --git a/test/rubygems/test_gem_commands_outdated_command.rb b/test/rubygems/test_gem_commands_outdated_command.rb index 4f88aef0f03bef..82e2b6c41e00c7 100644 --- a/test/rubygems/test_gem_commands_outdated_command.rb +++ b/test/rubygems/test_gem_commands_outdated_command.rb @@ -107,6 +107,20 @@ def test_execute_cooldown_missing_created_at_fails_open assert_equal 1, @ui.error.scan("publish times").size end + def test_execute_cooldown_unparsable_created_at_fails_open + util_setup_cooldown_repo "foo-0.2" => util_cooldown_time(30), + "foo-0.3" => "#{"9" * 400}-01-01T00:00:00Z" + + @cmd.options[:cooldown] = 7 + + use_ui @ui do + @cmd.execute + end + + assert_equal "foo (0.1 < 0.3)\n", @ui.output + assert_equal "", @ui.error + end + def test_cooldown_option @cmd.handle_options %w[--cooldown 7] diff --git a/test/rubygems/test_gem_compact_index_client_cache.rb b/test/rubygems/test_gem_compact_index_client_cache.rb index 2951f8e324191e..717a71eb50fa44 100644 --- a/test/rubygems/test_gem_compact_index_client_cache.rb +++ b/test/rubygems/test_gem_compact_index_client_cache.rb @@ -136,6 +136,22 @@ def test_fetch_info_rejects_name_escaping_cache_directory assert_empty fetcher.requests end + def test_info_rejects_dot_dot_name + assert_rejects_malformed_name ".." + end + + def test_info_rejects_dot_name + assert_rejects_malformed_name "." + end + + def test_info_rejects_empty_name + assert_rejects_malformed_name "" + end + + def test_info_rejects_name_with_null_byte + assert_rejects_malformed_name "a\0b" + end + def test_info_with_special_characters_uses_hashed_path fetcher = FakeFetcher.new("1.0.0\n") cache = Gem::CompactIndexClient::Cache.new(@dir, fetcher) @@ -146,4 +162,24 @@ def test_info_with_special_characters_uses_hashed_path assert_equal "1.0.0\n", @dir.join("info-special-characters", hashed).read refute @dir.join("info", "Rails").exist? end + + private + + def assert_rejects_malformed_name(name) + fetcher = FakeFetcher.new("1.0.0\n") + cache = Gem::CompactIndexClient::Cache.new(@dir, fetcher) + before = Dir.glob("**/*", File::FNM_DOTMATCH, base: @tempdir).sort + + e = assert_raise Gem::Exception do + cache.info(name, "no-match") + end + assert_includes e.message, "malformed gem name" + + assert_raise Gem::Exception do + cache.fetch_info(name) + end + + assert_empty fetcher.requests + assert_equal before, Dir.glob("**/*", File::FNM_DOTMATCH, base: @tempdir).sort + end end diff --git a/test/rubygems/test_gem_compact_index_client_http_fetcher.rb b/test/rubygems/test_gem_compact_index_client_http_fetcher.rb index 1ac4ed6a03300b..eb062de65d07ab 100644 --- a/test/rubygems/test_gem_compact_index_client_http_fetcher.rb +++ b/test/rubygems/test_gem_compact_index_client_http_fetcher.rb @@ -39,8 +39,15 @@ def initialize(location) end class FakeNotFound < Gem::Net::HTTPNotFound - def initialize + def initialize(error_message = nil) super("1.1", "404", "Not Found") + self["X-Error-Message"] = error_message if error_message + end + end + + class FakeNoContent < Gem::Net::HTTPNoContent + def initialize + super("1.1", "204", "No Content") end end @@ -223,6 +230,17 @@ def test_call_keeps_credentials_on_an_accepted_redirect assert_equal "s3cr3t", remote.requests.last.first.password end + def test_call_keeps_credentials_on_an_absolute_same_host_redirect + remote = FakeRemoteFetcher.new( + "https://user:s3cr3t@index.example/versions" => FakeRedirect.new("https://index.example/v2/versions"), + "https://user:s3cr3t@index.example/v2/versions" => FakeResponse.new("data") + ) + fetcher = Gem::CompactIndexClient::HTTPFetcher.new("https://user:s3cr3t@index.example", remote) + + assert_equal "data", fetcher.call("versions").body + assert_equal "s3cr3t", remote.requests.last.first.password + end + def test_call_drops_credentials_on_a_cross_host_redirect remote = FakeRemoteFetcher.new( "https://user:s3cr3t@index.example/versions" => FakeRedirect.new("https://mirror.example/versions"), @@ -246,14 +264,20 @@ def test_call_raises_after_too_many_redirects assert_match(/too many redirects/, error.message) end - def test_call_retries_without_range_on_range_not_satisfiable + def test_call_retries_without_range_and_etag_on_range_not_satisfiable requests = [] remote = Object.new remote.define_singleton_method(:request) do |uri, request_class, &block| request = request_class.new(uri) block&.call(request) requests << request - request["Range"] ? FakeRangeNotSatisfiable.new : FakeResponse.new("full data") + if request["Range"] + FakeRangeNotSatisfiable.new + elsif request["If-None-Match"] + Gem::Net::HTTPNotModified.new("1.1", "304", "Not Modified") + else + FakeResponse.new("full data") + end end fetcher = Gem::CompactIndexClient::HTTPFetcher.new("https://index.example", remote) @@ -262,7 +286,7 @@ def test_call_retries_without_range_on_range_not_satisfiable assert_equal "full data", response.body assert_equal 2, requests.size assert_nil requests.last["Range"] - assert_equal '"abc"', requests.last["If-None-Match"] + assert_nil requests.last["If-None-Match"] end def test_call_raises_on_range_not_satisfiable_without_range @@ -275,6 +299,16 @@ def test_call_raises_on_range_not_satisfiable_without_range assert_match(/bad response Range Not Satisfiable 416/, error.message) end + def test_call_raises_fetch_error_on_no_content + fetcher, _remote = fetcher_for("https://index.example/versions" => FakeNoContent.new) + + error = assert_raise Gem::RemoteFetcher::FetchError do + fetcher.call("versions") + end + + assert_match(/bad response No Content 204/, error.message) + end + def test_call_raises_fetch_error_on_failure_response fetcher, _remote = fetcher_for("https://index.example/versions" => FakeNotFound.new) @@ -284,4 +318,16 @@ def test_call_raises_fetch_error_on_failure_response assert_match(/bad response Not Found 404/, error.message) end + + def test_call_includes_x_error_message_in_fetch_error + fetcher, _remote = fetcher_for( + "https://index.example/versions" => FakeNotFound.new("blocked by corporate proxy policy") + ) + + error = assert_raise Gem::RemoteFetcher::FetchError do + fetcher.call("versions") + end + + assert_match(/bad response blocked by corporate proxy policy 404/, error.message) + end end diff --git a/test/rubygems/test_gem_cooldown.rb b/test/rubygems/test_gem_cooldown.rb index fe2ea4afd3cbb8..6950dcecc2c542 100644 --- a/test/rubygems/test_gem_cooldown.rb +++ b/test/rubygems/test_gem_cooldown.rb @@ -113,6 +113,13 @@ def test_parse_created_at_invalid assert_nil Gem::Cooldown.parse_created_at(7) end + def test_parse_created_at_rejects_years_outside_four_digits + # Time.iso8601 accepts these, but the distance from now overflows Float. + assert_nil Gem::Cooldown.parse_created_at("#{"9" * 400}-01-01T00:00:00Z") + assert_nil Gem::Cooldown.parse_created_at("-2026-06-05T10:30:45Z") + assert_nil Gem::Cooldown.parse_created_at("02026-06-05T10:30:45Z") + end + def with_tz(tz) orig_tz = ENV["TZ"] ENV["TZ"] = tz