rubygems-update 3.4.10 → 3.4.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -1
  3. data/CONTRIBUTING.md +1 -1
  4. data/Manifest.txt +6 -0
  5. data/bundler/CHANGELOG.md +58 -0
  6. data/bundler/exe/bundle +5 -13
  7. data/bundler/lib/bundler/build_metadata.rb +2 -2
  8. data/bundler/lib/bundler/definition.rb +76 -36
  9. data/bundler/lib/bundler/gem_version_promoter.rb +1 -1
  10. data/bundler/lib/bundler/installer.rb +1 -1
  11. data/bundler/lib/bundler/lazy_specification.rb +1 -1
  12. data/bundler/lib/bundler/lockfile_generator.rb +1 -1
  13. data/bundler/lib/bundler/lockfile_parser.rb +1 -0
  14. data/bundler/lib/bundler/man/bundle-cache.1 +2 -2
  15. data/bundler/lib/bundler/man/bundle-cache.1.ronn +2 -2
  16. data/bundler/lib/bundler/resolver/base.rb +1 -3
  17. data/bundler/lib/bundler/resolver.rb +16 -2
  18. data/bundler/lib/bundler/ruby_version.rb +1 -1
  19. data/bundler/lib/bundler/rubygems_ext.rb +5 -3
  20. data/bundler/lib/bundler/runtime.rb +1 -1
  21. data/bundler/lib/bundler/safe_marshal.rb +31 -0
  22. data/bundler/lib/bundler/settings.rb +3 -2
  23. data/bundler/lib/bundler/source/rubygems.rb +12 -13
  24. data/bundler/lib/bundler/spec_set.rb +2 -2
  25. data/bundler/lib/bundler/templates/newgem/bin/console.tt +0 -4
  26. data/bundler/lib/bundler/templates/newgem/ext/newgem/extconf-c.rb.tt +5 -0
  27. data/bundler/lib/bundler/templates/newgem/ext/newgem/newgem.c.tt +1 -1
  28. data/bundler/lib/bundler/templates/newgem/newgem.gemspec.tt +2 -1
  29. data/bundler/lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb +9 -4
  30. data/bundler/lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb +2 -2
  31. data/bundler/lib/bundler/vendor/uri/lib/uri/version.rb +1 -1
  32. data/bundler/lib/bundler/version.rb +1 -1
  33. data/bundler/lib/bundler.rb +8 -16
  34. data/lib/rubygems/command_manager.rb +2 -2
  35. data/lib/rubygems/commands/owner_command.rb +4 -2
  36. data/lib/rubygems/exceptions.rb +10 -0
  37. data/lib/rubygems/ext/builder.rb +3 -4
  38. data/lib/rubygems/ext/cargo_builder.rb +2 -2
  39. data/lib/rubygems/ext/rake_builder.rb +4 -2
  40. data/lib/rubygems/gemcutter_utilities.rb +48 -6
  41. data/lib/rubygems/installer.rb +16 -1
  42. data/lib/rubygems/request_set.rb +2 -2
  43. data/lib/rubygems/shellwords.rb +3 -0
  44. data/lib/rubygems/specification.rb +3 -1
  45. data/lib/rubygems/stub_specification.rb +2 -1
  46. data/lib/rubygems/webauthn_listener/response.rb +161 -0
  47. data/lib/rubygems/webauthn_listener.rb +92 -0
  48. data/lib/rubygems.rb +1 -1
  49. data/rubygems-update.gemspec +4 -3
  50. data/test/rubygems/helper.rb +14 -0
  51. data/test/rubygems/test_bundled_ca.rb +1 -1
  52. data/test/rubygems/test_config.rb +1 -1
  53. data/test/rubygems/test_deprecate.rb +1 -1
  54. data/test/rubygems/test_exit.rb +1 -1
  55. data/test/rubygems/test_gem.rb +7 -0
  56. data/test/rubygems/test_gem_commands_owner_command.rb +67 -0
  57. data/test/rubygems/test_gem_commands_pristine_command.rb +1 -1
  58. data/test/rubygems/test_gem_commands_push_command.rb +73 -0
  59. data/test/rubygems/test_gem_commands_setup_command.rb +1 -1
  60. data/test/rubygems/test_gem_commands_yank_command.rb +84 -0
  61. data/test/rubygems/test_gem_ext_cargo_builder.rb +1 -0
  62. data/test/rubygems/test_gem_gem_runner.rb +5 -5
  63. data/test/rubygems/test_gem_gemcutter_utilities.rb +72 -4
  64. data/test/rubygems/test_gem_installer.rb +50 -2
  65. data/test/rubygems/test_gem_uninstaller.rb +4 -4
  66. data/test/rubygems/test_kernel.rb +1 -1
  67. data/test/rubygems/test_project_sanity.rb +32 -3
  68. data/test/rubygems/test_remote_fetch_error.rb +1 -1
  69. data/test/rubygems/test_webauthn_listener.rb +120 -0
  70. data/test/rubygems/test_webauthn_listener_response.rb +93 -0
  71. data/test/rubygems/utilities.rb +44 -3
  72. metadata +14 -6
