From 3954a87d65f004e5148597ffa927dc7b9eef6fb8 Mon Sep 17 00:00:00 2001 From: Jenny Shen Date: Tue, 11 Jul 2023 00:40:05 -0400 Subject: [PATCH] [rubygems/rubygems] Create MultifactorAuthFetcher to reduce duplication among tests https://github.com/rubygems/rubygems/commit/dead211206 --- test/rubygems/multifactor_auth_fetcher.rb | 76 +++++++++++++ .../test_gem_commands_owner_command.rb | 85 ++++---------- .../test_gem_commands_push_command.rb | 89 ++++----------- .../test_gem_commands_yank_command.rb | 106 +++++------------- test/rubygems/test_gem_gemcutter_utilities.rb | 91 ++++----------- 5 files changed, 164 insertions(+), 283 deletions(-) create mode 100644 test/rubygems/multifactor_auth_fetcher.rb diff --git a/test/rubygems/multifactor_auth_fetcher.rb b/test/rubygems/multifactor_auth_fetcher.rb new file mode 100644 index 0000000000..e90a01c5ba --- /dev/null +++ b/test/rubygems/multifactor_auth_fetcher.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +## +# A MultifactorAuthFetcher is a FakeFetcher that adds paths to data for requests related to +# multi-factor authentication. +# + +require_relative "utilities" +require "json" + +class Gem::MultifactorAuthFetcher < Gem::FakeFetcher + attr_reader :host, :webauthn_url + + # GET /api/v1/webauthn_verification defaults to user does not have any security devices + def initialize(host: nil) + super() + @host = host || Gem.host + @path_token = "odow34b93t6aPCdY" + @webauthn_url = "#{@host}/webauthn_verification/#{@path_token}" + @data["#{@host}/api/v1/webauthn_verification"] = Gem::HTTPResponseFactory.create( + body: "You don't have any security devices", + code: 422, + msg: "Unprocessable Entity" + ) + end + + # given a url, return a response that requires multifactor authentication + def respond_with_require_otp(url, success_body) + response_fail = "You have enabled multifactor authentication" + + @data[url] = proc do + @call_count ||= 0 + if (@call_count += 1).odd? + Gem::HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized") + else + Gem::HTTPResponseFactory.create(body: success_body, code: 200, msg: "OK") + end + end + end + + # GET /api/v1/webauthn_verification returns a webauthn url + # GET /api/v1/webauthn_verification/:token/status.json (polling url) returns pending status + def respond_with_webauthn_url + @data["#{@host}/api/v1/webauthn_verification"] = Gem::HTTPResponseFactory.create(body: @webauthn_url, code: 200, msg: "OK") + @data["#{@host}/api/v1/webauthn_verification/#{@path_token}/status.json"] = Gem::HTTPResponseFactory.create( + body: { status: "pending", message: "Security device authentication is still pending." }.to_json, + code: 200, + msg: "OK" + ) + end + + # GET /api/v1/webauthn_verification/:token/status.json returns success status with OTP code + def respond_with_webauthn_polling(code) + @data["#{@host}/api/v1/webauthn_verification/#{@path_token}/status.json"] = Gem::HTTPResponseFactory.create( + body: { status: "success", code: code }.to_json, + code: 200, + msg: "OK" + ) + end + + # GET /api/v1/webauthn_verification/:token/status.json returns expired status + def respond_with_webauthn_polling_failure + @data["#{@host}/api/v1/webauthn_verification/#{@path_token}/status.json"] = Gem::HTTPResponseFactory.create( + body: { + status: "expired", + message: "The token in the link you used has either expired or been used already.", + }.to_json, + code: 200, + msg: "OK" + ) + end + + def webauthn_url_with_port(port) + "#{@webauthn_url}?port=#{port}" + end +end diff --git a/test/rubygems/test_gem_commands_owner_command.rb b/test/rubygems/test_gem_commands_owner_command.rb index fd9b1445a7..d737506ad2 100644 --- a/test/rubygems/test_gem_commands_owner_command.rb +++ b/test/rubygems/test_gem_commands_owner_command.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "helper" +require_relative "multifactor_auth_fetcher" require "rubygems/commands/owner_command" class TestGemCommandsOwnerCommand < Gem::TestCase @@ -11,7 +12,7 @@ class TestGemCommandsOwnerCommand < Gem::TestCase ENV["RUBYGEMS_HOST"] = nil @stub_ui = Gem::MockGemUi.new - @stub_fetcher = Gem::FakeFetcher.new + @stub_fetcher = Gem::MultifactorAuthFetcher.new Gem::RemoteFetcher.fetcher = @stub_fetcher Gem.configuration = nil Gem.configuration.rubygems_api_key = "ed244fbf2b1a52e012da8616c512fa47f9aa5250" @@ -324,15 +325,8 @@ EOF end def test_otp_verified_success - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_success = "Owner added successfully." - - @stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), - ] - @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = - HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") + @stub_fetcher.respond_with_require_otp("#{Gem.host}/api/v1/gems/freewill/owners", response_success) @otp_ui = Gem::MockGemUi.new "111111\n" use_ui @otp_ui do @@ -363,22 +357,12 @@ EOF end def test_with_webauthn_enabled_success - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_success = "Owner added successfully." port = 5678 server = TCPServer.new(port) - @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), - ] - @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}", - code: 200, - msg: "OK" - ) + @stub_fetcher.respond_with_require_otp("#{Gem.host}/api/v1/gems/freewill/owners", response_success) + @stub_fetcher.respond_with_webauthn_url TCPServer.stub(:new, server) do Gem::GemcutterUtilities::WebauthnListener.stub(:listener_thread, Thread.new { Thread.current[:otp] = "Uvh6T57tkWuUnWYo" }) do @@ -390,31 +374,20 @@ EOF server.close end - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @stub_ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@stub_fetcher.webauthn_url_with_port(port)} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @stub_ui.output assert_match "You are verified with a security device. You may close the browser window.", @stub_ui.output assert_equal "Uvh6T57tkWuUnWYo", @stub_fetcher.last_request["OTP"] assert_match response_success, @stub_ui.output end def test_with_webauthn_enabled_failure - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_success = "Owner added successfully." port = 5678 server = TCPServer.new(port) error = Gem::WebauthnVerificationError.new("Something went wrong") - @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), - ] - @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}", - code: 200, - msg: "OK" - ) + @stub_fetcher.respond_with_require_otp("#{Gem.host}/api/v1/gems/freewill/owners", response_success) + @stub_fetcher.respond_with_webauthn_url TCPServer.stub(:new, server) do Gem::GemcutterUtilities::WebauthnListener.stub(:listener_thread, Thread.new { Thread.current[:error] = error }) do @@ -426,32 +399,21 @@ EOF server.close end - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match @stub_fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @stub_ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@stub_fetcher.webauthn_url_with_port(port)} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @stub_ui.output assert_match "ERROR: Security device verification failed: Something went wrong", @stub_ui.error refute_match "You are verified with a security device. You may close the browser window.", @stub_ui.output refute_match response_success, @stub_ui.output end def test_with_webauthn_enabled_success_with_polling - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_success = "Owner added successfully." port = 5678 server = TCPServer.new(port) - @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), - ] - @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"success\",\"code\":\"Uvh6T57tkWuUnWYo\"}", - code: 200, - msg: "OK" - ) + @stub_fetcher.respond_with_require_otp("#{Gem.host}/api/v1/gems/freewill/owners", response_success) + @stub_fetcher.respond_with_webauthn_url + @stub_fetcher.respond_with_webauthn_polling("Uvh6T57tkWuUnWYo") TCPServer.stub(:new, server) do use_ui @stub_ui do @@ -461,8 +423,7 @@ EOF server.close end - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate " \ + assert_match "You have enabled multi-factor authentication. Please visit #{@stub_fetcher.webauthn_url_with_port(port)} to authenticate " \ "via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin " \ "command with the `--otp [your_code]` option.", @stub_ui.output assert_match "You are verified with a security device. You may close the browser window.", @stub_ui.output @@ -471,22 +432,16 @@ EOF end def test_with_webauthn_enabled_failure_with_polling - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_success = "Owner added successfully." port = 5678 server = TCPServer.new(port) - @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), - ] - @stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"expired\",\"message\":\"The token in the link you used has either expired or been used already.\"}", - code: 200, - msg: "OK" + @stub_fetcher.respond_with_require_otp( + "#{Gem.host}/api/v1/gems/freewill/owners", + response_success ) + @stub_fetcher.respond_with_webauthn_url + @stub_fetcher.respond_with_webauthn_polling_failure TCPServer.stub(:new, server) do use_ui @stub_ui do @@ -496,10 +451,8 @@ EOF server.close end - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match @stub_fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate " \ + assert_match "You have enabled multi-factor authentication. Please visit #{@stub_fetcher.webauthn_url_with_port(port)} to authenticate " \ "via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin " \ "command with the `--otp [your_code]` option.", @stub_ui.output assert_match "ERROR: Security device verification failed: The token in the link you used has either expired " \ diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index b364c34865..1f003f6ac6 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "helper" +require_relative "multifactor_auth_fetcher" require "rubygems/commands/push_command" require "rubygems/config_file" @@ -26,7 +27,7 @@ class TestGemCommandsPushCommand < Gem::TestCase @host = "https://rubygems.example" @api_key = Gem.configuration.rubygems_api_key - @fetcher = Gem::FakeFetcher.new + @fetcher = Gem::MultifactorAuthFetcher.new Gem::RemoteFetcher.fetcher = @fetcher @cmd = Gem::Commands::PushCommand.new @@ -386,15 +387,9 @@ class TestGemCommandsPushCommand < Gem::TestCase end def test_otp_verified_success - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_success = "Successfully registered gem: freewill (1.0.0)" - @fetcher.data["#{Gem.host}/api/v1/gems"] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), - ] - @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = - HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") + @fetcher.respond_with_require_otp("#{Gem.host}/api/v1/gems", response_success) @otp_ui = Gem::MockGemUi.new "111111\n" use_ui @otp_ui do @@ -427,22 +422,12 @@ class TestGemCommandsPushCommand < Gem::TestCase end def test_with_webauthn_enabled_success - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_success = "Successfully registered gem: freewill (1.0.0)" port = 5678 server = TCPServer.new(port) - @fetcher.data["#{Gem.host}/api/v1/gems"] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), - ] - @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}", - code: 200, - msg: "OK" - ) + @fetcher.respond_with_require_otp("#{Gem.host}/api/v1/gems", response_success) + @fetcher.respond_with_webauthn_url TCPServer.stub(:new, server) do Gem::GemcutterUtilities::WebauthnListener.stub(:listener_thread, Thread.new { Thread.current[:otp] = "Uvh6T57tkWuUnWYo" }) do @@ -454,31 +439,22 @@ class TestGemCommandsPushCommand < Gem::TestCase server.close end - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output assert_match "You are verified with a security device. You may close the browser window.", @ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] assert_match response_success, @ui.output end def test_with_webauthn_enabled_failure - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_success = "Successfully registered gem: freewill (1.0.0)" port = 5678 server = TCPServer.new(port) error = Gem::WebauthnVerificationError.new("Something went wrong") - @fetcher.data["#{Gem.host}/api/v1/gems"] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), - ] - @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}", - code: 200, - msg: "OK" - ) + @fetcher.respond_with_require_otp("#{Gem.host}/api/v1/gems", response_success) + @fetcher.respond_with_webauthn_url error = assert_raise Gem::MockGemUi::TermError do TCPServer.stub(:new, server) do @@ -494,30 +470,22 @@ class TestGemCommandsPushCommand < Gem::TestCase assert_equal 1, error.exit_code assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output assert_match "ERROR: Security device verification failed: Something went wrong", @ui.error refute_match "You are verified with a security device. You may close the browser window.", @ui.output refute_match response_success, @ui.output end def test_with_webauthn_enabled_success_with_polling - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_success = "Successfully registered gem: freewill (1.0.0)" port = 5678 server = TCPServer.new(port) - @fetcher.data["#{Gem.host}/api/v1/gems"] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), - ] - @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"success\",\"code\":\"Uvh6T57tkWuUnWYo\"}", - code: 200, - msg: "OK" - ) + @fetcher.respond_with_require_otp("#{Gem.host}/api/v1/gems", response_success) + @fetcher.respond_with_webauthn_url + @fetcher.respond_with_webauthn_polling("Uvh6T57tkWuUnWYo") TCPServer.stub(:new, server) do use_ui @ui do @@ -527,32 +495,22 @@ class TestGemCommandsPushCommand < Gem::TestCase server.close end - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate " \ - "via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin " \ - "command with the `--otp [your_code]` option.", @ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output assert_match "You are verified with a security device. You may close the browser window.", @ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] assert_match response_success, @ui.output end def test_with_webauthn_enabled_failure_with_polling - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_success = "Successfully registered gem: freewill (1.0.0)" port = 5678 server = TCPServer.new(port) - @fetcher.data["#{Gem.host}/api/v1/gems"] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"), - ] - @fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"expired\",\"message\":\"The token in the link you used has either expired or been used already.\"}", - code: 200, - msg: "OK" - ) + @fetcher.respond_with_require_otp("#{Gem.host}/api/v1/gems", response_success) + @fetcher.respond_with_webauthn_url + @fetcher.respond_with_webauthn_polling_failure error = assert_raise Gem::MockGemUi::TermError do TCPServer.stub(:new, server) do @@ -566,8 +524,7 @@ class TestGemCommandsPushCommand < Gem::TestCase assert_equal 1, error.exit_code assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate " \ + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} to authenticate " \ "via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin " \ "command with the `--otp [your_code]` option.", @ui.output assert_match "ERROR: Security device verification failed: The token in the link you used has either expired " \ diff --git a/test/rubygems/test_gem_commands_yank_command.rb b/test/rubygems/test_gem_commands_yank_command.rb index 6a038c6ccb..dcd7ba1a83 100644 --- a/test/rubygems/test_gem_commands_yank_command.rb +++ b/test/rubygems/test_gem_commands_yank_command.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "helper" +require_relative "multifactor_auth_fetcher" require "rubygems/commands/yank_command" class TestGemCommandsYankCommand < Gem::TestCase @@ -12,7 +13,8 @@ class TestGemCommandsYankCommand < Gem::TestCase @cmd = Gem::Commands::YankCommand.new @cmd.options[:host] = "http://example" - @fetcher = Gem::RemoteFetcher.fetcher + @fetcher = Gem::MultifactorAuthFetcher.new(host: "http://example") + Gem::RemoteFetcher.fetcher = @fetcher Gem.configuration.rubygems_api_key = "key" Gem.configuration.api_keys[:KEY] = "other" @@ -73,9 +75,6 @@ class TestGemCommandsYankCommand < Gem::TestCase HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"), ] - webauthn_uri = "http://example/api/v1/webauthn_verification" - @fetcher.data[webauthn_uri] = - HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") @cmd.options[:args] = %w[a] @cmd.options[:added_platform] = true @@ -97,9 +96,6 @@ class TestGemCommandsYankCommand < Gem::TestCase response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." yank_uri = "http://example/api/v1/gems/yank" @fetcher.data[yank_uri] = HTTPResponseFactory.create(body: response, code: 401, msg: "Unauthorized") - webauthn_uri = "http://example/api/v1/webauthn_verification" - @fetcher.data[webauthn_uri] = - HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity") @cmd.options[:args] = %w[a] @cmd.options[:added_platform] = true @@ -117,24 +113,11 @@ class TestGemCommandsYankCommand < Gem::TestCase end def test_with_webauthn_enabled_success - webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." - yank_uri = "http://example/api/v1/gems/yank" - webauthn_uri = "http://example/api/v1/webauthn_verification" - status_uri = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json" port = 5678 server = TCPServer.new(port) - @fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @fetcher.data[yank_uri] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"), - ] - @fetcher.data[status_uri] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}", - code: 200, - msg: "OK" - ) + @fetcher.respond_with_require_otp("http://example/api/v1/gems/yank", "Successfully yanked") + @fetcher.respond_with_webauthn_url @cmd.options[:args] = %w[a] @cmd.options[:added_platform] = true @@ -150,34 +133,22 @@ class TestGemCommandsYankCommand < Gem::TestCase server.close end - url_with_port = "#{webauthn_verification_url}?port=#{port}" assert_match %r{Yanking gem from http://example}, @ui.output - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output assert_match "You are verified with a security device. You may close the browser window.", @ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] assert_match "Successfully yanked", @ui.output end def test_with_webauthn_enabled_failure - webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." - yank_uri = "http://example/api/v1/gems/yank" - webauthn_uri = "http://example/api/v1/webauthn_verification" - status_uri = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json" port = 5678 server = TCPServer.new(port) error = Gem::WebauthnVerificationError.new("Something went wrong") - @fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @fetcher.data[yank_uri] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"), - ] - @fetcher.data[status_uri] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}", - code: 200, - msg: "OK" - ) + @fetcher.respond_with_require_otp("http://example/api/v1/gems/yank", "Successfully yanked") + @fetcher.respond_with_webauthn_url @cmd.options[:args] = %w[a] @cmd.options[:added_platform] = true @@ -196,35 +167,23 @@ class TestGemCommandsYankCommand < Gem::TestCase end assert_equal 1, error.exit_code - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key assert_match %r{Yanking gem from http://example}, @ui.output - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output assert_match "ERROR: Security device verification failed: Something went wrong", @ui.error refute_match "You are verified with a security device. You may close the browser window.", @ui.output refute_match "Successfully yanked", @ui.output end def test_with_webauthn_enabled_success_with_polling - webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." - yank_uri = "http://example/api/v1/gems/yank" - webauthn_uri = "http://example/api/v1/webauthn_verification" - status_uri = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json" port = 5678 server = TCPServer.new(port) - @fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @fetcher.data[yank_uri] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"), - ] - @fetcher.data[status_uri] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"success\",\"code\":\"Uvh6T57tkWuUnWYo\"}", - code: 200, - msg: "OK" - ) + @fetcher.respond_with_require_otp("http://example/api/v1/gems/yank", "Successfully yanked") + @fetcher.respond_with_webauthn_url + @fetcher.respond_with_webauthn_polling("Uvh6T57tkWuUnWYo") @cmd.options[:args] = %w[a] @cmd.options[:added_platform] = true @@ -238,35 +197,22 @@ class TestGemCommandsYankCommand < Gem::TestCase server.close end - url_with_port = "#{webauthn_verification_url}?port=#{port}" assert_match %r{Yanking gem from http://example}, @ui.output - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate " \ - "via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin " \ - "command with the `--otp [your_code]` option.", @ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output assert_match "You are verified with a security device. You may close the browser window.", @ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] assert_match "Successfully yanked", @ui.output end def test_with_webauthn_enabled_failure_with_polling - webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY" - response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." - yank_uri = "http://example/api/v1/gems/yank" - webauthn_uri = "http://example/api/v1/webauthn_verification" - status_uri = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json" port = 5678 server = TCPServer.new(port) - @fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK") - @fetcher.data[yank_uri] = [ - HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"), - HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"), - ] - @fetcher.data[status_uri] = Gem::HTTPResponseFactory.create( - body: "{\"status\":\"expired\",\"message\":\"The token in the link you used has either expired or been used already.\"}", - code: 200, - msg: "OK" - ) + @fetcher.respond_with_require_otp("http://example/api/v1/gems/yank", "Successfully yanked") + @fetcher.respond_with_webauthn_url + @fetcher.respond_with_webauthn_polling_failure @cmd.options[:args] = %w[a] @cmd.options[:added_platform] = true @@ -283,13 +229,11 @@ class TestGemCommandsYankCommand < Gem::TestCase end assert_equal 1, error.exit_code - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key assert_match %r{Yanking gem from http://example}, @ui.output - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate " \ - "via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin " \ - "command with the `--otp [your_code]` option.", @ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output assert_match "ERROR: Security device verification failed: The token in the link you used has either expired " \ "or been used already.", @ui.error refute_match "You are verified with a security device. You may close the browser window.", @ui.output diff --git a/test/rubygems/test_gem_gemcutter_utilities.rb b/test/rubygems/test_gem_gemcutter_utilities.rb index e9041c8d59..8282eb641b 100644 --- a/test/rubygems/test_gem_gemcutter_utilities.rb +++ b/test/rubygems/test_gem_gemcutter_utilities.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "helper" +require_relative "multifactor_auth_fetcher" require "rubygems" require "rubygems/command" require "rubygems/gemcutter_utilities" @@ -222,12 +223,11 @@ class TestGemGemcutterUtilities < Gem::TestCase end def test_sign_in_with_webauthn_enabled - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" port = 5678 server = TCPServer.new(port) @fetcher.respond_with_require_otp - @fetcher.respond_with_webauthn_url(webauthn_verification_url) + @fetcher.respond_with_webauthn_url TCPServer.stub(:new, server) do Gem::GemcutterUtilities::WebauthnListener.stub(:listener_thread, Thread.new { Thread.current[:otp] = "Uvh6T57tkWuUnWYo" }) do util_sign_in @@ -236,20 +236,20 @@ class TestGemGemcutterUtilities < Gem::TestCase server.close end - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output assert_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] end def test_sign_in_with_webauthn_enabled_with_error - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" port = 5678 server = TCPServer.new(port) error = Gem::WebauthnVerificationError.new("Something went wrong") @fetcher.respond_with_require_otp - @fetcher.respond_with_webauthn_url(webauthn_verification_url) + @fetcher.respond_with_webauthn_url error = assert_raise Gem::MockGemUi::TermError do TCPServer.stub(:new, server) do Gem::GemcutterUtilities::WebauthnListener.stub(:listener_thread, Thread.new { Thread.current[:error] = error }) do @@ -261,19 +261,19 @@ class TestGemGemcutterUtilities < Gem::TestCase end assert_equal 1, error.exit_code - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output assert_match "ERROR: Security device verification failed: Something went wrong", @sign_in_ui.error refute_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output refute_match "Signed in with API key:", @sign_in_ui.output end def test_sign_in_with_webauthn_enabled_with_polling - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" port = 5678 server = TCPServer.new(port) @fetcher.respond_with_require_otp - @fetcher.respond_with_webauthn_url(webauthn_verification_url) + @fetcher.respond_with_webauthn_url @fetcher.respond_with_webauthn_polling("Uvh6T57tkWuUnWYo") TCPServer.stub(:new, server) do @@ -282,20 +282,18 @@ class TestGemGemcutterUtilities < Gem::TestCase server.close end - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate " \ - "via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin " \ - "command with the `--otp [your_code]` option.", @sign_in_ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output assert_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] end def test_sign_in_with_webauthn_enabled_with_polling_failure - webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY" port = 5678 server = TCPServer.new(port) @fetcher.respond_with_require_otp - @fetcher.respond_with_webauthn_url(webauthn_verification_url) + @fetcher.respond_with_webauthn_url @fetcher.respond_with_webauthn_polling_failure assert_raise Gem::MockGemUi::TermError do @@ -306,10 +304,9 @@ class TestGemGemcutterUtilities < Gem::TestCase end end - url_with_port = "#{webauthn_verification_url}?port=#{port}" - assert_match "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate " \ - "via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin " \ - "command with the `--otp [your_code]` option.", @sign_in_ui.output + assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(port)} " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output assert_match "ERROR: Security device verification failed: " \ "The token in the link you used has either expired or been used already.", @sign_in_ui.error end @@ -349,64 +346,18 @@ class TestGemGemcutterUtilities < Gem::TestCase end end - class SignInFetcher < Gem::FakeFetcher - attr_reader :host, :api_key + class SignInFetcher < Gem::MultifactorAuthFetcher + attr_reader :api_key def initialize(host: nil) - super() - @host = host || Gem.host + super(host: host) @api_key = "a5fdbb6ba150cbb83aad2bb2fede64cf040453903" @data["#{@host}/api/v1/api_key"] = Gem::HTTPResponseFactory.create(body: @api_key, code: 200, msg: "OK") @data["#{@host}/api/v1/profile/me.yaml"] = Gem::HTTPResponseFactory.create(body: "mfa: disabled\n", code: 200, msg: "OK") - @data["#{@host}/api/v1/webauthn_verification"] = Gem::HTTPResponseFactory.create( - body: "You don't have any security devices", - code: 422, - msg: "Unprocessable Entity" - ) - end - - def respond_with_webauthn_url(url) - require "json" - @data["#{@host}/api/v1/webauthn_verification"] = Gem::HTTPResponseFactory.create(body: url, code: 200, msg: "OK") - @data["#{@host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create( - body: { status: "pending", message: "Security device authentication is still pending." }.to_json, - code: 200, - msg: "OK" - ) - end - - def respond_with_webauthn_polling(code) - require "json" - @data["#{@host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create( - body: { status: "success", code: code }.to_json, - code: 200, - msg: "OK" - ) - end - - def respond_with_webauthn_polling_failure - require "json" - @data["#{@host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create( - body: { - status: "expired", - message: "The token in the link you used has either expired or been used already.", - }.to_json, - code: 200, - msg: "OK" - ) end def respond_with_require_otp - response_fail = "You have enabled multifactor authentication" - - @data["#{host}/api/v1/api_key"] = proc do - @call_count ||= 0 - if (@call_count += 1).odd? - Gem::HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized") - else - Gem::HTTPResponseFactory.create(body: @api_key, code: 200, msg: "OK") - end - end + super("#{host}/api/v1/api_key", @api_key) end def respond_with_forbidden_api_key_response