diff --git a/app/controllers/api/v1/deletions_controller.rb b/app/controllers/api/v1/deletions_controller.rb index 9f4ad880547..e2d242c5831 100644 --- a/app/controllers/api/v1/deletions_controller.rb +++ b/app/controllers/api/v1/deletions_controller.rb @@ -40,9 +40,17 @@ def validate_gem_and_version begin version = params.expect(:version) platform = params.permit(:platform).fetch(:platform, nil) - @version = @rubygem.find_version!(number: version, platform: platform) + ruby_abi = params.permit(:ruby_abi).fetch(:ruby_abi, nil).presence + + if ruby_abi.present? && platform.blank? + return render plain: response_with_mfa_warning("The platform param is required when ruby_abi is specified."), + status: :bad_request + end + + @version = @rubygem.find_version!(number: version, platform: platform, ruby_abi: ruby_abi) rescue ActiveRecord::RecordNotFound - render plain: response_with_mfa_warning("The version #{version}#{" (#{platform})" if platform.present?} does not exist."), + details = "#{" (#{platform})" if platform.present?}#{" (Ruby ABI #{ruby_abi})" if ruby_abi.present?}" + render plain: response_with_mfa_warning("The version #{version}#{details} does not exist."), status: :not_found end end diff --git a/app/models/rubygem.rb b/app/models/rubygem.rb index 0ac55bc01a7..85550542743 100644 --- a/app/models/rubygem.rb +++ b/app/models/rubygem.rb @@ -199,9 +199,9 @@ def public_version_payload(number, platform = nil) payload(version).merge!(version.as_json) if version end - def find_version!(number:, platform:) + def find_version!(number:, platform:, ruby_abi: nil) platform = platform.presence || "ruby" - versions.find_by!(number: number, platform: platform) + versions.find_by!(number: number, platform: platform, ruby_abi: ruby_abi) end def find_version_by_slug!(slug) diff --git a/test/functional/api/v1/deletions_controller_test.rb b/test/functional/api/v1/deletions_controller_test.rb index 72736637f8d..01f0f963daa 100644 --- a/test/functional/api/v1/deletions_controller_test.rb +++ b/test/functional/api/v1/deletions_controller_test.rb @@ -32,6 +32,131 @@ class Api::V1::DeletionsControllerTest < ActionController::TestCase end end + context "for a gem with content-addressable versions across Ruby ABIs" do + setup do + @rubygem = create(:rubygem, name: "sandworm") + @abi32 = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux-musl", gem_platform: "x86_64-linux-musl", + required_ruby_version: "~> 3.2.0", ruby_abi: "3.2", sha256: Digest::SHA2.base64digest("sandworm-1.0.0-3.2")) + @abi34 = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux-musl", gem_platform: "x86_64-linux-musl", + required_ruby_version: "~> 3.4.0", ruby_abi: "3.4", sha256: Digest::SHA2.base64digest("sandworm-1.0.0-3.4")) + create(:ownership, user: @user, rubygem: @rubygem) + RubygemFs.instance.store("gems/#{@abi32.full_name}.gem", "") + RubygemFs.instance.store("gems/#{@abi34.full_name}.gem", "") + end + + context "ON DELETE with a ruby_abi param" do + setup do + delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "3.2" } + end + + should respond_with :success + + should "yank only the targeted Ruby ABI variant" do + refute_predicate @abi32.reload, :indexed? + assert_predicate @abi34.reload, :indexed? + end + + should "respond with the deleted variant including its content address and Ruby ABI" do + assert_includes @response.body, "Successfully deleted gem: sandworm (1.0.0-724c5206, Platform: x86_64-linux-musl, Ruby ABI 3.2)" + end + end + + context "ON DELETE without a ruby_abi param" do + setup do + delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", platform: "x86_64-linux-musl" } + end + + should respond_with :not_found + + should "not yank either Ruby ABI variant" do + assert_predicate @abi32.reload, :indexed? + assert_predicate @abi34.reload, :indexed? + end + + should "respond that the version does not exist" do + assert_includes @response.body, "The version 1.0.0 (x86_64-linux-musl) does not exist." + end + end + + context "ON DELETE with a ruby_abi param that does not match any variant" do + setup do + delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "3.3" } + end + + should respond_with :not_found + + should "not yank either Ruby ABI variant" do + assert_predicate @abi32.reload, :indexed? + assert_predicate @abi34.reload, :indexed? + end + + should "respond that the version does not exist" do + assert_includes @response.body, "The version 1.0.0 (x86_64-linux-musl) (Ruby ABI 3.3) does not exist." + end + end + + context "ON DELETE with a ruby_abi param but no platform" do + setup do + delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", ruby_abi: "3.2" } + end + + should respond_with :bad_request + + should "not yank either Ruby ABI variant" do + assert_predicate @abi32.reload, :indexed? + assert_predicate @abi34.reload, :indexed? + end + + should "respond that the platform param is required" do + assert_includes @response.body, "The platform param is required when ruby_abi is specified." + end + end + + context "when a version supporting multiple Ruby ABIs coexists" do + setup do + @multi_abi = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux-musl", gem_platform: "x86_64-linux-musl", + required_ruby_version: ">= 3.2") + RubygemFs.instance.store("gems/#{@multi_abi.full_name}.gem", "") + end + + context "ON DELETE with only a platform param" do + setup do + delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", platform: "x86_64-linux-musl" } + end + + should respond_with :success + + should "yank only the version supporting multiple Ruby ABIs and keep the single ABI variants" do + refute_predicate @multi_abi.reload, :indexed? + assert_predicate @abi32.reload, :indexed? + assert_predicate @abi34.reload, :indexed? + end + + should "respond with the deleted platform version" do + assert_includes @response.body, "Successfully deleted gem: sandworm (1.0.0-x86_64-linux-musl)" + end + end + + context "ON DELETE with an empty ruby_abi param" do + setup do + delete :create, params: { gem_name: @rubygem.slug, version: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "" } + end + + should respond_with :success + + should "treat the empty ruby_abi as absent and yank the version supporting multiple Ruby ABIs" do + refute_predicate @multi_abi.reload, :indexed? + assert_predicate @abi32.reload, :indexed? + assert_predicate @abi34.reload, :indexed? + end + + should "respond with the deleted platform version" do + assert_includes @response.body, "Successfully deleted gem: sandworm (1.0.0-x86_64-linux-musl)" + end + end + end + end + context "for a gem SomeGem with a version 0.1.0" do setup do @rubygem = create(:rubygem, name: "SomeGem") diff --git a/test/models/rubygem_test.rb b/test/models/rubygem_test.rb index 483336d591e..e7ebaa25ab4 100644 --- a/test/models/rubygem_test.rb +++ b/test/models/rubygem_test.rb @@ -80,6 +80,19 @@ class RubygemTest < ActiveSupport::TestCase assert_equal version3_ruby, @rubygem.most_recent_version end + should "find versions by number, platform and Ruby ABI" do + plain = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux-musl", gem_platform: "x86_64-linux-musl", + required_ruby_version: ">= 3.2") + abi34 = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux-musl", gem_platform: "x86_64-linux-musl", + required_ruby_version: "~> 3.4.0", ruby_abi: "3.4", sha256: Digest::SHA2.base64digest("abi34-1.0.0")) + + assert_equal plain, @rubygem.find_version!(number: "1.0.0", platform: "x86_64-linux-musl") + assert_equal abi34, @rubygem.find_version!(number: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "3.4") + assert_raises(ActiveRecord::RecordNotFound) do + @rubygem.find_version!(number: "1.0.0", platform: "x86_64-linux-musl", ruby_abi: "3.2") + end + end + should "mark the latest version for each Ruby ABI per platform" do abi32_old = create(:version, rubygem: @rubygem, number: "1.0.0", platform: "x86_64-linux-musl", gem_platform: "x86_64-linux-musl", required_ruby_version: "~> 3.2.0", ruby_abi: "3.2", sha256: Digest::SHA2.base64digest("abi32-1.0.0"))