diff options
Diffstat (limited to 'spec/bundler/support/artifice')
53 files changed, 775 insertions, 485 deletions
diff --git a/spec/bundler/support/artifice/compact_index.rb b/spec/bundler/support/artifice/compact_index.rb index fb068fa9b5..ebc4d0ae5b 100644 --- a/spec/bundler/support/artifice/compact_index.rb +++ b/spec/bundler/support/artifice/compact_index.rb @@ -1,120 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -$LOAD_PATH.unshift Dir[Spec::Path.base_system_gem_path.join("gems/compact_index*/lib")].first.to_s -require "compact_index" - -class CompactIndexAPI < Endpoint - helpers do - include Spec::Path - - def load_spec(name, version, platform, gem_repo) - full_name = "#{name}-#{version}" - full_name += "-#{platform}" if platform != "ruby" - Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) - end - - def etag_response - response_body = yield - checksum = Digest(:MD5).hexdigest(response_body) - return if not_modified?(checksum) - headers "ETag" => quote(checksum) - headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" - content_type "text/plain" - requested_range_for(response_body) - rescue StandardError => e - puts e - puts e.backtrace - raise - end - - def not_modified?(checksum) - etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"]) - - return unless etags.include?(checksum) - headers "ETag" => quote(checksum) - status 304 - body "" - end - - def requested_range_for(response_body) - ranges = Rack::Utils.byte_ranges(env, response_body.bytesize) - - if ranges - status 206 - body ranges.map! {|range| slice_body(response_body, range) }.join - else - status 200 - body response_body - end - end - - def quote(string) - %("#{string}") - end - - def parse_etags(value) - value ? value.split(/, ?/).select {|s| s.sub!(/"(.*)"/, '\1') } : [] - end - - def slice_body(body, range) - body.byteslice(range) - end - - def gems(gem_repo = default_gem_repo) - @gems ||= {} - @gems[gem_repo] ||= begin - specs = Bundler::Deprecate.skip_during do - %w[specs.4.8 prerelease_specs.4.8].map do |filename| - Marshal.load(File.open(gem_repo.join(filename)).read).map do |name, version, platform| - load_spec(name, version, platform, gem_repo) - end - end.flatten - end - - specs.group_by(&:name).map do |name, versions| - gem_versions = versions.map do |spec| - deps = spec.dependencies.select {|d| d.type == :runtime }.map do |d| - reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") - CompactIndex::Dependency.new(d.name, reqs) - end - checksum = begin - Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").base64digest - rescue StandardError - nil - end - CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, - deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s) - end - CompactIndex::Gem.new(name, gem_versions) - end - end - end - end - - get "/names" do - etag_response do - CompactIndex.names(gems.map(&:name)) - end - end - - get "/versions" do - etag_response do - file = tmp("versions.list") - FileUtils.rm_f(file) - file = CompactIndex::VersionsFile.new(file.to_s) - file.create(gems) - file.contents - end - end - - get "/info/:name" do - etag_response do - gem = gems.find {|g| g.name == params[:name] } - CompactIndex.info(gem ? gem.versions : []) - end - end -end +require_relative "helpers/compact_index" +require_relative "helpers/artifice" Artifice.activate_with(CompactIndexAPI) diff --git a/spec/bundler/support/artifice/compact_index_api_missing.rb b/spec/bundler/support/artifice/compact_index_api_missing.rb index 6514fde01e..f771f7d1f0 100644 --- a/spec/bundler/support/artifice/compact_index_api_missing.rb +++ b/spec/bundler/support/artifice/compact_index_api_missing.rb @@ -1,18 +1,13 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexApiMissing < CompactIndexAPI get "/fetch/actual/gem/:id" do - warn params[:id] - if params[:id] == "rack-1.0.gemspec.rz" - halt 404 - else - File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") - end + halt 404 end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexApiMissing) diff --git a/spec/bundler/support/artifice/compact_index_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_basic_authentication.rb index 775f1a3977..b9115cdd86 100644 --- a/spec/bundler/support/artifice/compact_index_basic_authentication.rb +++ b/spec/bundler/support/artifice/compact_index_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexBasicAuthentication < CompactIndexAPI before do @@ -12,4 +10,6 @@ class CompactIndexBasicAuthentication < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb index 1abe64236c..83b147d2ae 100644 --- a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb +++ b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexChecksumMismatch < CompactIndexAPI get "/versions" do - headers "ETag" => quote("123") + headers "Repr-Digest" => "sha-256=:ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=:" headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" content_type "text/plain" - body "" + body "content does not match the checksum" end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexChecksumMismatch) diff --git a/spec/bundler/support/artifice/compact_index_concurrent_download.rb b/spec/bundler/support/artifice/compact_index_concurrent_download.rb index 14c31f35a4..5d55b8a72b 100644 --- a/spec/bundler/support/artifice/compact_index_concurrent_download.rb +++ b/spec/bundler/support/artifice/compact_index_concurrent_download.rb @@ -1,19 +1,18 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexConcurrentDownload < CompactIndexAPI get "/versions" do versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions") - # Verify the original (empty) content hasn't been deleted, e.g. on a retry - File.binread(versions) == "" || raise("Original file should be present and empty") + # Verify the original content hasn't been deleted, e.g. on a retry + data = File.binread(versions) + data == "created_at" || raise("Original file should be present with expected content") # Verify this is only requested once for a partial download - env["HTTP_RANGE"] || raise("Missing Range header for expected partial download") + env["HTTP_RANGE"] == "bytes=#{data.bytesize - 1}-" || raise("Missing Range header for expected partial download") # Overwrite the file in parallel, which should be then overwritten # after a successful download to prevent corruption @@ -29,4 +28,6 @@ class CompactIndexConcurrentDownload < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexConcurrentDownload) diff --git a/spec/bundler/support/artifice/compact_index_creds_diff_host.rb b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb index cfe22c7f51..401e8a98d8 100644 --- a/spec/bundler/support/artifice/compact_index_creds_diff_host.rb +++ b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexCredsDiffHost < CompactIndexAPI helpers do @@ -36,4 +34,6 @@ class CompactIndexCredsDiffHost < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexCredsDiffHost) diff --git a/spec/bundler/support/artifice/compact_index_etag_match.rb b/spec/bundler/support/artifice/compact_index_etag_match.rb new file mode 100644 index 0000000000..08d7b5ec53 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_etag_match.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexEtagMatch < CompactIndexAPI + get "/versions" do + raise "ETag header should be present" unless env["HTTP_IF_NONE_MATCH"] + headers "ETag" => env["HTTP_IF_NONE_MATCH"] + status 304 + body "" + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexEtagMatch) diff --git a/spec/bundler/support/artifice/compact_index_extra.rb b/spec/bundler/support/artifice/compact_index_extra.rb index cec368276a..cd41b3ecca 100644 --- a/spec/bundler/support/artifice/compact_index_extra.rb +++ b/spec/bundler/support/artifice/compact_index_extra.rb @@ -1,37 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate - -class CompactIndexExtra < CompactIndexAPI - get "/extra/versions" do - halt 404 - end - - get "/extra/api/v1/dependencies" do - halt 404 - end - - get "/extra/specs.4.8.gz" do - File.binread("#{gem_repo2}/specs.4.8.gz") - end - - get "/extra/prerelease_specs.4.8.gz" do - File.binread("#{gem_repo2}/prerelease_specs.4.8.gz") - end - - get "/extra/quick/Marshal.4.8/:id" do - redirect "/extra/fetch/actual/gem/#{params[:id]}" - end - - get "/extra/fetch/actual/gem/:id" do - File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") - end - - get "/extra/gems/:id" do - File.binread("#{gem_repo2}/gems/#{params[:id]}") - end -end +require_relative "helpers/compact_index_extra" +require_relative "helpers/artifice" Artifice.activate_with(CompactIndexExtra) diff --git a/spec/bundler/support/artifice/compact_index_extra_api.rb b/spec/bundler/support/artifice/compact_index_extra_api.rb index 5cc13421a8..8b9d304ab4 100644 --- a/spec/bundler/support/artifice/compact_index_extra_api.rb +++ b/spec/bundler/support/artifice/compact_index_extra_api.rb @@ -1,52 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate - -class CompactIndexExtraApi < CompactIndexAPI - get "/extra/names" do - etag_response do - CompactIndex.names(gems(gem_repo4).map(&:name)) - end - end - - get "/extra/versions" do - etag_response do - file = tmp("versions.list") - FileUtils.rm_f(file) - file = CompactIndex::VersionsFile.new(file.to_s) - file.create(gems(gem_repo4)) - file.contents - end - end - - get "/extra/info/:name" do - etag_response do - gem = gems(gem_repo4).find {|g| g.name == params[:name] } - CompactIndex.info(gem ? gem.versions : []) - end - end - - get "/extra/specs.4.8.gz" do - File.binread("#{gem_repo4}/specs.4.8.gz") - end - - get "/extra/prerelease_specs.4.8.gz" do - File.binread("#{gem_repo4}/prerelease_specs.4.8.gz") - end - - get "/extra/quick/Marshal.4.8/:id" do - redirect "/extra/fetch/actual/gem/#{params[:id]}" - end - - get "/extra/fetch/actual/gem/:id" do - File.binread("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") - end - - get "/extra/gems/:id" do - File.binread("#{gem_repo4}/gems/#{params[:id]}") - end -end +require_relative "helpers/compact_index_extra_api" +require_relative "helpers/artifice" Artifice.activate_with(CompactIndexExtraApi) diff --git a/spec/bundler/support/artifice/compact_index_extra_api_missing.rb b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb index b9d757c266..df6ede584c 100644 --- a/spec/bundler/support/artifice/compact_index_extra_api_missing.rb +++ b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index_extra_api" - -Artifice.deactivate +require_relative "helpers/compact_index_extra_api" class CompactIndexExtraAPIMissing < CompactIndexExtraApi get "/extra/fetch/actual/gem/:id" do @@ -14,4 +12,6 @@ class CompactIndexExtraAPIMissing < CompactIndexExtraApi end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexExtraAPIMissing) diff --git a/spec/bundler/support/artifice/compact_index_extra_missing.rb b/spec/bundler/support/artifice/compact_index_extra_missing.rb index ff1e47a1bb..255c89afdb 100644 --- a/spec/bundler/support/artifice/compact_index_extra_missing.rb +++ b/spec/bundler/support/artifice/compact_index_extra_missing.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index_extra" - -Artifice.deactivate +require_relative "helpers/compact_index_extra" class CompactIndexExtraMissing < CompactIndexExtra get "/extra/fetch/actual/gem/:id" do @@ -14,4 +12,6 @@ class CompactIndexExtraMissing < CompactIndexExtra end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexExtraMissing) diff --git a/spec/bundler/support/artifice/compact_index_forbidden.rb b/spec/bundler/support/artifice/compact_index_forbidden.rb index 3eebe0fbd8..18c30ed9a2 100644 --- a/spec/bundler/support/artifice/compact_index_forbidden.rb +++ b/spec/bundler/support/artifice/compact_index_forbidden.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexForbidden < CompactIndexAPI get "/versions" do @@ -10,4 +8,6 @@ class CompactIndexForbidden < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexForbidden) diff --git a/spec/bundler/support/artifice/compact_index_host_redirect.rb b/spec/bundler/support/artifice/compact_index_host_redirect.rb index 304c897d68..4f82bf3812 100644 --- a/spec/bundler/support/artifice/compact_index_host_redirect.rb +++ b/spec/bundler/support/artifice/compact_index_host_redirect.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexHostRedirect < CompactIndexAPI - get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do redirect "http://bundler.localgemserver.test#{request.path_info}" end @@ -18,4 +16,6 @@ class CompactIndexHostRedirect < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexHostRedirect) diff --git a/spec/bundler/support/artifice/compact_index_no_gem.rb b/spec/bundler/support/artifice/compact_index_no_gem.rb index 0a4be08a46..71f6629688 100644 --- a/spec/bundler/support/artifice/compact_index_no_gem.rb +++ b/spec/bundler/support/artifice/compact_index_no_gem.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexNoGem < CompactIndexAPI get "/gems/:id" do @@ -10,4 +8,6 @@ class CompactIndexNoGem < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexNoGem) diff --git a/spec/bundler/support/artifice/compact_index_partial_update.rb b/spec/bundler/support/artifice/compact_index_partial_update.rb index cb1c7b9481..f111d91ef9 100644 --- a/spec/bundler/support/artifice/compact_index_partial_update.rb +++ b/spec/bundler/support/artifice/compact_index_partial_update.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexPartialUpdate < CompactIndexAPI # Stub the server to never return 304s. This simulates the behaviour of @@ -25,7 +23,7 @@ class CompactIndexPartialUpdate < CompactIndexAPI # Verify that a partial request is made, starting from the index of the # final byte of the cached file. unless env["HTTP_RANGE"] == "bytes=#{File.binread(cached_versions_path).bytesize - 1}-" - raise("Range header should be present, and start from the index of the final byte of the cache.") + raise("Range header should be present, and start from the index of the final byte of the cache. #{env["HTTP_RANGE"].inspect}") end etag_response do @@ -35,4 +33,6 @@ class CompactIndexPartialUpdate < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexPartialUpdate) diff --git a/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb new file mode 100644 index 0000000000..ac04336636 --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +# The purpose of this Artifice is to test that an incremental response is invalidated +# and a second request is issued for the full content. +class CompactIndexPartialUpdateBadDigest < CompactIndexAPI + def partial_update_bad_digest + response_body = yield + if request.env["HTTP_RANGE"] + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest("wrong digest on ranged request")}:" + else + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:" + end + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + end + + get "/versions" do + partial_update_bad_digest do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents([], calculate_info_checksums: true) + end + end + + get "/info/:name" do + partial_update_bad_digest do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPartialUpdateBadDigest) diff --git a/spec/bundler/support/artifice/compact_index_partial_update_no_etag_not_incremental.rb b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb index acf76dfbf0..99bae039f0 100644 --- a/spec/bundler/support/artifice/compact_index_partial_update_no_etag_not_incremental.rb +++ b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require_relative "compact_index" +require_relative "helpers/compact_index" -Artifice.deactivate - -class CompactIndexPartialUpdateNoEtagNotIncremental < CompactIndexAPI - def partial_update_no_etag +# The purpose of this Artifice is to test that an incremental response is ignored +# when the digest is not present to verify that the partial response is valid. +class CompactIndexPartialUpdateNoDigestNotIncremental < CompactIndexAPI + def partial_update_no_digest response_body = yield headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" content_type "text/plain" @@ -13,12 +13,12 @@ class CompactIndexPartialUpdateNoEtagNotIncremental < CompactIndexAPI end get "/versions" do - partial_update_no_etag do + partial_update_no_digest do file = tmp("versions.list") FileUtils.rm_f(file) file = CompactIndex::VersionsFile.new(file.to_s) file.create(gems) - lines = file.contents([], :calculate_info_checksums => true).split("\n") + lines = file.contents([], calculate_info_checksums: true).split("\n") name, versions, checksum = lines.last.split(" ") # shuffle versions so new versions are not appended to the end @@ -27,7 +27,7 @@ class CompactIndexPartialUpdateNoEtagNotIncremental < CompactIndexAPI end get "/info/:name" do - partial_update_no_etag do + partial_update_no_digest do gem = gems.find {|g| g.name == params[:name] } lines = CompactIndex.info(gem ? gem.versions : []).split("\n") @@ -37,4 +37,6 @@ class CompactIndexPartialUpdateNoEtagNotIncremental < CompactIndexAPI end end -Artifice.activate_with(CompactIndexPartialUpdateNoEtagNotIncremental) +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPartialUpdateNoDigestNotIncremental) diff --git a/spec/bundler/support/artifice/compact_index_precompiled_before.rb b/spec/bundler/support/artifice/compact_index_precompiled_before.rb new file mode 100644 index 0000000000..b5f72f546a --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_precompiled_before.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexPrecompiledBefore < CompactIndexAPI + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + move_ruby_variant_to_the_end(CompactIndex.info(gem ? gem.versions : [])) + end + end + + private + + def move_ruby_variant_to_the_end(response) + lines = response.split("\n") + ruby = lines.find {|line| /\A\d+\.\d+\.\d* \|/.match(line) } + lines.delete(ruby) + lines.push(ruby).join("\n") + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexPrecompiledBefore) diff --git a/spec/bundler/support/artifice/compact_index_range_ignored.rb b/spec/bundler/support/artifice/compact_index_range_ignored.rb new file mode 100644 index 0000000000..2303682c1f --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_range_ignored.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index" + +class CompactIndexRangeIgnored < CompactIndexAPI + # Stub the server to not return 304 so that we don't bypass all the logic + def not_modified?(_checksum) + false + end + + get "/versions" do + cached_versions_path = File.join( + Bundler.rubygems.user_home, ".bundle", "cache", "compact_index", + "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions" + ) + + # Verify a cached copy of the versions file exists + unless File.binread(cached_versions_path).size > 0 + raise("Cached versions file should be present and have content") + end + + # Verify that a partial request is made, starting from the index of the + # final byte of the cached file. + unless env.delete("HTTP_RANGE") + raise("Expected client to write the full response on the first try") + end + + etag_response do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end +end + +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexRangeIgnored) diff --git a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb index bb616125bb..8a7c4b79b0 100644 --- a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb +++ b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexRangeNotSatisfiable < CompactIndexAPI get "/versions" do @@ -31,4 +29,6 @@ class CompactIndexRangeNotSatisfiable < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexRangeNotSatisfiable) diff --git a/spec/bundler/support/artifice/compact_index_rate_limited.rb b/spec/bundler/support/artifice/compact_index_rate_limited.rb index 570105e2a0..4495491635 100644 --- a/spec/bundler/support/artifice/compact_index_rate_limited.rb +++ b/spec/bundler/support/artifice/compact_index_rate_limited.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexRateLimited < CompactIndexAPI class RequestCounter @@ -45,4 +43,6 @@ class CompactIndexRateLimited < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexRateLimited) diff --git a/spec/bundler/support/artifice/compact_index_redirects.rb b/spec/bundler/support/artifice/compact_index_redirects.rb index 99adc797bf..f7ba393239 100644 --- a/spec/bundler/support/artifice/compact_index_redirects.rb +++ b/spec/bundler/support/artifice/compact_index_redirects.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexRedirect < CompactIndexAPI get "/fetch/actual/gem/:id" do @@ -18,4 +16,6 @@ class CompactIndexRedirect < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexRedirect) diff --git a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb index 7d427b5382..96259385e7 100644 --- a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb +++ b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexStrictBasicAuthentication < CompactIndexAPI before do @@ -12,9 +10,11 @@ class CompactIndexStrictBasicAuthentication < CompactIndexAPI # Only accepts password == "password" unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" - halt 403, "Authentication failed" + halt 401, "Authentication failed" end end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb index 036fac70b3..15850599b6 100644 --- a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb +++ b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexWrongDependencies < CompactIndexAPI get "/info/:name" do @@ -14,4 +12,6 @@ class CompactIndexWrongDependencies < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexWrongDependencies) diff --git a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb index 8add32b88f..9bd2ca0a9d 100644 --- a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb +++ b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb @@ -1,15 +1,14 @@ # frozen_string_literal: true -require_relative "compact_index" - -Artifice.deactivate +require_relative "helpers/compact_index" class CompactIndexWrongGemChecksum < CompactIndexAPI get "/info/:name" do etag_response do name = params[:name] gem = gems.find {|g| g.name == name } - checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "ab" * 22 } + # This generates the hexdigest "2222222222222222222222222222222222222222222222222222222222222222" + checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=" } versions = gem ? gem.versions : [] versions.each {|v| v.checksum = checksum } CompactIndex.info(versions) @@ -17,4 +16,6 @@ class CompactIndexWrongGemChecksum < CompactIndexAPI end end +require_relative "helpers/artifice" + Artifice.activate_with(CompactIndexWrongGemChecksum) diff --git a/spec/bundler/support/artifice/endpoint.rb b/spec/bundler/support/artifice/endpoint.rb index c00113b28f..15242a7942 100644 --- a/spec/bundler/support/artifice/endpoint.rb +++ b/spec/bundler/support/artifice/endpoint.rb @@ -1,115 +1,6 @@ # frozen_string_literal: true -require_relative "../path" - -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{artifice,mustermann,rack,tilt,sinatra,ruby2_keywords}-*/lib")].map(&:to_s)) - -require "artifice" -require "sinatra/base" - -ALL_REQUESTS = [] # rubocop:disable Style/MutableConstant -ALL_REQUESTS_MUTEX = Thread::Mutex.new - -at_exit do - if expected = ENV["BUNDLER_SPEC_ALL_REQUESTS"] - expected = expected.split("\n").sort - actual = ALL_REQUESTS.sort - - unless expected == actual - raise "Unexpected requests!\nExpected:\n\t#{expected.join("\n\t")}\n\nActual:\n\t#{actual.join("\n\t")}" - end - end -end - -class Endpoint < Sinatra::Base - def self.all_requests - @all_requests ||= [] - end - - set :raise_errors, true - set :show_exceptions, false - - def call!(*) - super.tap do - ALL_REQUESTS_MUTEX.synchronize do - ALL_REQUESTS << @request.url - end - end - end - - helpers do - include Spec::Path - - def default_gem_repo - if ENV["BUNDLER_SPEC_GEM_REPO"] - Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"]) - else - case request.host - when "gem.repo1" - Spec::Path.gem_repo1 - when "gem.repo2" - Spec::Path.gem_repo2 - when "gem.repo3" - Spec::Path.gem_repo3 - when "gem.repo4" - Spec::Path.gem_repo4 - else - Spec::Path.gem_repo1 - end - end - end - - def dependencies_for(gem_names, gem_repo = default_gem_repo) - return [] if gem_names.nil? || gem_names.empty? - - all_specs = %w[specs.4.8 prerelease_specs.4.8].map do |filename| - Marshal.load(File.open(gem_repo.join(filename)).read) - end.inject(:+) - - all_specs.map do |name, version, platform| - spec = load_spec(name, version, platform, gem_repo) - next unless gem_names.include?(spec.name) - { - :name => spec.name, - :number => spec.version.version, - :platform => spec.platform.to_s, - :dependencies => spec.dependencies.select {|dep| dep.type == :runtime }.map do |dep| - [dep.name, dep.requirement.requirements.map {|a| a.join(" ") }.join(", ")] - end, - } - end.compact - end - - def load_spec(name, version, platform, gem_repo) - full_name = "#{name}-#{version}" - full_name += "-#{platform}" if platform != "ruby" - Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) - end - end - - get "/quick/Marshal.4.8/:id" do - redirect "/fetch/actual/gem/#{params[:id]}" - end - - get "/fetch/actual/gem/:id" do - File.binread("#{default_gem_repo}/quick/Marshal.4.8/#{params[:id]}") - end - - get "/gems/:id" do - File.binread("#{default_gem_repo}/gems/#{params[:id]}") - end - - get "/api/v1/dependencies" do - Marshal.dump(dependencies_for(params[:gems])) - end - - get "/specs.4.8.gz" do - File.binread("#{default_gem_repo}/specs.4.8.gz") - end - - get "/prerelease_specs.4.8.gz" do - File.binread("#{default_gem_repo}/prerelease_specs.4.8.gz") - end -end +require_relative "helpers/endpoint" +require_relative "helpers/artifice" Artifice.activate_with(Endpoint) diff --git a/spec/bundler/support/artifice/endpoint_500.rb b/spec/bundler/support/artifice/endpoint_500.rb index a0d850a44d..b1ed1964c8 100644 --- a/spec/bundler/support/artifice/endpoint_500.rb +++ b/spec/bundler/support/artifice/endpoint_500.rb @@ -2,17 +2,16 @@ require_relative "../path" -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{artifice,mustermann,rack,tilt,sinatra,ruby2_keywords}-*/lib")].map(&:to_s)) +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{mustermann,rack,tilt,sinatra,ruby2_keywords,base64}-*/lib")].map(&:to_s)) -require "artifice" require "sinatra/base" -Artifice.deactivate - class Endpoint500 < Sinatra::Base before do halt 500 end end +require_relative "helpers/artifice" + Artifice.activate_with(Endpoint500) diff --git a/spec/bundler/support/artifice/endpoint_api_forbidden.rb b/spec/bundler/support/artifice/endpoint_api_forbidden.rb index edc2463424..6bdc5896d6 100644 --- a/spec/bundler/support/artifice/endpoint_api_forbidden.rb +++ b/spec/bundler/support/artifice/endpoint_api_forbidden.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointApiForbidden < Endpoint get "/api/v1/dependencies" do @@ -10,4 +8,6 @@ class EndpointApiForbidden < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointApiForbidden) diff --git a/spec/bundler/support/artifice/endpoint_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_basic_authentication.rb index ff3d1493d6..e8e3569e63 100644 --- a/spec/bundler/support/artifice/endpoint_basic_authentication.rb +++ b/spec/bundler/support/artifice/endpoint_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointBasicAuthentication < Endpoint before do @@ -12,4 +10,6 @@ class EndpointBasicAuthentication < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb index 8b8972cedd..ce30de0a68 100644 --- a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb +++ b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointCredsDiffHost < Endpoint helpers do @@ -36,4 +34,6 @@ class EndpointCredsDiffHost < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointCredsDiffHost) diff --git a/spec/bundler/support/artifice/endpoint_extra.rb b/spec/bundler/support/artifice/endpoint_extra.rb index 942c4352b7..021fd435fe 100644 --- a/spec/bundler/support/artifice/endpoint_extra.rb +++ b/spec/bundler/support/artifice/endpoint_extra.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointExtra < Endpoint get "/extra/api/v1/dependencies" do @@ -30,4 +28,6 @@ class EndpointExtra < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointExtra) diff --git a/spec/bundler/support/artifice/endpoint_extra_api.rb b/spec/bundler/support/artifice/endpoint_extra_api.rb index 1cfef7a7fc..a965af6e73 100644 --- a/spec/bundler/support/artifice/endpoint_extra_api.rb +++ b/spec/bundler/support/artifice/endpoint_extra_api.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointExtraApi < Endpoint get "/extra/api/v1/dependencies" do @@ -31,4 +29,6 @@ class EndpointExtraApi < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointExtraApi) diff --git a/spec/bundler/support/artifice/endpoint_extra_missing.rb b/spec/bundler/support/artifice/endpoint_extra_missing.rb index 5fd9238207..73e2defb32 100644 --- a/spec/bundler/support/artifice/endpoint_extra_missing.rb +++ b/spec/bundler/support/artifice/endpoint_extra_missing.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint_extra" - -Artifice.deactivate +require_relative "helpers/endpoint_extra" class EndpointExtraMissing < EndpointExtra get "/extra/fetch/actual/gem/:id" do @@ -14,4 +12,6 @@ class EndpointExtraMissing < EndpointExtra end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointExtraMissing) diff --git a/spec/bundler/support/artifice/endpoint_fallback.rb b/spec/bundler/support/artifice/endpoint_fallback.rb index 08edf232e3..742e563f07 100644 --- a/spec/bundler/support/artifice/endpoint_fallback.rb +++ b/spec/bundler/support/artifice/endpoint_fallback.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointFallback < Endpoint DEPENDENCY_LIMIT = 60 @@ -16,4 +14,6 @@ class EndpointFallback < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointFallback) diff --git a/spec/bundler/support/artifice/endpoint_host_redirect.rb b/spec/bundler/support/artifice/endpoint_host_redirect.rb index 338cbcad00..6ce51bed93 100644 --- a/spec/bundler/support/artifice/endpoint_host_redirect.rb +++ b/spec/bundler/support/artifice/endpoint_host_redirect.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointHostRedirect < Endpoint - get "/fetch/actual/gem/:id", :host_name => "localgemserver.test" do + get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do redirect "http://bundler.localgemserver.test#{request.path_info}" end @@ -14,4 +12,6 @@ class EndpointHostRedirect < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointHostRedirect) diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/endpoint_marshal_fail.rb index 22c13e3e17..74ce321de6 100644 --- a/spec/bundler/support/artifice/endpoint_marshal_fail.rb +++ b/spec/bundler/support/artifice/endpoint_marshal_fail.rb @@ -1,13 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint_fallback" - -Artifice.deactivate - -class EndpointMarshalFail < EndpointFallback - get "/api/v1/dependencies" do - "f0283y01hasf" - end -end +require_relative "helpers/endpoint_marshal_fail" +require_relative "helpers/artifice" Artifice.activate_with(EndpointMarshalFail) diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb index c341c3993f..ea4cfbe965 100644 --- a/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb +++ b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint_marshal_fail" - -Artifice.deactivate +require_relative "helpers/endpoint_marshal_fail" class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail before do @@ -12,4 +10,6 @@ class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointMarshalFailBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_mirror_source.rb b/spec/bundler/support/artifice/endpoint_mirror_source.rb index 788a9027f3..fed7a746b9 100644 --- a/spec/bundler/support/artifice/endpoint_mirror_source.rb +++ b/spec/bundler/support/artifice/endpoint_mirror_source.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require_relative "endpoint" +require_relative "helpers/endpoint" class EndpointMirrorSource < Endpoint get "/gems/:id" do - if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" + if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" && request.env["HTTP_USER_AGENT"].start_with?("bundler") File.binread("#{gem_repo1}/gems/#{params[:id]}") else halt 500 @@ -12,4 +12,6 @@ class EndpointMirrorSource < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointMirrorSource) diff --git a/spec/bundler/support/artifice/endpoint_redirect.rb b/spec/bundler/support/artifice/endpoint_redirect.rb index ee97fccf64..84f546ba9d 100644 --- a/spec/bundler/support/artifice/endpoint_redirect.rb +++ b/spec/bundler/support/artifice/endpoint_redirect.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointRedirect < Endpoint get "/fetch/actual/gem/:id" do @@ -14,4 +12,6 @@ class EndpointRedirect < Endpoint end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointRedirect) diff --git a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb index 4d4da08770..dff360c5c5 100644 --- a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb +++ b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint" - -Artifice.deactivate +require_relative "helpers/endpoint" class EndpointStrictBasicAuthentication < Endpoint before do @@ -12,9 +10,11 @@ class EndpointStrictBasicAuthentication < Endpoint # Only accepts password == "password" unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz" - halt 403, "Authentication failed" + halt 401, "Authentication failed" end end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointStrictBasicAuthentication) diff --git a/spec/bundler/support/artifice/endpoint_timeout.rb b/spec/bundler/support/artifice/endpoint_timeout.rb index c118da1893..86b793e499 100644 --- a/spec/bundler/support/artifice/endpoint_timeout.rb +++ b/spec/bundler/support/artifice/endpoint_timeout.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require_relative "endpoint_fallback" - -Artifice.deactivate +require_relative "helpers/endpoint_fallback" class EndpointTimeout < EndpointFallback SLEEP_TIMEOUT = 3 @@ -12,4 +10,6 @@ class EndpointTimeout < EndpointFallback end end +require_relative "helpers/artifice" + Artifice.activate_with(EndpointTimeout) diff --git a/spec/bundler/support/artifice/fail.rb b/spec/bundler/support/artifice/fail.rb index f69f2eccc6..8822e5b8e2 100644 --- a/spec/bundler/support/artifice/fail.rb +++ b/spec/bundler/support/artifice/fail.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true -require "net/http" +require "bundler/vendored_net_http" -# We can't use artifice here because it uses rack - -module Artifice; end # for < 2.0, Net::HTTP::Persistent::SSLReuse - -class Fail < Net::HTTP - # Net::HTTP uses a @newimpl instance variable to decide whether +class Fail < Gem::Net::HTTP + # Gem::Net::HTTP uses a @newimpl instance variable to decide whether # to use a legacy implementation. Since we are subclassing - # Net::HTTP, we must set it + # Gem::Net::HTTP, we must set it @newimpl = true def request(req, body = nil, &block) @@ -21,14 +17,11 @@ class Fail < Net::HTTP end def exception(req) - name = ENV.fetch("BUNDLER_SPEC_EXCEPTION") { "Errno::ENETUNREACH" } - const = name.split("::").reduce(Object) {|mod, sym| mod.const_get(sym) } - const.new("host down: Bundler spec artifice fail! #{req["PATH_INFO"]}") + Errno::ENETUNREACH.new("host down: Bundler spec artifice fail! #{req["PATH_INFO"]}") end end -# Replace Net::HTTP with our failing subclass -::Net.class_eval do - remove_const(:HTTP) - const_set(:HTTP, ::Fail) -end +require_relative "helpers/artifice" + +# Replace Gem::Net::HTTP with our failing subclass +Artifice.replace_net_http(::Fail) diff --git a/spec/bundler/support/artifice/helpers/artifice.rb b/spec/bundler/support/artifice/helpers/artifice.rb new file mode 100644 index 0000000000..788268295c --- /dev/null +++ b/spec/bundler/support/artifice/helpers/artifice.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# This module was initially borrowed from https://github.com/wycats/artifice +module Artifice + # Activate Artifice with a particular Rack endpoint. + # + # Calling this method will replace the Gem::Net::HTTP system + # with a replacement that routes all requests to the + # Rack endpoint. + # + # @param [#call] endpoint A valid Rack endpoint + def self.activate_with(endpoint) + require_relative "rack_request" + + Net::HTTP.endpoint = endpoint + replace_net_http(Artifice::Net::HTTP) + end + + # Deactivate the Artifice replacement. + def self.deactivate + replace_net_http(::Gem::Net::HTTP) + end + + def self.replace_net_http(value) + ::Gem::Net.class_eval do + remove_const(:HTTP) + const_set(:HTTP, value) + end + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index.rb b/spec/bundler/support/artifice/helpers/compact_index.rb new file mode 100644 index 0000000000..a803a2d30a --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +$LOAD_PATH.unshift Dir[Spec::Path.base_system_gem_path.join("gems/compact_index*/lib")].first.to_s +require "compact_index" +require "digest" + +class CompactIndexAPI < Endpoint + helpers do + include Spec::Path + + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) + end + + def etag_response + response_body = yield + etag = Digest::MD5.hexdigest(response_body) + headers "ETag" => quote(etag) + return if not_modified?(etag) + headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:" + headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60" + content_type "text/plain" + requested_range_for(response_body) + rescue StandardError => e + puts e + puts e.backtrace + raise + end + + def not_modified?(etag) + etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"]) + + return unless etags.include?(etag) + status 304 + body "" + end + + def requested_range_for(response_body) + ranges = Rack::Utils.byte_ranges(env, response_body.bytesize) + + if ranges + status 206 + body ranges.map! {|range| slice_body(response_body, range) }.join + else + status 200 + body response_body + end + end + + def quote(string) + %("#{string}") + end + + def parse_etags(value) + value ? value.split(/, ?/).select {|s| s.sub!(/"(.*)"/, '\1') } : [] + end + + def slice_body(body, range) + body.byteslice(range) + end + + def gems(gem_repo = default_gem_repo) + @gems ||= {} + @gems[gem_repo] ||= begin + specs = Bundler::Deprecate.skip_during do + %w[specs.4.8 prerelease_specs.4.8].map do |filename| + Marshal.load(File.open(gem_repo.join(filename)).read).map do |name, version, platform| + load_spec(name, version, platform, gem_repo) + end + end.flatten + end + + specs.group_by(&:name).map do |name, versions| + gem_versions = versions.map do |spec| + deps = spec.runtime_dependencies.map do |d| + reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") + CompactIndex::Dependency.new(d.name, reqs) + end + begin + checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") do + Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").hexdigest + end + rescue StandardError + checksum = nil + end + CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s) + end + CompactIndex::Gem.new(name, gem_versions) + end + end + end + end + + get "/names" do + etag_response do + CompactIndex.names(gems.map(&:name)) + end + end + + get "/versions" do + etag_response do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems) + file.contents + end + end + + get "/info/:name" do + etag_response do + gem = gems.find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra.rb b/spec/bundler/support/artifice/helpers/compact_index_extra.rb new file mode 100644 index 0000000000..9e742630dd --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_extra.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexExtra < CompactIndexAPI + get "/extra/versions" do + halt 404 + end + + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo2}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb new file mode 100644 index 0000000000..d9a7d83d23 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexExtraApi < CompactIndexAPI + get "/extra/names" do + etag_response do + CompactIndex.names(gems(gem_repo4).map(&:name)) + end + end + + get "/extra/versions" do + etag_response do + file = tmp("versions.list") + FileUtils.rm_f(file) + file = CompactIndex::VersionsFile.new(file.to_s) + file.create(gems(gem_repo4)) + file.contents + end + end + + get "/extra/info/:name" do + etag_response do + gem = gems(gem_repo4).find {|g| g.name == params[:name] } + CompactIndex.info(gem ? gem.versions : []) + end + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo4}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo4}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo4}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint.rb b/spec/bundler/support/artifice/helpers/endpoint.rb new file mode 100644 index 0000000000..83ba1be0fc --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require_relative "../../path" + +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{mustermann,rack,tilt,sinatra,ruby2_keywords,base64}-*/lib")].map(&:to_s)) + +require "sinatra/base" + +ALL_REQUESTS = [] # rubocop:disable Style/MutableConstant +ALL_REQUESTS_MUTEX = Thread::Mutex.new + +at_exit do + if expected = ENV["BUNDLER_SPEC_ALL_REQUESTS"] + expected = expected.split("\n").sort + actual = ALL_REQUESTS.sort + + unless expected == actual + raise "Unexpected requests!\nExpected:\n\t#{expected.join("\n\t")}\n\nActual:\n\t#{actual.join("\n\t")}" + end + end +end + +class Endpoint < Sinatra::Base + def self.all_requests + @all_requests ||= [] + end + + set :raise_errors, true + set :show_exceptions, false + + def call!(*) + super.tap do + ALL_REQUESTS_MUTEX.synchronize do + ALL_REQUESTS << @request.url + end + end + end + + helpers do + include Spec::Path + + def default_gem_repo + if ENV["BUNDLER_SPEC_GEM_REPO"] + Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"]) + else + case request.host + when "gem.repo1" + Spec::Path.gem_repo1 + when "gem.repo2" + Spec::Path.gem_repo2 + when "gem.repo3" + Spec::Path.gem_repo3 + when "gem.repo4" + Spec::Path.gem_repo4 + else + Spec::Path.gem_repo1 + end + end + end + + def dependencies_for(gem_names, gem_repo = default_gem_repo) + return [] if gem_names.nil? || gem_names.empty? + + all_specs = %w[specs.4.8 prerelease_specs.4.8].map do |filename| + Marshal.load(File.open(gem_repo.join(filename)).read) + end.inject(:+) + + all_specs.map do |name, version, platform| + spec = load_spec(name, version, platform, gem_repo) + next unless gem_names.include?(spec.name) + { + name: spec.name, + number: spec.version.version, + platform: spec.platform.to_s, + dependencies: spec.runtime_dependencies.map do |dep| + [dep.name, dep.requirement.requirements.map {|a| a.join(" ") }.join(", ")] + end, + } + end.compact + end + + def load_spec(name, version, platform, gem_repo) + full_name = "#{name}-#{version}" + full_name += "-#{platform}" if platform != "ruby" + Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz")))) + end + end + + get "/quick/Marshal.4.8/:id" do + redirect "/fetch/actual/gem/#{params[:id]}" + end + + get "/fetch/actual/gem/:id" do + File.binread("#{default_gem_repo}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/gems/:id" do + File.binread("#{default_gem_repo}/gems/#{params[:id]}") + end + + get "/api/v1/dependencies" do + Marshal.dump(dependencies_for(params[:gems])) + end + + get "/specs.4.8.gz" do + File.binread("#{default_gem_repo}/specs.4.8.gz") + end + + get "/prerelease_specs.4.8.gz" do + File.binread("#{default_gem_repo}/prerelease_specs.4.8.gz") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_extra.rb b/spec/bundler/support/artifice/helpers/endpoint_extra.rb new file mode 100644 index 0000000000..ad08495b50 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_extra.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +class EndpointExtra < Endpoint + get "/extra/api/v1/dependencies" do + halt 404 + end + + get "/extra/specs.4.8.gz" do + File.binread("#{gem_repo2}/specs.4.8.gz") + end + + get "/extra/prerelease_specs.4.8.gz" do + File.binread("#{gem_repo2}/prerelease_specs.4.8.gz") + end + + get "/extra/quick/Marshal.4.8/:id" do + redirect "/extra/fetch/actual/gem/#{params[:id]}" + end + + get "/extra/fetch/actual/gem/:id" do + File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}") + end + + get "/extra/gems/:id" do + File.binread("#{gem_repo2}/gems/#{params[:id]}") + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_fallback.rb b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb new file mode 100644 index 0000000000..a232930b67 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "endpoint" + +class EndpointFallback < Endpoint + DEPENDENCY_LIMIT = 60 + + get "/api/v1/dependencies" do + if params[:gems] && params[:gems].size <= DEPENDENCY_LIMIT + Marshal.dump(dependencies_for(params[:gems])) + else + halt 413, "Too many gems to resolve, please request less than #{DEPENDENCY_LIMIT} gems" + end + end +end diff --git a/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb new file mode 100644 index 0000000000..c409d39d99 --- /dev/null +++ b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative "endpoint_fallback" + +class EndpointMarshalFail < EndpointFallback + get "/api/v1/dependencies" do + "f0283y01hasf" + end +end diff --git a/spec/bundler/support/artifice/helpers/rack_request.rb b/spec/bundler/support/artifice/helpers/rack_request.rb new file mode 100644 index 0000000000..f419bacb8c --- /dev/null +++ b/spec/bundler/support/artifice/helpers/rack_request.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "rack/test" +require "bundler/vendored_net_http" + +module Artifice + module Net + # This is an internal object that can receive Rack requests + # to the application using the Rack::Test API + class RackRequest + include Rack::Test::Methods + attr_reader :app + + def initialize(app) + @app = app + end + end + + class HTTP < ::Gem::Net::HTTP + class << self + attr_accessor :endpoint + end + + # Gem::Net::HTTP uses a @newimpl instance variable to decide whether + # to use a legacy implementation. Since we are subclassing + # Gem::Net::HTTP, we must set it + @newimpl = true + + # We don't need to connect, so blank out this method + def connect + end + + # Replace the Gem::Net::HTTP request method with a method + # that converts the request into a Rack request and + # dispatches it to the Rack endpoint. + # + # @param [Net::HTTPRequest] req A Gem::Net::HTTPRequest + # object, or one if its subclasses + # @param [optional, String, #read] body This should + # be sent as "rack.input". If it's a String, it will + # be converted to a StringIO. + # @return [Net::HTTPResponse] + # + # @yield [Net::HTTPResponse] If a block is provided, + # this method will yield the Gem::Net::HTTPResponse to + # it after the body is read. + def request(req, body = nil, &block) + rack_request = RackRequest.new(self.class.endpoint) + + req.each_header do |header, value| + rack_request.header(header, value) + end + + scheme = use_ssl? ? "https" : "http" + prefix = "#{scheme}://#{addr_port}" + body_stream_contents = req.body_stream.read if req.body_stream + + response = rack_request.request("#{prefix}#{req.path}", + { method: req.method, input: body || req.body || body_stream_contents }) + + make_net_http_response(response, &block) + end + + private + + # This method takes a Rack response and creates a Gem::Net::HTTPResponse + # Instead of trying to mock HTTPResponse directly, we just convert + # the Rack response into a String that looks like a normal HTTP + # response and call Gem::Net::HTTPResponse.read_new + # + # @param [Array(#to_i, Hash, #each)] response a Rack response + # @return [Net::HTTPResponse] + # @yield [Net::HTTPResponse] If a block is provided, yield the + # response to it after the body is read + def make_net_http_response(response) + status = response.status + headers = response.headers + body = response.body + + response_string = [] + response_string << "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}" + + headers.each do |header, value| + response_string << "#{header}: #{value}" + end + + response_string << "" << body + + response_io = ::Gem::Net::BufferedIO.new(StringIO.new(response_string.join("\n"))) + res = ::Gem::Net::HTTPResponse.read_new(response_io) + + res.reading_body(response_io, true) do + yield res if block_given? + end + + res + end + end + end +end diff --git a/spec/bundler/support/artifice/vcr.rb b/spec/bundler/support/artifice/vcr.rb index ceb133346f..7b9a8bdeaf 100644 --- a/spec/bundler/support/artifice/vcr.rb +++ b/spec/bundler/support/artifice/vcr.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -require "net/http" +require "bundler/vendored_net_http" require_relative "../path" -CASSETTE_PATH = "#{Spec::Path.spec_dir}/support/artifice/vcr_cassettes" +CASSETTE_PATH = "#{Spec::Path.spec_dir}/support/artifice/vcr_cassettes".freeze +USED_CASSETTES_PATH = "#{Spec::Path.spec_dir}/support/artifice/used_cassettes.txt".freeze CASSETTE_NAME = ENV.fetch("BUNDLER_SPEC_VCR_CASSETTE_NAME") { "realworld" } -class BundlerVCRHTTP < Net::HTTP +class BundlerVCRHTTP < Gem::Net::HTTP class RequestHandler attr_reader :http, :request, :body, :response_block def initialize(http, request, body = nil, &response_block) @@ -22,6 +23,10 @@ class BundlerVCRHTTP < Net::HTTP @__vcr_request_handler = handler end + File.open(USED_CASSETTES_PATH, "a+") do |f| + f.puts request_pair_paths.map {|path| Pathname.new(path).relative_path_from(Spec::Path.source_root).to_s }.join("\n") + end + if recorded_response? recorded_response else @@ -36,13 +41,13 @@ class BundlerVCRHTTP < Net::HTTP def recorded_response File.open(request_pair_paths.last, "rb:ASCII-8BIT") do |response_file| - response_io = ::Net::BufferedIO.new(response_file) - ::Net::HTTPResponse.read_new(response_io).tap do |response| + response_io = ::Gem::Net::BufferedIO.new(response_file) + ::Gem::Net::HTTPResponse.read_new(response_io).tap do |response| response.decode_content = request.decode_content if request.respond_to?(:decode_content) response.uri = request.uri response.reading_body(response_io, request.response_body_permitted?) do - response_block.call(response) if response_block + response_block&.call(response) end end end @@ -74,25 +79,8 @@ class BundlerVCRHTTP < Net::HTTP def request_pair_paths %w[request response].map do |kind| - File.join(CASSETTE_PATH, CASSETTE_NAME, file_name_for_key(key + [kind])) - end - end - - def read_stored_request(path) - contents = File.binread(path) - headers = {} - method = nil - path = nil - contents.lines.grep(/^> /).each do |line| - if line =~ /^> (GET|HEAD|POST|PATCH|PUT|DELETE) (.*)/ - method = $1 - path = $2.strip - elsif line =~ /^> (.*?): (.*)/ - headers[$1] = $2 - end + File.join(CASSETTE_PATH, CASSETTE_NAME, file_name_for_key(key), kind) end - body = contents =~ /^([^>].*)/m && $1 - Net::HTTP.const_get(method.capitalize).new(path, headers).tap {|r| r.body = body if body } end def request_to_string(request) @@ -158,8 +146,7 @@ class BundlerVCRHTTP < Net::HTTP alias_method :request, :request_with_vcr end -# Replace Net::HTTP with our VCR subclass -::Net.class_eval do - remove_const(:HTTP) - const_set(:HTTP, BundlerVCRHTTP) -end +require_relative "helpers/artifice" + +# Replace Gem::Net::HTTP with our VCR subclass +Artifice.replace_net_http(BundlerVCRHTTP) diff --git a/spec/bundler/support/artifice/windows.rb b/spec/bundler/support/artifice/windows.rb index 674470688d..fea991c071 100644 --- a/spec/bundler/support/artifice/windows.rb +++ b/spec/bundler/support/artifice/windows.rb @@ -2,13 +2,10 @@ require_relative "../path" -$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{artifice,mustermann,rack,tilt,sinatra,ruby2_keywords}-*/lib")].map(&:to_s)) +$LOAD_PATH.unshift(*Dir[Spec::Path.base_system_gem_path.join("gems/{mustermann,rack,tilt,sinatra,ruby2_keywords,base64}-*/lib")].map(&:to_s)) -require "artifice" require "sinatra/base" -Artifice.deactivate - class Windows < Sinatra::Base set :raise_errors, true set :show_exceptions, false @@ -43,4 +40,6 @@ class Windows < Sinatra::Base end end +require_relative "helpers/artifice" + Artifice.activate_with(Windows) |