@@ -72,6 +72,9 @@ class TestGemCommandsYankCommand < Gem::TestCase
72
72
  HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
73
73
  HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
74
74
  ]
75
+ webauthn_uri = "http://example/api/v1/webauthn_verification"
76
+ @fetcher.data[webauthn_uri] =
77
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
75
78
 
76
79
  @cmd.options[:args] = %w[a]
77
80
  @cmd.options[:added_platform] = true
@@ -93,6 +96,9 @@ class TestGemCommandsYankCommand < Gem::TestCase
93
96
  response = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
94
97
  yank_uri = "http://example/api/v1/gems/yank"
95
98
  @fetcher.data[yank_uri] = HTTPResponseFactory.create(body: response, code: 401, msg: "Unauthorized")
99
+ webauthn_uri = "http://example/api/v1/webauthn_verification"
100
+ @fetcher.data[webauthn_uri] =
101
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
96
102
 
97
103
  @cmd.options[:args] = %w[a]
98
104
  @cmd.options[:added_platform] = true
@@ -109,6 +115,84 @@ class TestGemCommandsYankCommand < Gem::TestCase
109
115
  assert_equal "111111", @fetcher.last_request["OTP"]
110
116
  end
111
117
 
118
+ def test_with_webauthn_enabled_success
119
+ webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY"
120
+ response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
121
+ yank_uri = "http://example/api/v1/gems/yank"
122
+ webauthn_uri = "http://example/api/v1/webauthn_verification"
123
+ port = 5678
124
+ server = TCPServer.new(port)
125
+
126
+ @fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
127
+ @fetcher.data[yank_uri] = [
128
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
129
+ HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
130
+ ]
131
+
132
+ @cmd.options[:args] = %w[a]
133
+ @cmd.options[:added_platform] = true
134
+ @cmd.options[:version] = req("= 1.0")
135
+
136
+ TCPServer.stub(:new, server) do
137
+ Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
138
+ use_ui @ui do
139
+ @cmd.execute
140
+ end
141
+ end
142
+ ensure
143
+ server.close
144
+ end
145
+
146
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
147
+ assert_match %r{Yanking gem from http://example}, @ui.output
148
+ 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
149
+ assert_match "You are verified with a security device. You may close the browser window.", @ui.output
150
+ assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
151
+ assert_match "Successfully yanked", @ui.output
152
+ end
153
+
154
+ def test_with_webauthn_enabled_failure
155
+ webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY"
156
+ response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
157
+ yank_uri = "http://example/api/v1/gems/yank"
158
+ webauthn_uri = "http://example/api/v1/webauthn_verification"
159
+ port = 5678
160
+ server = TCPServer.new(port)
161
+ raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
162
+
163
+ @fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
164
+ @fetcher.data[yank_uri] = [
165
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
166
+ HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
167
+ ]
168
+
169
+ @cmd.options[:args] = %w[a]
170
+ @cmd.options[:added_platform] = true
171
+ @cmd.options[:version] = req("= 1.0")
172
+
173
+ error = assert_raise Gem::MockGemUi::TermError do
174
+ TCPServer.stub(:new, server) do
175
+ Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
176
+ use_ui @ui do
177
+ @cmd.execute
178
+ end
179
+ end
180
+ ensure
181
+ server.close
182
+ end
183
+ end
184
+ assert_equal 1, error.exit_code
185
+
186
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
187
+
188
+ assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key
189
+ assert_match %r{Yanking gem from http://example}, @ui.output
190
+ 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
191
+ assert_match "ERROR: Security device verification failed: Something went wrong", @ui.error
192
+ refute_match "You are verified with a security device. You may close the browser window.", @ui.output
193
+ refute_match "Successfully yanked", @ui.output
194
+ end
195
+
112
196
  def test_execute_key
113
197
  yank_uri = "http://example/api/v1/gems/yank"
114
198
  @fetcher.data[yank_uri] = HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK")
@@ -145,6 +145,7 @@ class TestGemExtCargoBuilder < Gem::TestCase
145
145
  system(@rust_envs, "cargo", "-V", out: IO::NULL, err: [:child, :out])
146
146
  pend "cargo not present" unless $?.success?
147
147
  pend "ruby.h is not provided by ruby repo" if ruby_repo?
148
+ pend "rust toolchain of mingw is broken" if mingw_windows?
148
149
  end
149
150
 
150
151
  def assert_ffi_handle(bundle, name)
@@ -3,9 +3,6 @@ require_relative "helper"
3
3
 
4
4
  class TestGemGemRunner < Gem::TestCase
5
5
  def setup
6
- @orig_gem_home = ENV["GEM_HOME"]
7
- ENV["GEM_HOME"] = @gemhome
8
-
9
6
  require "rubygems/command"
10
7
  @orig_args = Gem::Command.build_args
11
8
  @orig_specific_extra_args = Gem::Command.specific_extra_args_hash.dup
@@ -13,18 +10,21 @@ class TestGemGemRunner < Gem::TestCase
13
10
 
14
11
  super
15
12
 
13
+ @orig_gem_home = ENV["GEM_HOME"]
14
+ ENV["GEM_HOME"] = @gemhome
15
+
16
16
  require "rubygems/gem_runner"
17
17
  @runner = Gem::GemRunner.new
18
18
  end
19
19
 
20
20
  def teardown
21
+ ENV["GEM_HOME"] = @orig_gem_home
22
+
21
23
  super
22
24
 
23
25
  Gem::Command.build_args = @orig_args
24
26
  Gem::Command.specific_extra_args_hash = @orig_specific_extra_args
25
27
  Gem::Command.extra_args = @orig_extra_args
26
-
27
- ENV["GEM_HOME"] = @orig_gem_home
28
28
  end
29
29
 
30
30
  def test_do_configuration
@@ -230,10 +230,77 @@ class TestGemGemcutterUtilities < Gem::TestCase
230
230
  assert_equal "111111", @fetcher.last_request["OTP"]
231
231
  end
232
232
 
233
- def util_sign_in(response, host = nil, args = [], extra_input = "")
234
- email = "you@example.com"
235
- password = "secret"
236
- profile_response = HTTPResponseFactory.create(body: "mfa: disabled\n", code: 200, msg: "OK")
233
+ def test_sign_in_with_webauthn_enabled
234
+ webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
235
+ response_fail = "You have enabled multifactor authentication"
236
+ api_key = "a5fdbb6ba150cbb83aad2bb2fede64cf040453903"
237
+ port = 5678
238
+ server = TCPServer.new(port)
239
+
240
+ TCPServer.stub(:new, server) do
241
+ Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
242
+ util_sign_in(proc do
243
+ @call_count ||= 0
244
+ if (@call_count += 1).odd?
245
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized")
246
+ else
247
+ HTTPResponseFactory.create(body: api_key, code: 200, msg: "OK")
248
+ end
249
+ end, nil, [], "", webauthn_verification_url)
250
+ end
251
+ ensure
252
+ server.close
253
+ end
254
+
255
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
256
+ 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
257
+ assert_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output
258
+ assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
259
+ end
260
+
261
+ def test_sign_in_with_webauthn_enabled_with_error
262
+ webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
263
+ response_fail = "You have enabled multifactor authentication"
264
+ api_key = "a5fdbb6ba150cbb83aad2bb2fede64cf040453903"
265
+ port = 5678
266
+ server = TCPServer.new(port)
267
+ raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
268
+
269
+ error = assert_raise Gem::MockGemUi::TermError do
270
+ TCPServer.stub(:new, server) do
271
+ Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
272
+ util_sign_in(proc do
273
+ @call_count ||= 0
274
+ if (@call_count += 1).odd?
275
+ HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized")
276
+ else
277
+ HTTPResponseFactory.create(body: api_key, code: 200, msg: "OK")
278
+ end
279
+ end, nil, [], "", webauthn_verification_url)
280
+ end
281
+ ensure
282
+ server.close
283
+ end
284
+ end
285
+ assert_equal 1, error.exit_code
286
+
287
+ url_with_port = "#{webauthn_verification_url}?port=#{port}"
288
+ 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
289
+ assert_match "ERROR: Security device verification failed: Something went wrong", @sign_in_ui.error
290
+ refute_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output
291
+ refute_match "Signed in with API key:", @sign_in_ui.output
292
+ end
293
+
294
+ def util_sign_in(response, host = nil, args = [], extra_input = "", webauthn_url = nil)
295
+ email = "you@example.com"
296
+ password = "secret"
297
+ profile_response = HTTPResponseFactory.create(body: "mfa: disabled\n", code: 200, msg: "OK")
298
+ webauthn_response =
299
+ if webauthn_url
300
+ HTTPResponseFactory.create(body: webauthn_url, code: 200, msg: "OK")
301
+ else
302
+ HTTPResponseFactory.create(body: "You don't have any security devices", code: 422, msg: "Unprocessable Entity")
303
+ end
237
304
 
238
305
  if host
239
306
  ENV["RUBYGEMS_HOST"] = host
@@ -244,6 +311,7 @@ class TestGemGemcutterUtilities < Gem::TestCase
244
311
  @fetcher = Gem::FakeFetcher.new
245
312
  @fetcher.data["#{host}/api/v1/api_key"] = response
246
313
  @fetcher.data["#{host}/api/v1/profile/me.yaml"] = profile_response
314
+ @fetcher.data["#{host}/api/v1/webauthn_verification"] = webauthn_response
247
315
  Gem::RemoteFetcher.fetcher = @fetcher
248
316
 
249
317
  @sign_in_ui = Gem::MockGemUi.new("#{email}\n#{password}\n\n\n\n\n\n\n\n\n" + extra_input)
@@ -766,7 +766,7 @@ gem 'other', version
766
766
  def test_generate_plugins
767
767
  installer = util_setup_installer do |spec|
768
768
  write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io|
769
- io.write "puts __FILE__"
769
+ io.write "# do nothing"
770
770
  end
771
771
 
772
772
  spec.files += %w[lib/rubygems_plugin.rb]
@@ -853,11 +853,59 @@ gem 'other', version
853
853
  refute_includes File.read(build_root_path), build_root
854
854
  end
855
855
 
856
+ class << self
857
+ attr_accessor :plugin_loaded
858
+ attr_accessor :post_install_is_called
859
+ end
860
+
861
+ def test_use_plugin_immediately
862
+ self.class.plugin_loaded = false
863
+ self.class.post_install_is_called = false
864
+ spec_version = nil
865
+ plugin_path = nil
866
+ installer = util_setup_installer do |spec|
867
+ spec_version = spec.version
868
+ plugin_path = File.join("lib", "rubygems_plugin.rb")
869
+ write_file File.join(@tempdir, plugin_path) do |io|
870
+ io.write <<-PLUGIN
871
+ #{self.class}.plugin_loaded = true
872
+ Gem.post_install do
873
+ #{self.class}.post_install_is_called = true
874
+ end
875
+ PLUGIN
876
+ end
877
+ spec.files += [plugin_path]
878
+ plugin_path = File.join(spec.gem_dir, plugin_path)
879
+ end
880
+ build_rake_in do
881
+ installer.install
882
+ end
883
+ assert self.class.plugin_loaded, "plugin is not loaded"
884
+ assert self.class.post_install_is_called,
885
+ "post install hook registered by plugin is not called"
886
+
887
+ self.class.plugin_loaded = false
888
+ $LOADED_FEATURES.delete(plugin_path)
889
+ installer_new = util_setup_installer do |spec_new|
890
+ spec_new.version = spec_version.version.succ
891
+ plugin_path = File.join("lib", "rubygems_plugin.rb")
892
+ write_file File.join(@tempdir, plugin_path) do |io|
893
+ io.write "#{self.class}.plugin_loaded = true"
894
+ end
895
+ spec_new.files += [plugin_path]
896
+ end
897
+ build_rake_in do
898
+ installer_new.install
899
+ end
900
+ assert !self.class.plugin_loaded,
901
+ "plugin is loaded even when old version is already loaded"
902
+ end
903
+
856
904
  def test_keeps_plugins_up_to_date
857
905
  # NOTE: version a-2 is already installed by setup hooks
858
906
 
859
907
  write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io|
860
- io.write "puts __FILE__"
908
+ io.write "# do nothing"
861
909
  end
862
910
 
863
911
  build_rake_in do
@@ -172,7 +172,7 @@ class TestGemUninstaller < Gem::InstallerTestCase
172
172
 
173
173
  def test_remove_plugins
174
174
  write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io|
175
- io.write "puts __FILE__"
175
+ io.write "# do nothing"
176
176
  end
177
177
 
178
178
  @spec.files += %w[lib/rubygems_plugin.rb]
@@ -189,7 +189,7 @@ class TestGemUninstaller < Gem::InstallerTestCase
189
189
 
190
190
  def test_remove_plugins_with_install_dir
191
191
  write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io|
192
- io.write "puts __FILE__"
192
+ io.write "# do nothing"
193
193
  end
194
194
 
195
195
  @spec.files += %w[lib/rubygems_plugin.rb]
@@ -207,7 +207,7 @@ class TestGemUninstaller < Gem::InstallerTestCase
207
207
 
208
208
  def test_regenerate_plugins_for
209
209
  write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io|
210
- io.write "puts __FILE__"
210
+ io.write "# do nothing"
211
211
  end
212
212
 
213
213
  @spec.files += %w[lib/rubygems_plugin.rb]
@@ -634,7 +634,7 @@ create_makefile '#{@spec.name}'
634
634
 
635
635
  def test_uninstall_keeps_plugins_up_to_date
636
636
  write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io|
637
- io.write "puts __FILE__"
637
+ io.write "# do nothing"
638
638
  end
639
639
 
640
640
  plugin_path = File.join Gem.plugindir, "a_plugin.rb"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "helper"
3
3
 
4
- class TestKernel < Gem::TestCase
4
+ class TestGemKernel < Gem::TestCase
5
5
  def setup
6
6
  super
7
7
 
@@ -3,13 +3,36 @@
3
3
  require_relative "helper"
4
4
  require "open3"
5
5
 
6
- class TestProjectSanity < Gem::TestCase
6
+ class TestGemProjectSanity < Gem::TestCase
7
+ def setup
8
+ end
9
+
10
+ def teardown
11
+ end
12
+
7
13
  def test_manifest_is_up_to_date
8
- pend unless File.exist?(File.expand_path("../../Rakefile", __dir__))
14
+ pend unless File.exist?("#{root}/Rakefile")
9
15
 
10
16
  _, status = Open3.capture2e("rake check_manifest")
11
17
 
12
- assert status.success?, "Expected Manifest.txt to be up to date, but it's not. Run `rake update_manifest` to sync it."
18
+ unless status.success?
19
+ original_contents = File.read("#{root}/Manifest.txt")
20
+
21
+ # Update the manifest to see if it fixes the problem
22
+ Open3.capture2e("rake update_manifest")
23
+
24
+ out, status = Open3.capture2e("rake check_manifest")
25
+
26
+ # If `rake update_manifest` fixed the problem, that was the original
27
+ # issue, otherwise it was an unknown error, so print the error output
28
+ if status.success?
29
+ File.write("#{root}/Manifest.txt", original_contents)
30
+
31
+ raise "Expected Manifest.txt to be up to date, but it's not. Run `rake update_manifest` to sync it."
32
+ else
33
+ raise "There was an error running `rake check_manifest`: #{out}"
34
+ end
35
+ end
13
36
  end
14
37
 
15
38
  def test_require_rubygems_package
@@ -17,4 +40,10 @@ class TestProjectSanity < Gem::TestCase
17
40
 
18
41
  assert status.success?, err
19
42
  end
43
+
44
+ private
45
+
46
+ def root
47
+ File.expand_path("../..", __dir__)
48
+ end
20
49
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "helper"
3
3
 
4
- class TestRemoteFetchError < Gem::TestCase
4
+ class TestGemRemoteFetchError < Gem::TestCase
5
5
  def test_password_redacted
6
6
  error = Gem::RemoteFetcher::FetchError.new("There was an error fetching", "https://user:secret@gemsource.org")
7
7
  refute_match %r{secret}, error.to_s
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helper"
4
+ require "rubygems/webauthn_listener"
5
+
6
+ class WebauthnListenerTest < Gem::TestCase
7
+ def setup
8
+ super
9
+ @server = TCPServer.new 0
10
+ @port = @server.addr[1].to_s
11
+ end
12
+
13
+ def test_wait_for_otp_code_get_follows_options
14
+ wait_for_otp_code
15
+ assert Gem::MockBrowser.options(URI("http://localhost:#{@port}?code=xyz")).is_a? Net::HTTPNoContent
16
+ assert Gem::MockBrowser.get(URI("http://localhost:#{@port}?code=xyz")).is_a? Net::HTTPOK
17
+ end
18
+
19
+ def test_wait_for_otp_code_options_request
20
+ wait_for_otp_code
21
+ response = Gem::MockBrowser.options URI("http://localhost:#{@port}?code=xyz")
22
+
23
+ assert response.is_a? Net::HTTPNoContent
24
+ assert_equal Gem.host, response["access-control-allow-origin"]
25
+ assert_equal "POST", response["access-control-allow-methods"]
26
+ assert_equal "Content-Type, Authorization, x-csrf-token", response["access-control-allow-headers"]
27
+ assert_equal "close", response["Connection"]
28
+ end
29
+
30
+ def test_wait_for_otp_code_get_request
31
+ wait_for_otp_code
32
+ response = Gem::MockBrowser.get URI("http://localhost:#{@port}?code=xyz")
33
+
34
+ assert response.is_a? Net::HTTPOK
35
+ assert_equal "text/plain", response["Content-Type"]
36
+ assert_equal "7", response["Content-Length"]
37
+ assert_equal Gem.host, response["access-control-allow-origin"]
38
+ assert_equal "POST", response["access-control-allow-methods"]
39
+ assert_equal "Content-Type, Authorization, x-csrf-token", response["access-control-allow-headers"]
40
+ assert_equal "close", response["Connection"]
41
+ assert_equal "success", response.body
42
+
43
+ @thread.join
44
+ assert_equal "xyz", @thread[:otp]
45
+ end
46
+
47
+ def test_wait_for_otp_code_invalid_post_req_method
48
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Invalid HTTP method POST received.")
49
+ response = Gem::MockBrowser.post URI("http://localhost:#{@port}?code=xyz")
50
+
51
+ assert response
52
+ assert response.is_a? Net::HTTPMethodNotAllowed
53
+ assert_equal "GET, OPTIONS", response["allow"]
54
+ assert_equal "close", response["Connection"]
55
+
56
+ @thread.join
57
+ assert_nil @thread[:otp]
58
+ end
59
+
60
+ def test_wait_for_otp_code_incorrect_path
61
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Page at /path not found.")
62
+ response = Gem::MockBrowser.post URI("http://localhost:#{@port}/path?code=xyz")
63
+
64
+ assert response.is_a? Net::HTTPNotFound
65
+ assert_equal "close", response["Connection"]
66
+
67
+ @thread.join
68
+ assert_nil @thread[:otp]
69
+ end
70
+
71
+ def test_wait_for_otp_code_no_params_response
72
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Did not receive OTP from https://rubygems.org.")
73
+ response = Gem::MockBrowser.get URI("http://localhost:#{@port}")
74
+
75
+ assert response.is_a? Net::HTTPBadRequest
76
+ assert_equal "text/plain", response["Content-Type"]
77
+ assert_equal "22", response["Content-Length"]
78
+ assert_equal "close", response["Connection"]
79
+ assert_equal "missing code parameter", response.body
80
+
81
+ @thread.join
82
+ assert_nil @thread[:otp]
83
+ end
84
+
85
+ def test_wait_for_otp_code_incorrect_params
86
+ wait_for_otp_code_expect_error_with_message("Security device verification failed: Did not receive OTP from https://rubygems.org.")
87
+ response = Gem::MockBrowser.get URI("http://localhost:#{@port}?param=xyz")
88
+
89
+ assert response.is_a? Net::HTTPBadRequest
90
+ assert_equal "text/plain", response["Content-Type"]
91
+ assert_equal "22", response["Content-Length"]
92
+ assert_equal "close", response["Connection"]
93
+ assert_equal "missing code parameter", response.body
94
+
95
+ @thread.join
96
+ assert_nil @thread[:otp]
97
+ end
98
+
99
+ private
100
+
101
+ def wait_for_otp_code
102
+ @thread = Thread.new do
103
+ Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(Gem.host, @server)
104
+ end
105
+ @thread.abort_on_exception = true
106
+ @thread.report_on_exception = false
107
+ end
108
+
109
+ def wait_for_otp_code_expect_error_with_message(message)
110
+ @thread = Thread.new do
111
+ error = assert_raise Gem::WebauthnVerificationError do
112
+ Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(Gem.host, @server)
113
+ end
114
+
115
+ assert_equal message, error.message
116
+ end
117
+ @thread.abort_on_exception = true
118
+ @thread.report_on_exception = false
119
+ end
120
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helper"
4
+ require "rubygems/webauthn_listener/response"
5
+
6
+ class WebauthnListenerResponseTest < Gem::TestCase
7
+ def setup
8
+ super
9
+ @host = "rubygems.example"
10
+ end
11
+
12
+ def test_ok_response_to_s
13
+ to_s = Gem::WebauthnListener::OkResponse.new(@host).to_s
14
+
15
+ expected_to_s = <<~RESPONSE
16
+ HTTP/1.1 200 OK\r
17
+ connection: close\r
18
+ access-control-allow-origin: rubygems.example\r
19
+ access-control-allow-methods: POST\r
20
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
21
+ content-type: text/plain\r
22
+ content-length: 7\r
23
+ \r
24
+ success
25
+ RESPONSE
26
+
27
+ assert_equal expected_to_s, to_s
28
+ end
29
+
30
+ def test_no_to_s_response_to_s
31
+ to_s = Gem::WebauthnListener::NoContentResponse.new(@host).to_s
32
+
33
+ expected_to_s = <<~RESPONSE
34
+ HTTP/1.1 204 No Content\r
35
+ connection: close\r
36
+ access-control-allow-origin: rubygems.example\r
37
+ access-control-allow-methods: POST\r
38
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
39
+ \r
40
+ RESPONSE
41
+
42
+ assert_equal expected_to_s, to_s
43
+ end
44
+
45
+ def test_method_not_allowed_response_to_s
46
+ to_s = Gem::WebauthnListener::MethodNotAllowedResponse.new(@host).to_s
47
+
48
+ expected_to_s = <<~RESPONSE
49
+ HTTP/1.1 405 Method Not Allowed\r
50
+ connection: close\r
51
+ access-control-allow-origin: rubygems.example\r
52
+ access-control-allow-methods: POST\r
53
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
54
+ allow: GET, OPTIONS\r
55
+ \r
56
+ RESPONSE
57
+
58
+ assert_equal expected_to_s, to_s
59
+ end
60
+
61
+ def test_method_not_found_response_to_s
62
+ to_s = Gem::WebauthnListener::NotFoundResponse.new(@host).to_s
63
+
64
+ expected_to_s = <<~RESPONSE
65
+ HTTP/1.1 404 Not Found\r
66
+ connection: close\r
67
+ access-control-allow-origin: rubygems.example\r
68
+ access-control-allow-methods: POST\r
69
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
70
+ \r
71
+ RESPONSE
72
+
73
+ assert_equal expected_to_s, to_s
74
+ end
75
+
76
+ def test_bad_request_response_to_s
77
+ to_s = Gem::WebauthnListener::BadRequestResponse.new(@host).to_s
78
+
79
+ expected_to_s = <<~RESPONSE
80
+ HTTP/1.1 400 Bad Request\r
81
+ connection: close\r
82
+ access-control-allow-origin: rubygems.example\r
83
+ access-control-allow-methods: POST\r
84
+ access-control-allow-headers: Content-Type, Authorization, x-csrf-token\r
85
+ content-type: text/plain\r
86
+ content-length: 22\r
87
+ \r
88
+ missing code parameter
89
+ RESPONSE
90
+
91
+ assert_equal expected_to_s, to_s
92
+ end
93
+ end