diff --git a/app/views/layouts/_session.html.erb b/app/views/layouts/_session.html.erb
new file mode 100644
index 00000000000..677752bc08b
--- /dev/null
+++ b/app/views/layouts/_session.html.erb
@@ -0,0 +1,53 @@
+
+ <% if signed_in? %>
+ <%# This class is used in the tests :( I need it to be the same class until the old design is gone. %>
+ <%= link_to(dashboard_path, data: { action: "dialog#open", dialog_target: "button" }, class: "header__popup-link") do %>
+ <%= avatar 64, "user_gravatar", theme: :dark, class: "h-9 w-9 rounded" %>
+ <% end %>
+ <% else %>
+
+
+ <%= render AlertComponent.new(style: :neutral, closeable: true) do %>
+ Design Under Construction.
+ Learn more
+ <% end %>
+
+ <% flash.each do |name, msg| %>
+ <%= render AlertComponent.new(style: name, closeable: true) do %>
+ <%= flash_message(name, msg) %>
+ <% end %>
+ <% end %>
+
+ <%= yield %>
+
+
+ <% end %>
+
+
+
+
+
+
diff --git a/app/views/layouts/onboarding.html.erb b/app/views/layouts/onboarding.html.erb
new file mode 100644
index 00000000000..42f82fef964
--- /dev/null
+++ b/app/views/layouts/onboarding.html.erb
@@ -0,0 +1,31 @@
+<% content_for :main do %>
+
+
+ <% flash.each do |name, msg| %>
+ <%= render AlertComponent.new(style: name, closeable: true) do %>
+ <%= flash_message(name, msg) %>
+ <% end %>
+ <% end %>
+
+
+
+ <%# At desktop width, these outer 2 divs make the full content background with 2 columns of content %>
+
+
Create an Organization
+
+
+ <%# mobile: main makes a full width section with a max width inner container %>
+
+
+ <%= yield %>
+
+
+
+ <%# At mobile width, the aside is hidden %>
+
+
+
+<% end %>
+<%= render template: "layouts/hammy" %>
diff --git a/app/views/layouts/subject.html.erb b/app/views/layouts/subject.html.erb
new file mode 100644
index 00000000000..64f01248ec2
--- /dev/null
+++ b/app/views/layouts/subject.html.erb
@@ -0,0 +1,42 @@
+<%#
+ This is a subject focused layout, like a profile or organization where
+ the user or organization stays on the left side while the main content changes.
+ On mobile, the subject connects to the header in color and spacing, making
+ it clear that the subject content is part of the page context.
+%>
+<% content_for :main do %>
+
+
+
+ <%= render AlertComponent.new(style: :neutral, closeable: true) do %>
+ Design Under Construction.
+ Learn more
+ <% end %>
+
+ <% flash.each do |name, msg| %>
+ <%= render AlertComponent.new(style: name, closeable: true) do %>
+ <%= flash_message(name, msg) %>
+ <% end %>
+ <% end %>
+
+
+
+ <%# At desktop width, these outer 2 divs make the full content background with 2 columns of content %>
+
+
+ <%# At mobile width, the inner aside and main make two stacked full width sections %>
+
+
+
+
+ <%# This tag contains the recovery codes and should not be a part of the form %>
+ <%= text_area_tag "source", "#{@mfa_recovery_codes.join("\n")}\n", class: "recovery-code-list", rows: @mfa_recovery_codes.size + 1, cols: @mfa_recovery_codes.first.length + 1, readonly: true, data: { clipboard_target: "source" } %>
-
+<% end %>
diff --git a/app/views/organizations/onboarding/users/edit.html.erb b/app/views/organizations/onboarding/users/edit.html.erb
new file mode 100644
index 00000000000..0f1be2b5b93
--- /dev/null
+++ b/app/views/organizations/onboarding/users/edit.html.erb
@@ -0,0 +1,96 @@
+<% content_for :aside do %>
+ <%= render "organizations/onboarding/summary", current_step: 3 %>
+<% end %>
+
+<%= render "organizations/onboarding/progress", current_step: 3 %>
+
+
Manage Members
+
+
+ Below we have listed all the owners of the gems you selected.
+ If you do nothing, these existing owners will remain owners of their gems.
+ You can invite them to join the organization now or you can add members later.
+
- Or, to upgrade to the latest RubyGems:
+ RubyGems is a package management framework for Ruby.
+
+ Upgrade to the latest RubyGems at the command line:
$ gem update --system
- You might be running into some bug that prevents you from upgrading rubygems the standard way. In that case, you can try upgrading manually:
+ If you run into problems that prevent you from upgrading rubygems the standard way, you can try upgrading manually:
-
<%=link_to "Download from above", "#formats" %>
-
Unpack into a directory and cd there
-
Install with: ruby setup.rb
+
Download the latest version
+
Unpack and cd into the unpacked directory
+
Install with:
+
ruby setup.rb
+
+
+ For more details and other options, run: ruby setup.rb --help
+
Found a security issue with RubyGems or RubyGems.org?
Please follow these steps to report it.
@@ -131,4 +131,4 @@
security@rubygems.org or
open an issue on GitHub. Thanks!
RubyGems.org is made possible by support from the Ruby community. These companies provide the servers and developers that create the public infrastructure of the Ruby programming language.
Ruby Central, Inc. is a nonprofit 501(c)3 organization dedicated to the support and advocacy of the worldwide Ruby community. They organize the annual RubyConf and RailsConf as opportunities for Rubyists to collaborate and network. As part of those conferences, Ruby Central also operates the Opportunity Scholarship program to consistently bring new people into the tech community and to provide them with a comfortable, welcoming environment to explore our community and its events.
-
+
+
They also fund and operate the Ruby Central Community Grant, underwriting events or open source projects that benefit the Ruby community, and they fund the ongoing server costs associated with running RubyGems.org.
- You can support Ruby Central by attending or [sponsoring](sponsors@rubycentral.org) a conference, or by [joining as a supporting member](https://rubycentral.org/#/portal/signup).
+ You can support Ruby Central by attending or contacting Ruby Central about sponsoring a conference, or by joining as a supporting member.
@@ -21,16 +22,12 @@
Fastly provides the RubyGems.org content delivery network, allowing Ruby developers all over the world to download gems from nearby servers around the world.
-
-
A password manager, digital vault, form filler and secure digital wallet. 1Password remembers all your passwords for you to help keep account information safe.
Avo is a custom Content Management System for Ruby on Rails that saves developers and teams months of development time. It's built on modern technologies and provides all the necessary hooks to ensure developers ship the best experiences to their customers.
Avo enables the RubyGems.org team to quickly build internal tools with limited resources.
+ <% end %>
+ <% end %>
+<% end %>
diff --git a/bin/brakeman b/bin/brakeman
index b4fe8de2663..ace1c9ba08a 100755
--- a/bin/brakeman
+++ b/bin/brakeman
@@ -1,27 +1,7 @@
#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-#
-# This file was generated by Bundler.
-#
-# The application 'brakeman' is installed as part of a gem, and
-# this file is here to facilitate running it.
-#
-
-ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
-
-bundle_binstub = File.expand_path("bundle", __dir__)
-
-if File.file?(bundle_binstub)
- if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
- load(bundle_binstub)
- else
- abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
-Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
- end
-end
-
require "rubygems"
require "bundler/setup"
+ARGV.unshift("--ensure-latest")
+
load Gem.bin_path("brakeman", "brakeman")
diff --git a/bin/rubocop b/bin/rubocop
index 369a05bedb5..40330c0ff1c 100755
--- a/bin/rubocop
+++ b/bin/rubocop
@@ -1,27 +1,8 @@
#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-#
-# This file was generated by Bundler.
-#
-# The application 'rubocop' is installed as part of a gem, and
-# this file is here to facilitate running it.
-#
-
-ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
-
-bundle_binstub = File.expand_path("bundle", __dir__)
-
-if File.file?(bundle_binstub)
- if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
- load(bundle_binstub)
- else
- abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
-Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
- end
-end
-
require "rubygems"
require "bundler/setup"
+# explicit rubocop config increases performance slightly while avoiding config confusion.
+ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
+
load Gem.bin_path("rubocop", "rubocop")
diff --git a/bin/setup b/bin/setup
index e2675753e1d..51afbda953b 100755
--- a/bin/setup
+++ b/bin/setup
@@ -1,8 +1,8 @@
#!/usr/bin/env ruby
require "fileutils"
-# path to your application root.
APP_ROOT = File.expand_path("..", __dir__)
+APP_NAME = "gemcutter"
def system!(*args)
system(*args, exception: true)
@@ -36,4 +36,8 @@ FileUtils.chdir APP_ROOT do
puts "\n== Restarting application server =="
system! "bin/rails restart"
+
+ # puts "\n== Configuring puma-dev =="
+ # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}"
+ # system "curl -Is https://#{APP_NAME}.test/up | head -n 1"
end
diff --git a/config/application.rb b/config/application.rb
index 7de954883d6..1328ecd5bd4 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -12,13 +12,18 @@
# require "action_text/engine"
require "action_view/railtie"
# require "action_cable/engine"
-require "sprockets/railtie"
require "rails/test_unit/railtie"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
+# allow dotenv to specify RAILS_GROUPS
+if defined?(Dotenv::Rails)
+ Dotenv::Rails.load
+ Bundler.require(*Rails.groups)
+end
+
module Gemcutter
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
@@ -44,7 +49,7 @@ class Application < Rails::Application
config.i18n.available_locales = [:en, :nl, "zh-CN", "zh-TW", "pt-BR", :fr, :es, :de, :ja]
config.i18n.fallbacks = [:en]
- config.middleware.insert 0, Rack::UTF8Sanitizer
+ config.middleware.insert 0, Rack::Sanitizer
config.middleware.use Rack::Attack
config.middleware.use Rack::Deflater
@@ -69,6 +74,10 @@ class Application < Rails::Application
config.active_support.cache_format_version = 7.1
config.action_dispatch.rescue_responses["Rack::Multipart::EmptyContentError"] = :bad_request
+
+ config.action_dispatch.default_headers.merge!(
+ "Cross-Origin-Opener-Policy" => "same-origin"
+ )
end
def self.config
@@ -101,9 +110,9 @@ def self.config
GEM_REQUEST_LIMIT = 400
VERSIONS_PER_PAGE = 100
SEPARATE_ADMIN_HOST = config["separate_admin_host"]
- ENABLE_DEVELOPMENT_ADMIN_LOG_IN = Rails.env.local?
+ ENABLE_DEVELOPMENT_LOG_IN = Rails.env.local?
MAIL_SENDER = "RubyGems.org ".freeze
PAGES = %w[
- about data download faq migrate security sponsors
+ about data download security sponsors
].freeze
end
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
new file mode 100644
index 00000000000..2a902bfc302
--- /dev/null
+++ b/config/brakeman.ignore
@@ -0,0 +1,52 @@
+{
+ "ignored_warnings": [
+ {
+ "warning_type": "Mass Assignment",
+ "warning_code": 105,
+ "fingerprint": "85e397bacae5462238e8ce59c0fcd1045a414e603588ae313c5156c09a623fed",
+ "check_name": "PermitAttributes",
+ "message": "Potentially dangerous key allowed for mass assignment",
+ "file": "app/controllers/owners_controller.rb",
+ "line": 101,
+ "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
+ "code": "params.permit(:role)",
+ "render_path": null,
+ "location": {
+ "type": "method",
+ "class": "OwnersController",
+ "method": "update_params"
+ },
+ "user_input": ":role",
+ "confidence": "Medium",
+ "cwe_id": [
+ 915
+ ],
+ "note": ""
+ },
+ {
+ "warning_type": "Mass Assignment",
+ "warning_code": 105,
+ "fingerprint": "b57c75e671196846b60667930fc3c1a4d02122b7ec3ae5ae0cf8cecc6c1d0b63",
+ "check_name": "PermitAttributes",
+ "message": "Potentially dangerous key allowed for mass assignment",
+ "file": "app/controllers/api/v1/owners_controller.rb",
+ "line": 84,
+ "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
+ "code": "params.permit(:role)",
+ "render_path": null,
+ "location": {
+ "type": "method",
+ "class": "Api::V1::OwnersController",
+ "method": "ownership_params"
+ },
+ "user_input": ":role",
+ "confidence": "Medium",
+ "cwe_id": [
+ 915
+ ],
+ "note": ""
+ }
+ ],
+ "updated": "2024-09-21 16:16:50 -0700",
+ "brakeman_version": "6.2.1"
+}
diff --git a/config/brakeman.yml b/config/brakeman.yml
index d9492b31e1d..26a7bbdc235 100644
--- a/config/brakeman.yml
+++ b/config/brakeman.yml
@@ -7,5 +7,5 @@
:output_files:
- reports/brakeman/brakeman.json
- reports/brakeman/brakeman.html
-:rails5: true
+:rails7: true
:github_repo: rubygems/rubygems.org
diff --git a/config/deploy/production/secrets.ejson b/config/deploy/production/secrets.ejson
index 23e4cd93c0b..8826939da3e 100644
--- a/config/deploy/production/secrets.ejson
+++ b/config/deploy/production/secrets.ejson
@@ -22,7 +22,7 @@
"client_id": "EJ[1:YVJ0VwGrcSYWmu7lQOJPUdkXYI5VBgGXLla1+Fo1eA8=:jnplUfdfAMq/te3PyIN3SejasXCukW/g:0LsAhlffVl+6W4k5sRLWpew4D8ohT5ym3Ou+O1CsZgd/fvu16YWRpgvX345ewip0]",
"github_key": "EJ[1:dz+xczzzSxiEdCK5nfz6u2RkBOEeeOTlMD2eWIHTAg8=:3ZtgHbbvis6EJ5UymIZBWPwc+Zvf722/:gcsPiEVuVZIitCZ3t2216Jn5re2vBTyN7FElhowCKpQUglwU]",
"github_secret": "EJ[1:dz+xczzzSxiEdCK5nfz6u2RkBOEeeOTlMD2eWIHTAg8=:3bGij9eYVzE6mJ4XmIT/QBBRmnCi9nMs:8jcKV1Hl8TYTMQaQ8SV+agQ6c9UA9lb1DHKy2J/W//gdlJ2r/r4qlKeI/zoWMX2e461aB1Dx7XQ=]",
- "avo_license_key": "EJ[1:6oWEkRT4PhQJLxAVEt3F66gOvAd2bB/J7cF+DpFv4Gk=:atX3EOd7BdoSBqJFPaqASO6/u6R2MLlR:G4TyGlkc1EZWpUJ0mQTS9Bw1O5V9Hww3DZtV+MQuDg9pzw8n7frV+GQek4wDqI29QLk0Kg==]",
+ "avo_license_key": "EJ[1:vHG/UgXm5H2xr/ExsZdTi7Lep9NzpQkKXFUlNb9FOVU=:dRDXtqaYSBhiY5cuHJutRa8R2ZUUE8h3:+xyDjGvAJmtwxHdMcGQ21GDRnVagGG009JSw]",
"datadog_csp_api_key": "EJ[1:MJLOVnheQZD8mqT8Q5xlN8u6Qd9eH4sbO1kcsg/TTR4=:0eAj1g7msiS86qexeMNsWU+1vy3pXSKK:4YT1qDTiBirwkNsbNdhN90lPlF51qiMNChue7uMpEYuAiKTI3TnwOWJd7sk0PmARVsIn]",
"hook_relay_account_id": "EJ[1:Px+UVqbFzeNGqkAlgQ9Vz00tXWKza1XmLLHD/iQT5wM=:JsT1PTsSpMaxE0FV7ODyJ5E1FQAzv9v/:tMUgIFIUJH+WhT+kHS5pne5Uw9qvdLqLd2aAfJ9EehzIYpH6Bl6EQg==]",
"hook_relay_hook_id": "EJ[1:Px+UVqbFzeNGqkAlgQ9Vz00tXWKza1XmLLHD/iQT5wM=:Vt1tMd8/9ZhSHL80yrWIWUy2wMOh8Iqt:MUwOcVrjWl7i8RRNg68Sb/Fa+RYsLL54yc+adRK84i5jWWa9fmSgRg==]",
diff --git a/config/deploy/staging/secrets.ejson b/config/deploy/staging/secrets.ejson
index ed0a20bd927..4efe779d58d 100644
--- a/config/deploy/staging/secrets.ejson
+++ b/config/deploy/staging/secrets.ejson
@@ -22,7 +22,7 @@
"client_id": "EJ[1:G3QtTGP/Pf0HRv8+b6zf3xI3TmNiOPmidA24ZSgQgjU=:AZtoluRkpeCK96i3FxPgamAkAlwRuTKq:NrpTfXVR1rAA0UyJzYVmCvJvQ5lr/VLyAoHDz0IY0cmC0aJLO8HWKsAYWq3T4w2D]",
"github_key": "EJ[1:w54WPX1mXZxgJoUu4zPMVMd5zCWl0rbbfuyqcvJOP1o=:XX+D9g9OUcALhAK1aRB5uK4hmUOhmq+a:kEEf22rN2vnyXE2I5me5Fc+2rCu070VA8IpcsbeJ8Xh/3261]",
"github_secret": "EJ[1:w54WPX1mXZxgJoUu4zPMVMd5zCWl0rbbfuyqcvJOP1o=:0aEWndKlt5UpeGXSNfX8jWtc5UqnKk1g:MmBDQQEzb9tUaxVIL7bUUBbg2/UvhyvbnHcvlfqWztdSVHAFuvGe+tVtHjEdCdMzrJzG5d6UVBo=]",
- "avo_license_key": "EJ[1:TD6sa+BG2xSLyZddF8CUdq8egeXQfz1oeMaSD/DV/z8=:iXP5X+kGqtPPjluF93iyxTRiwA4sq5ch:My9uCrd+KqROpd1l3x8HgQYvi/gRs+FBQ41IdmqouwbVJBRipuHotXYGHBcqOtvFwpk1/w==]",
+ "avo_license_key": "EJ[1:xcimfAiJn0jrCDMpC+0kpX0WGUO3QxTcjfZSV8/zMBg=:Kkeih5DzMjlewV8GjD+iBOhQHinGXswy:hDpP9A4MhyRLAAoAmekw9soIdVHWso4TClPK]",
"datadog_csp_api_key": "EJ[1:M1xidXJHz2i/vKthLOnwYbEnkFjYneq+d6Ryj6TXdFQ=:fxMZfI3MptOYF/hXcrnrWK/SDTU1SzZC:Gn807pB1wSTTNIyTJ5cpGCvN5+vplIZUxi/a8/s+UFDJE52zeM5KyhvmK6f0J8mDa2vh]",
"hook_relay_account_id": "EJ[1:JT93bSSDxXR0tIyvuhE4hVVEav3DdVDYtCuhLTlwwUw=:OUkdjS28VFFsXWnK0c2zBubgen83JLIZ:r9s3A3e3UuNrYRelD3HzIKsNvEGlGhvKmZayZt7wZGHrxB+yTb8gQQ==]",
"hook_relay_hook_id": "EJ[1:JT93bSSDxXR0tIyvuhE4hVVEav3DdVDYtCuhLTlwwUw=:n1YqUGXM5FdcwoAJjIXXgHxWqiF/voeL:sj7Gbn405v2hw4CKkU6N7Fhgb3J/5RnrAZpp+Xcn61ZTNJq0qFXBiA==]",
diff --git a/config/deploy/web.yaml.erb b/config/deploy/web.yaml.erb
index ba6c2c32020..c9e4f42feef 100644
--- a/config/deploy/web.yaml.erb
+++ b/config/deploy/web.yaml.erb
@@ -61,6 +61,8 @@ spec:
value: "<%= environment %>"
- name: ENV
value: "<%= environment %>"
+ - name: RAILS_GROUPS
+ value: "avo"
- name: WEB_CONCURRENCY
value: "<%= environment == 'production' ? 8 : 2 %>"
- name: RAILS_MAX_THREADS
@@ -219,6 +221,10 @@ spec:
preStop:
exec:
command: ["sleep", "25"]
+ volumeMounts:
+ - name: sigstore-signing-token
+ mountPath: /var/run/secrets/sigstore
+ readOnly: true
- name: nginx
image: nginx:1.25.2
imagePullPolicy: IfNotPresent
@@ -298,3 +304,10 @@ spec:
secret:
secretName: nginx-basic-auth
<% end %>
+ - name: sigstore-signing-token
+ projected:
+ sources:
+ - serviceAccountToken:
+ path: sigstore-signing-token
+ expirationSeconds: 600
+ audience: sigstore
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 63cbd9de3ce..2384e1ee68d 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,4 +1,3 @@
-require_relative "../../lib/gemcutter/middleware/hostess"
require "active_support/core_ext/integer/time"
Rails.application.configure do
@@ -15,7 +14,7 @@
# Show full error reports.
config.consider_all_requests_local = true
- # Enable server timing
+ # Enable server timing.
config.server_timing = true
# Enable/disable caching. By default caching is disabled.
@@ -26,9 +25,7 @@
config.cache_store = :mem_cache_store,
{ compress: true, compression_min_size: 524_288 }
- config.public_file_server.headers = {
- "Cache-Control" => "public, max-age=#{2.days.to_i}"
- }
+ config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" }
else
config.action_controller.perform_caching = false
@@ -40,6 +37,8 @@
config.action_mailer.raise_delivery_errors = true
+ # Disable caching for Action Mailer templates even if Action Controller
+ # caching is enabled.
config.action_mailer.perform_caching = false
config.action_mailer.default_url_options = { host: Gemcutter::HOST,
@@ -75,13 +74,18 @@
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
+ require_relative "../../lib/gemcutter/middleware/hostess"
config.middleware.use Gemcutter::Middleware::Hostess
+
# Annotate rendered view with file names.
- # config.action_view.annotate_rendered_view_with_filenames = true
+ config.action_view.annotate_rendered_view_with_filenames = true
- # Raise error when a before_action's only/except options reference missing actions
+ # Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true
+ # Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
+ config.generators.apply_rubocop_autocorrect_after_generate!
+
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
@@ -119,8 +123,6 @@
'Cache-Control' => 'max-age=315360000, public',
'Expires' => 'Thu, 31 Dec 2037 23:55:55 GMT'
}
- config.assets.js_compressor = :terser
- config.assets.css_compressor = :sass
config.assets.compile = false
config.assets.digest = true
config.assets.debug = false
@@ -129,4 +131,6 @@
config.active_record.verbose_query_logs = false
config.action_view.cache_template_loading = true
end
+
+ config.hosts << "rubygems.test"
end
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 8d5862924f6..8d95758ee47 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -1,5 +1,3 @@
-require Rails.root.join("config", "secret") if Rails.root.join("config", "secret.rb").file?
-require_relative "../../lib/gemcutter/middleware/redirector"
require "active_support/core_ext/integer/time"
Rails.application.configure do
@@ -22,21 +20,14 @@
# key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true
- # Disable serving static files from the `/public` folder by default since
- # Apache or NGINX already handles this.
+ # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead.
config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
config.public_file_server.headers = {
'Cache-Control' => 'max-age=315360000, public',
'Expires' => 'Thu, 31 Dec 2037 23:55:55 GMT'
}
- # Compress JavaScript using a preprocessor
- config.assets.js_compressor = :terser
-
- # Compress CSS using a preprocessor.
- config.assets.css_compressor = :sass
-
- # Do not fallback to assets pipeline if a precompiled asset is missed.
+ # Do not fall back to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
@@ -62,7 +53,6 @@
# Include generic and useful information about system operation, but avoid logging too much
# information to avoid inadvertent exposure of personally identifiable information (PII).
$stdout.sync = true
- config.log_level = :info
config.rails_semantic_logger.format = :json
config.rails_semantic_logger.semantic = true
config.rails_semantic_logger.add_file_appender = false
@@ -71,13 +61,20 @@
# Prepend all log lines with the following tags.
# config.log_tags = [ :request_id ]
+ # "info" includes generic and useful information about system operation, but avoids logging too much
+ # information to avoid inadvertent exposure of personally identifiable information (PII). If you
+ # want to log everything, set the level to "debug".
+ config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
+
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
# Use a real queuing backend for Active Job (and separate queues per environment).
- # config.active_job.queue_adapter = :resque
+ # config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "gemcutter_production"
+ # Disable caching for Action Mailer templates even if Action Controller
+ # caching is enabled.
config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.
@@ -122,5 +119,6 @@
value_max_bytes: 2_097_152 # 2MB
}
+ require_relative "../../lib/gemcutter/middleware/redirector"
config.middleware.use Gemcutter::Middleware::Redirector
end
diff --git a/config/environments/staging.rb b/config/environments/staging.rb
index f2a52cdfe87..ae4726d99a0 100644
--- a/config/environments/staging.rb
+++ b/config/environments/staging.rb
@@ -30,10 +30,6 @@
'Expires' => 'Thu, 31 Dec 2037 23:55:55 GMT'
}
- # Compress JavaScripts and CSS.
- config.assets.js_compressor = :terser
- config.assets.css_compressor = :sass
-
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 73358c44122..a65ac8280f1 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,4 +1,3 @@
-require_relative "../../lib/gemcutter/middleware/redirector"
require "active_support/core_ext/integer/time"
# The test environment is used exclusively to run your application's
@@ -35,12 +34,17 @@
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
+ # Disable caching for Action Mailer templates even if Action Controller
+ # caching is enabled.
config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
+
+ # Unlike controllers, the mailer instance doesn't have any context about the
+ # incoming request so you'll need to provide the :host parameter yourself.
config.action_mailer.default_url_options = { host: Gemcutter::HOST,
port: "31337",
protocol: Gemcutter::PROTOCOL }
@@ -48,7 +52,7 @@
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
- require 'clearance_backdoor'
+ require_relative "../../lib/clearance_backdoor"
config.middleware.use ClearanceBackdoor
# Raise exceptions for disallowed deprecations.
@@ -65,7 +69,7 @@
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
- # Raise error when a before_action's only/except options reference missing actions
+ # Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true
BCrypt::Engine.cost = BCrypt::Engine::MIN_COST
diff --git a/config/importmap.rb b/config/importmap.rb
index c4acae20adf..6061894a513 100644
--- a/config/importmap.rb
+++ b/config/importmap.rb
@@ -1,16 +1,23 @@
# Pin npm packages by running ./bin/importmap
pin "jquery" # @3.7.1
-pin "@rails/ujs", to: "@rails--ujs.js" # @7.1.3
+pin "@rails/ujs", to: "@rails--ujs.js" # @7.1.3-4
pin "application"
pin_all_from "app/javascript/src", under: "src"
-pin "clipboard" # @2.0.11
-# stimulus.min.js is a compiled asset from stimulus-rails gem
-pin "@hotwired/stimulus", to: "stimulus.min.js"
+# If the version of turbo-rails changes, check that the CSP hash of the ProgressBar stylesheet didn't change.
+# Look for an error in the browser console indicating that a stylesheet was skipped.
+# 'turbo.min.js' is embedded in the turbo-rails gem.
+pin "@hotwired/turbo-rails", to: "turbo.min.js"
+
+pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2
# stimulus-loading.js is a compiled asset only available from stimulus-rails gem
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
+pin "@stimulus-components/clipboard", to: "@stimulus-components--clipboard.js" # @5.0.0
+pin "@stimulus-components/dialog", to: "@stimulus-components--dialog.js" # @1.0.1
+pin "@stimulus-components/reveal", to: "@stimulus-components--reveal.js" # @5.0.0
+pin "@stimulus-components/checkbox-select-all", to: "@stimulus-components--checkbox-select-all.js" # @6.0.0
# vendored and adapted from https://github.com/mdo/github-buttons/blob/master/src/js.js
pin "github-buttons"
@@ -20,3 +27,4 @@
# Avo custom JS entrypoint
pin "avo.custom", preload: false
pin "stimulus-rails-nested-form", preload: false # @4.1.0
+pin "local-time" # @3.0.2
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index 4658f8394f0..487324424ff 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -5,8 +5,3 @@
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
-
-# Precompile additional assets.
-# application.js, application.css, and all non-JS/CSS in the app/assets
-# folder are already added.
-Rails.application.config.assets.precompile += %w[*.png *.jpg *.jpeg *.gif *.svg]
diff --git a/config/initializers/avo.rb b/config/initializers/avo.rb
index 17c1acae253..a7c018b0454 100644
--- a/config/initializers/avo.rb
+++ b/config/initializers/avo.rb
@@ -1,13 +1,12 @@
# For more information regarding these settings check out our docs https://docs.avohq.io
-Avo.configure do |config|
+Avo.configure do |config| # rubocop:disable Metrics/BlockLength
## == Routing ==
config.root_path = '/admin'
# Where should the user be redirected when visting the `/avo` url
- config.home_path = "/admin/dashboards/dashy"
+ config.home_path = "/admin/dashboards/dashy" if defined?(Avo::Pro)
## == Licensing ==
- config.license = 'pro' # change this to 'pro' when you add the license key
config.license_key = ENV['AVO_LICENSE_KEY']
## == Set the context ==
@@ -18,6 +17,13 @@
## == Authentication ==
config.current_user_method = :admin_user
config.authenticate_with do
+ if !Rails.env.local? && !(Avo.license.valid? && Avo.license.advanced?)
+ raise "Avo::Pro is missing in #{Rails.env}." \
+ "\nRails.groups=#{Rails.groups.inspect}" \
+ "\nAvo.license=#{Avo.license.inspect}" \
+ "\nAvo.configuration.license=#{Avo.configuration.license.inspect}"
+ end
+
redirect_to '/' unless _current_user&.valid?
Current.user = begin
User.security_user
@@ -98,33 +104,36 @@
# end
## == Menus ==
- config.main_menu = lambda {
- section "Dashboards", icon: "dashboards" do
- all_dashboards
- end
+ if defined?(Avo::Pro)
+ config.main_menu = lambda {
+ section "Dashboards", icon: "dashboards" do
+ all_dashboards
+ end
- section "Resources", icon: "resources" do
- Avo::App.resources_for_navigation.group_by { |r| r.model_class.module_parent_name }.sort_by { |k, _| k.to_s }.each do |namespace, reses|
- if namespace.present?
- group namespace.titleize, icon: "folder" do
+ section "Resources", icon: "resources" do
+ Avo.resource_manager.resources_for_navigation(current_user).group_by { |r| r.model_class.module_parent_name }
+ .sort_by { |k, _| k.to_s }.each do |namespace, reses|
+ if namespace.present?
+ group namespace.titleize, icon: "folder" do
+ reses.each do |res|
+ resource res.route_key
+ end
+ end
+ else
reses.each do |res|
resource res.route_key
end
end
- else
- reses.each do |res|
- resource res.route_key
- end
end
end
- end
- unless all_tools.empty?
- section "Tools", icon: "tools" do
- all_tools
+ unless all_tools.empty?
+ section "Tools", icon: "tools" do
+ all_tools
+ end
end
- end
- }
+ }
+ end
config.profile_menu = lambda {
link_to "Admin Profile",
@@ -136,11 +145,8 @@
Rails.configuration.to_prepare do
Avo::ApplicationController.include GitHubOAuthable
Avo::BaseController.prepend AvoAuditable
- Avo::BaseResource.include Concerns::AvoAuditableResource
-
- Avo::ApplicationController.content_security_policy do |policy|
- policy.style_src :self, "https://fonts.googleapis.com", :unsafe_inline
- end
+ Avo::BaseResource.prepend Avo::Resources::Concerns::AvoAuditableResource
+ Avo::Concerns::HasItems.prepend Avo::Resources::Concerns::AvoAuditableResource::HasItemsIncludeComment
# Fix for https://github.com/rails/rails/issues/49783
Avo::Views::ResourceEditComponent.class_eval do
diff --git a/config/initializers/better_html.rb b/config/initializers/better_html.rb
index 07b30994ef3..3eb5dc80cfe 100644
--- a/config/initializers/better_html.rb
+++ b/config/initializers/better_html.rb
@@ -1,5 +1,5 @@
BetterHtml.configure do |config|
config.template_exclusion_filter = proc { |filename|
- filename.include?("avo")
+ filename.include?("avo") || filename.include?("/railties-")
}
end
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index f332248831b..df1cf833606 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -1,49 +1,51 @@
# Be sure to restart your server when you modify this file.
-# Define an application-wide content security policy
-# For further information see the following documentation
-# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+# Define an application-wide content security policy.
+# See the Securing Rails Applications Guide for more information:
+# https://guides.rubyonrails.org/security.html#content-security-policy-header
-Rails.application.config.content_security_policy do |policy|
- policy.default_src :self
- policy.font_src :self, "https://fonts.gstatic.com"
- policy.img_src :self, "https://secure.gaug.es", "https://gravatar.com", "https://www.gravatar.com", "https://secure.gravatar.com",
- "https://*.fastly-insights.com", "https://avatars.githubusercontent.com"
- policy.object_src :none
- # NOTE: This scirpt_src is overridden for all requests in ApplicationController
- # This is the baseline in case the override is ever skipped
- policy.script_src :self, "https://secure.gaug.es", "https://www.fastly-insights.com"
- policy.style_src :self, "https://fonts.googleapis.com"
- policy.connect_src :self, "https://s3-us-west-2.amazonaws.com/rubygems-dumps/", "https://*.fastly-insights.com", "https://fastly-insights.com",
- "https://api.github.com", "http://localhost:*"
- policy.form_action :self, "https://github.com/login/oauth/authorize"
- policy.frame_ancestors :self
- policy.base_uri :self
+Rails.application.configure do
+ config.content_security_policy do |policy|
+ policy.default_src :self
+ policy.font_src :self, "https://fonts.gstatic.com"
+ policy.img_src :self, "data:", "https://secure.gaug.es", "https://gravatar.com", "https://www.gravatar.com", "https://secure.gravatar.com",
+ "https://*.fastly-insights.com", "https://avatars.githubusercontent.com"
+ policy.object_src :none
+ # NOTE: This scirpt_src is overridden for all requests in ApplicationController
+ # This is the baseline in case the override is ever skipped
+ policy.script_src :self, "https://secure.gaug.es", "https://www.fastly-insights.com"
+ policy.style_src :self, :unsafe_inline, "https://fonts.googleapis.com"
+ policy.connect_src :self, "https://s3-us-west-2.amazonaws.com/rubygems-dumps/", "https://*.fastly-insights.com", "https://fastly-insights.com",
+ "https://api.github.com", "http://localhost:*"
+ policy.form_action :self, "https://github.com/login/oauth/authorize"
+ policy.frame_ancestors :self
+ policy.base_uri :self
- # Specify URI for violation reports
- policy.report_uri lambda {
- dd_api_key = ENV['DATADOG_CSP_API_KEY'].presence
- url = ActionDispatch::Http::URL.url_for(
- protocol: 'https',
- host: 'csp-report.browser-intake-datadoghq.com',
- path: '/api/v2/logs',
- params: {
- "dd-api-key": dd_api_key,
- "dd-evp-origin": 'content-security-policy',
- ddsource: 'csp-report',
- ddtags: {
- service: "rubygems.org",
- version: AppRevision.version,
- env: Rails.env,
- trace_id: Datadog::Tracing.correlation&.trace_id,
- "gemcutter.user.id": (current_user.id if respond_to?(:signed_in?) && signed_in?)
- }.compact.map { |k, v| "#{k}:#{v}" }.join(',')
- }
- )
- # ensure we compute the URL on development/test,
- # but onlu return it if the API key is configures
- url if dd_api_key
- }
+ # Specify URI for violation reports
+ policy.report_uri lambda {
+ dd_api_key = ENV['DATADOG_CSP_API_KEY'].presence
+ url = ActionDispatch::Http::URL.url_for(
+ protocol: 'https',
+ host: 'csp-report.browser-intake-datadoghq.com',
+ path: '/api/v2/logs',
+ params: {
+ "dd-api-key": dd_api_key,
+ "dd-evp-origin": 'content-security-policy',
+ ddsource: 'csp-report',
+ ddtags: {
+ service: "rubygems.org",
+ version: AppRevision.version,
+ env: Rails.env,
+ trace_id: Datadog::Tracing.correlation&.trace_id,
+ "gemcutter.user.id": (current_user.id if respond_to?(:signed_in?) && signed_in?)
+ }.compact.map { |k, v| "#{k}:#{v}" }.join(',')
+ }
+ )
+ # ensure we compute the URL on development/test,
+ # but onlu return it if the API key is configures
+ url if dd_api_key
+ }
+ end
end
# Generate session nonces for permitted importmap, inline scripts, and inline styles.
@@ -53,8 +55,4 @@
request.session.send(:load_for_write!) # force session to be created
request.session.id.to_s.presence || SecureRandom.base64(16)
}
-Rails.application.config.content_security_policy_nonce_directives = %w[script-src style-src]
-
-# Report CSP violations to a specified URI. See:
-# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
-# config.content_security_policy_report_only = truepoint
+Rails.application.config.content_security_policy_nonce_directives = %w[script-src]
diff --git a/config/initializers/datadog.rb b/config/initializers/datadog.rb
index 5a508fe401d..9681e9c18aa 100644
--- a/config/initializers/datadog.rb
+++ b/config/initializers/datadog.rb
@@ -1,5 +1,7 @@
require "app_revision"
+return if Rails.env.local? # Don't enable Datadog in local Development & Test environments
+
Datadog.configure do |c|
# unified service tagging
@@ -9,7 +11,7 @@
# Enabling datadog functionality
- enabled = !Rails.env.local? && ENV["DD_AGENT_HOST"].present? && !defined?(Rails::Console)
+ enabled = ENV["DD_AGENT_HOST"].present? && !defined?(Rails::Console)
c.runtime_metrics.enabled = enabled
c.profiling.enabled = enabled
c.tracing.enabled = enabled
@@ -43,10 +45,12 @@
c.tracing.instrument :opensearch, service_name: c.service
c.tracing.instrument :pg
c.tracing.instrument :rails, request_queuing: true
- c.tracing.instrument :shoryuken
+ c.tracing.instrument :shoryuken if defined?(Shoryuken)
end
Datadog::Tracing.before_flush(
# Remove spans for the /internal/ping endpoint
Datadog::Tracing::Pipeline::SpanFilter.new { |span| span.resource == "Internal::PingController#index" }
)
+
+require "datadog/auto_instrument"
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
index d997f9aa485..89c5d312f58 100644
--- a/config/initializers/filter_parameter_logging.rb
+++ b/config/initializers/filter_parameter_logging.rb
@@ -3,7 +3,7 @@
# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
# Use this to limit dissemination of sensitive information.
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
-Rails.application.config.filter_parameters += %I[
- password passw secret token _key crypt salt certificate otp ssn api_key recovery_codes seed
- jwt
+Rails.application.config.filter_parameters += %i[
+ passw email secret token _key crypt salt certificate otp ssn
+ api_key recovery_codes seed jwt password
]
diff --git a/config/initializers/honeybadger.rb b/config/initializers/honeybadger.rb
index fa6efe727ab..cf96d851c43 100644
--- a/config/initializers/honeybadger.rb
+++ b/config/initializers/honeybadger.rb
@@ -1,7 +1,13 @@
-Honeybadger.configure do |config|
- config.report_data = false if Rails.env.development?
- config.before_notify do |notice|
- notice.halt! if ActionDispatch::ExceptionWrapper.rescue_responses.key?(notice.error_class)
+return if Rails.env.local? # Don't enable Honeybadger in local Development & Test environments
+
+Rails.logger.silence(:error) do
+ require "honeybadger"
+
+ Honeybadger.configure do |config|
+ config.before_notify do |notice|
+ notice.halt! if ActionDispatch::ExceptionWrapper.rescue_responses.key?(notice.error_class)
+ end
+
+ config.logger = SemanticLogger[Honeybadger]
end
- config.logger = SemanticLogger[Honeybadger]
end
diff --git a/config/initializers/new_framework_defaults_7_2.rb b/config/initializers/new_framework_defaults_7_2.rb
new file mode 100644
index 00000000000..b549c4a258a
--- /dev/null
+++ b/config/initializers/new_framework_defaults_7_2.rb
@@ -0,0 +1,70 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file eases your Rails 7.2 framework defaults upgrade.
+#
+# Uncomment each configuration one by one to switch to the new default.
+# Once your application is ready to run with all new defaults, you can remove
+# this file and set the `config.load_defaults` to `7.2`.
+#
+# Read the Guide for Upgrading Ruby on Rails for more info on each option.
+# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
+
+###
+# Controls whether Active Job's `#perform_later` and similar methods automatically defer
+# the job queuing to after the current Active Record transaction is committed.
+#
+# Example:
+# Topic.transaction do
+# topic = Topic.create(...)
+# NewTopicNotificationJob.perform_later(topic)
+# end
+#
+# In this example, if the configuration is set to `:never`, the job will
+# be enqueued immediately, even though the `Topic` hasn't been committed yet.
+# Because of this, if the job is picked up almost immediately, or if the
+# transaction doesn't succeed for some reason, the job will fail to find this
+# topic in the database.
+#
+# If `enqueue_after_transaction_commit` is set to `:default`, the queue adapter
+# will define the behaviour.
+#
+# Note: Active Job backends can disable this feature. This is generally done by
+# backends that use the same database as Active Record as a queue, hence they
+# don't need this feature.
+#++
+# Rails.application.config.active_job.enqueue_after_transaction_commit = :default
+
+###
+# Adds image/webp to the list of content types Active Storage considers as an image
+# Prevents automatic conversion to a fallback PNG, and assumes clients support WebP, as they support gif, jpeg, and png.
+# This is possible due to broad browser support for WebP, but older browsers and email clients may still not support
+# WebP. Requires imagemagick/libvips built with WebP support.
+#++
+# Rails.application.config.active_storage.web_image_content_types = %w[image/png image/jpeg image/gif image/webp]
+
+###
+# Enable validation of migration timestamps. When set, an ActiveRecord::InvalidMigrationTimestampError
+# will be raised if the timestamp prefix for a migration is more than a day ahead of the timestamp
+# associated with the current time. This is done to prevent forward-dating of migration files, which can
+# impact migration generation and other migration commands.
+#
+# Applications with existing timestamped migrations that do not adhere to the
+# expected format can disable validation by setting this config to `false`.
+#++
+# Rails.application.config.active_record.validate_migration_timestamps = true
+
+###
+# Controls whether the PostgresqlAdapter should decode dates automatically with manual queries.
+#
+# Example:
+# ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.select_value("select '2024-01-01'::date") #=> Date
+#
+# This query used to return a `String`.
+#++
+# Rails.application.config.active_record.postgresql_adapter_decode_dates = true
+
+###
+# Enables YJIT as of Ruby 3.3, to bring sizeable performance improvements. If you are
+# deploying to a memory constrained environment you may want to set this to `false`.
+#++
+# Rails.application.config.yjit = true
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index a41c0d6777e..0bacd8f5845 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -11,3 +11,20 @@
OmniAuth::AuthenticityTokenProtection.default_options(key: "csrf.token", authenticity_param: "_csrf")
OmniAuth.config.logger = SemanticLogger[OmniAuth]
+
+class FailureEndpoint < OmniAuth::FailureEndpoint
+ # ensures that same-site: strict cookies are available for csrf validation
+ def call
+ if env["omniauth.error.type"] == :csrf_detected && env["HTTP_SEC_FETCH_SITE"] == "cross-site"
+ request = Rack::Request.new(env)
+ # avoid overwriting the (real) session cookie
+ request.session.options[:skip] = true
+ # redirect to the same path, but with a meta refresh to avoid the browser treating the request as cross-site
+ [303, {}, [""]]
+ else
+ super
+ end
+ end
+end
+
+OmniAuth.config.on_failure = FailureEndpoint
diff --git a/config/initializers/prosopite.rb b/config/initializers/prosopite.rb
index 95e7e0208d6..19c65ff5a37 100644
--- a/config/initializers/prosopite.rb
+++ b/config/initializers/prosopite.rb
@@ -11,8 +11,9 @@
"app/mailers/",
# avo auditing potentially loads things multiple times, but it will be bounded by the size of the audit
- "app/avo/actions/base_action.rb",
+ "app/avo/actions/application_action.rb",
"app/components/avo/fields/audited_changes_field/show_component.html.erb",
+ "app/components/avo/views/resource_index_component.html.erb",
# calls count for each owner, AR doesn't yet allow preloading aggregates
"app/views/ownership_requests/_ownership_request.html.erb"
diff --git a/config/initializers/requires.rb b/config/initializers/requires.rb
index 966db0b8de5..8a6f441cff6 100644
--- a/config/initializers/requires.rb
+++ b/config/initializers/requires.rb
@@ -5,3 +5,4 @@
require 'rack/rewindable_input'
require 'elastic_searcher'
require 'github_secret_scanning'
+require 'access'
diff --git a/config/initializers/semantic_logger.rb b/config/initializers/semantic_logger.rb
index 44ddc2ab5c4..52e3cf17a36 100644
--- a/config/initializers/semantic_logger.rb
+++ b/config/initializers/semantic_logger.rb
@@ -29,7 +29,7 @@ def append_info_to_payload(payload)
url: request.url
}
- method_and_path = [request.method, request.path].select(&:present?)
+ method_and_path = [request.method, request.path].compact_blank
method_and_path_string = method_and_path.empty? ? ' ' : " #{method_and_path.join(' ')} "
payload[:message] ||= "[#{response.status}]#{method_and_path_string}(#{payload.fetch(:controller)}##{payload.fetch(:action)})"
diff --git a/config/initializers/sendgrid.rb b/config/initializers/sendgrid.rb
index 810b664e5a3..51a64f8dce7 100644
--- a/config/initializers/sendgrid.rb
+++ b/config/initializers/sendgrid.rb
@@ -6,7 +6,7 @@
password: ENV['SENDGRID_PASSWORD'],
domain: 'mailer.rubygems.org',
authentication: :plain,
- enable_starttls_auto: true
+ enable_starttls: true
}
ActionMailer::Base.delivery_method = :smtp
end
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 28c03b11a72..445a076ac0d 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -5,7 +5,7 @@
# Be sure to restart your server when you modify this file.
-Rails.application.config.session_store :cookie_store, key: '_rubygems_session'
+Rails.application.config.session_store :cookie_store, key: '_rubygems_session', same_site: :strict
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information
diff --git a/config/initializers/sigstore.rb b/config/initializers/sigstore.rb
new file mode 100644
index 00000000000..6328db2eb58
--- /dev/null
+++ b/config/initializers/sigstore.rb
@@ -0,0 +1,12 @@
+require "sigstore/verifier"
+require "sigstore/rekor/client"
+require "sigstore/models"
+require "sigstore/policy"
+require "sigstore/signer"
+
+module Sigstore::Loggable::ClassMethods
+ undef_method :logger
+ def logger
+ @semantic_logger ||= SemanticLogger[self] # rubocop:disable Naming/MemoizedInstanceVariableName
+ end
+end
diff --git a/config/initializers/statsd.rb b/config/initializers/statsd.rb
index 2e91b49bc84..a3721b165bb 100644
--- a/config/initializers/statsd.rb
+++ b/config/initializers/statsd.rb
@@ -57,8 +57,10 @@
ActiveSupport::Notifications.subscribe("perform_job.good_job") do |event|
execution = event.payload[:execution]
+ # TODO: remove || execution after GoodJob 4 upgrade
+ job = event.payload[:job] || execution
- result = if event.payload[:retried] || execution.retried_good_job_id.present?
+ result = if event.payload[:retried] || job.retried_good_job_id.present?
:retried
elsif event.payload[:unhandled_error]
:unhandled_error
@@ -72,7 +74,7 @@
job_class: execution.serialized_params['job_class'],
exception: event.payload.dig(:exception, 0),
queue: execution.queue_name,
- priority: execution.priority,
+ priority: job.priority,
result:
}
diff --git a/config/initializers/strong_migrations.rb b/config/initializers/strong_migrations.rb
index 17f3f042095..1d977dad858 100644
--- a/config/initializers/strong_migrations.rb
+++ b/config/initializers/strong_migrations.rb
@@ -12,7 +12,7 @@
# Set the version of the production database
# so the right checks are run in development
-StrongMigrations.target_version = "11.3"
+StrongMigrations.target_version = "13"
# Add custom checks
# StrongMigrations.add_check do |method, args|
diff --git a/config/initializers/zeitwerk.rb b/config/initializers/zeitwerk.rb
index 896e8817c1b..11a2829e0d1 100644
--- a/config/initializers/zeitwerk.rb
+++ b/config/initializers/zeitwerk.rb
@@ -9,6 +9,12 @@
Rails.autoloaders.main.ignore(Rails.root.join("lib/puma/plugin"))
+unless defined?(Avo::Pro)
+ Rails.autoloaders.main.ignore(Rails.root.join("app/avo/cards"))
+ Rails.autoloaders.main.ignore(Rails.root.join("app/avo/dashboards"))
+ Rails.autoloaders.main.ignore(Rails.root.join("lib/admin/authorization_client.rb"))
+end
+
Rails.autoloaders.once.inflector.inflect(
"http" => "HTTP",
"oidc" => "OIDC",
diff --git a/config/locales/de.yml b/config/locales/de.yml
index d490bee4403..66e603971e6 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -4,6 +4,7 @@ de:
copied: Kopiert!
copy_to_clipboard: In die Zwischenablage kopieren
edit: Bearbeiten
+ hide: Verbergen
verification_expired:
feed_latest: RubyGems.org | Neueste Gems
feed_subscribed: RubyGems.org | Abonnierte Gems
@@ -26,7 +27,11 @@ de:
locale_name: Deutsch
none: None
not_found: Nicht gefunden
+ forbidden:
+ api_gem_not_found:
api_key_forbidden:
+ api_key_soft_deleted:
+ api_key_insufficient_scope:
please_sign_up: Zugriff verweigert. Bitte melden Sie sich unter https://rubygems.org
mit einem Konto an
please_sign_in: Bitte melden Sie sich an, um fortzufahren.
@@ -35,6 +40,7 @@ de:
otp_missing: Sie haben die Mehrfaktor-Authentifizierung aktiviert, aber keinen OTP-Code
angegeben. Bitte füllen Sie diesen aus und versuchen Sie es erneut.
sign_in: Anmelden
+ sign_out: Abmelden
sign_up: Registrieren
dependency_list: Alle transitiven Abhängigkeiten anzeigen
multifactor_authentication: Mehrfaktorauthentifizierung
@@ -42,8 +48,10 @@ de:
this_rubygem_could_not_be_found: Dieses Ruby Gem konnte nicht gefunden werden.
time_ago: seit %{duration}
title: RubyGems.org
- update: Aktualisieren
+ total_downloads: Downloads insgesamt
try_again: Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.
+ update: Aktualisieren
+ view_all: Alle anzeigen
advanced_search: Erweiterte Suche
authenticate: Authentifizieren
helpers:
@@ -69,6 +77,14 @@ de:
full_name: Vollständiger Name
handle: Benutzername
password: Passwort
+ ownership/role:
+ owner:
+ maintainer:
+ membership/role:
+ owner:
+ admin:
+ maintainer:
+ outside_contributor:
api_key:
oidc_api_key_role: OIDC-API-Schlüsselrolle
oidc/id_token:
@@ -91,11 +107,26 @@ de:
attributes:
expires_at:
inclusion:
+ organization:
+ attributes:
+ handle:
+ invalid:
ownership:
attributes:
user_id:
already_confirmed: ist bereits Eigentümer dieses Gems
already_invited: wurde bereits zu diesem Gem eingeladen
+ ownership_request:
+ attributes:
+ user_id:
+ taken:
+ existing:
+ user:
+ attributes:
+ handle:
+ invalid:
+ password:
+ bcrypt_length:
version:
attributes:
gem_full_name:
@@ -169,6 +200,7 @@ de:
können. Neuer API-Schlüssel:'
mfa: MFA
expiration:
+ update_owner:
new:
new_api_key: Neuer API-Schlüssel
reset:
@@ -193,9 +225,10 @@ de:
mine: Meine Gems
my_subscriptions: Abonnements
no_owned_html: Du hast noch keine Gems gepusht. Schau doch vielleicht die Dokumentation
- auf %{creating_link} an und %{migrating_link} ein Gem from RubyForge.
+ auf %{creating_link}.
no_subscriptions_html: Du hast noch keine Gems abonniert. Besuche %{gem_link}
um das Gem zu abonnieren!
+ organizations:
title: Dashboard
dependencies:
show:
@@ -226,6 +259,7 @@ de:
blog: Blog
contribute: Beitragen
data: Daten
+ docs: Dokumentation
designed_by: Design von
discussion_forum: Diskussion
gems_served_by: Gems angeboten von
@@ -240,6 +274,9 @@ de:
statistics: Statistiken
status: Status
supported_by: Unterstützt von
+ supported_by_html: |
+ RubyGems.org
+ wird unterstützt von
tested_by:
tracking_by: Getrackt von
uptime: Betriebszeit
@@ -247,14 +284,18 @@ de:
secured_by:
looking_for_maintainers:
header:
+ profile: Profil
dashboard: Dashboard
- settings:
- edit_profile:
+ settings: Einstellungen
+ edit_profile: Profil bearbeiten
search_gem_html: Suche Gems…
- sign_in: Anmelden
- sign_out: Abmelden
- sign_up: Registrieren
mfa_banner_html:
+ breadcrumbs:
+ home: Startseite
+ dashboard: Dashboard
+ settings: Einstellungen
+ org_name: 'Organisation: %{name}'
+ subscriptions: Abonnements
mailer:
confirm_your_email: Bitte bestätigen Sie Ihre E-Mail-Adresse mit dem Link, der
an Ihre E-Mail gesendet wurde.
@@ -352,6 +393,12 @@ de:
subtitle: Hallo %{user_handle}!
body_html: Du wurdest als Besitzer für das %{gem}-Gem
auf %{host} von %{remover} entfernt.
+ owner_updated:
+ subject:
+ title:
+ subtitle:
+ body_html:
+ body_text:
ownerhip_request_closed:
title: ANFRAGE ZUR BESITZERRECHTIGUNG
subtitle: Hallo %{handle}!
@@ -410,6 +457,8 @@ de:
gepusht wurde.
gem_trusted_publisher_added:
title: VERTRAUENSWÜRDIGER PUBLISHER HINZUGEFÜGT
+ admin_manual:
+ title:
news:
show:
title: Neue Veröffentlichungen — Alle Gems
@@ -421,45 +470,41 @@ de:
about:
contributors_amount: "%{count} Rubyists"
downloads_amount: Millionen von Gem-Downloads
- checkout_code: bitte prüfe den Code
- mit_licensed: MIT-lizenziert
+ checkout_code: sieh dir einfach den Code an
+ mit_licensed: MIT Lizenz
logo_header: Auf der Suche nach unserem Logo?
- logo_details: Wähle einfach den Download-Button, um drei .PNGs und eine .SVG
- des RubyGems-Logos für dich zu erhalten.
+ logo_details: Klick einfach auf den Download-Button, um drei PNGs und eine SVG
+ des RubyGems-Logos zu erhalten.
founding_html: Das Projekt wurde im April 2009 von %{founder} gestartet und
- hat seitdem über %{contributors} und %{downloads}. Ab dem Release von RubyGems
- 1.3.6 wurde die Website von Gemcutter zu %{title} umbenannt, um eine zentrale
- Rolle in der Ruby-Community zu verfestigen.
- support_html: Obwohl RubyGems.org nicht von einem bestimmten Unternehmen ausgeführt
+ ist seitdem dank %{contributors} und %{downloads} gewachsen. Mit dem Release
+ von RubyGems 1.3.6 wurde die Website von Gemcutter zu %{title} umbenannt,
+ um ihre zentrale Rolle in der Ruby-Community zu verdeutlichen.
+ support_html: Obwohl RubyGems.org nicht von einem zentralen Unternehmen geführt
wird, haben viele Unternehmen uns bereits unterstützt. Das aktuelle Design,
- die Illustrationen und die Front-End-Entwicklung für dieser Webseite wurden
- von %{dockyard} erstellt. %{github} hat auch von unschätzbarem Wert uns geholfen
- den Code mit anderen Entwicklern zuteilen, um an dem Code leichter zusammenarbeiten.
- Die Website wurde mit %{heroku} gestartet, dessen großen Dienst uns geholfen,
- RubyGems als eine praktikable Lösung zu erweisen, wo die ganze Gemeinschaft
- darauf verlassen kann. Our infrastructure is currently hosted on %{aws}.
+ die Illustrationen und die Frontend-Entwicklung wurden von %{dockyard} erstellt.
+ %{github} war auch von unschätzbarem Wert und hat uns sehr geholfen, zusammenzuarbeiten
+ und Code zu teilen. Die Website wurde mit %{heroku} gestartet, großartiger
+ Service dazu beitrug, RubyGems als eine praktikable Lösung zu beweisen, auf
+ die sich die gesamte Community verlassen kann. Unsere Infrastruktur wird derzeit
+ auf %{aws} gehostet.
technical_html: 'Einige Einblicke in die technischen Aspekte dieser Webseite:
- Es ist 100% in Ruby geschrieben. Die Hauptseite ist verwendet die %{rails}-Software.
- Die Gems werden in %{s3}, served by %{fastly}, gehostet und die Zeit zwischen
- der Veröffentlichung eines neuen Gems und nachdem er bereit zur Installation
- freigegeben ist, ist minimal. Für mehr Informationen siehe %{source_code}
- (%{license}) auf GitHub.'
+ Sie ist zu 100% in Ruby geschrieben. Die Hauptseite ist eine %{rails} App.
+ Die Gems werden auf %{s3} gehostet und über %{fastly} ausgeliefert, und die
+ Zeit nach der Veröffentlichung eines neuen Gems, bis es zur Installation bereit
+ seht, beträgt meistens nur einige wenige Sekunden. Für mehr Informationen
+ %{source_code}, der mit der %{license} auf GitHub bereitseht.'
title: Über uns
purpose:
- better_api: Bereitstellen eines besseren APIs für den Umgang mit Gems
- enable_community: Der Gemeinschaft ermöglichen, die Webseite zu verbessern
+ better_api: Eine bessere API bereitzustellen, um mit Gems zu arbeiten
+ enable_community: Es der Community zu ermöglichen, diese Webseite zu verbessern
und zu erweitern
- header: 'Willkommen in %{site}, die Community für Ruby Gem-Hostingservice.
- Die Gründe sind vielfältig:'
- transparent_pages: Erstellen transparenter und zugänglicher Projektseiten
+ header: 'Willkommen bei RubyGems.org, dem Gem-Hosting-Srrvice der Ruby-Community.
+ Dieses Projekt hat drei Ziele:'
+ transparent_pages: Mehr transparente und barrierefreie Projektseiten zu erstellen
data:
title: RubyGems.org Data Dumps
download:
title: Download RubyGems
- faq:
- title: FAQ
- migrate:
- title: Migrate gems
security:
title: Security
sponsors:
@@ -511,12 +556,17 @@ de:
die Sicherheit deines Kontos, indem du ein
neues Gerät einrichtest. Erfahre
mehr!"
+ api:
+ mfa_required:
+ mfa_required_not_yet_enabled:
+ mfa_required_weak_level_enabled:
+ mfa_recommended_not_yet_enabled:
+ mfa_recommended_weak_level_enabled:
recovery:
- copied: "[ kopiert ]"
continue: Weiter
title: Wiederherstellungscodes
- copy: "[ kopieren ]"
saved: Ich bestätige, dass ich meine Wiederherstellungscodes gespeichert habe.
+ confirm_dialog:
note_html: Bitte kopieren und speichern
Sie diese Wiederherstellungscodes. Sie können diese Codes verwenden, um sich
anzumelden und Ihre MFA zurückzusetzen, wenn Sie Ihr Authentifizierungsgerät
@@ -593,7 +643,13 @@ de:
failed_verification:
title:
close_browser:
+ organization_onboardings:
+ confirm:
+ success:
owners:
+ edit:
+ role:
+ title:
confirm:
confirmed_email:
token_expired:
@@ -605,6 +661,8 @@ de:
confirmed_at:
added_by:
action:
+ role:
+ role_field:
email_field:
submit_button:
info:
@@ -615,6 +673,9 @@ de:
resent_notice:
create:
success_notice:
+ update:
+ update_current_user_role:
+ success_notice:
destroy:
removed_notice:
failed_notice:
@@ -698,6 +759,7 @@ de:
rubygems:
aside:
downloads_for_this_version: Für diese Version
+ gem_version_age: Version veröffentlicht
required_ruby_version: Erforderliche Ruby-Version
required_rubygems_version:
requires_mfa:
@@ -743,6 +805,7 @@ de:
sha_256_checksum: SHA 256-Prüfsumme
signature_period:
expired:
+ provenance_header:
version_navigation:
previous_version:
next_version:
@@ -784,7 +847,7 @@ de:
updated:
yanked:
show:
- subtitle: für %{query}
+ subtitle_html: für %{query}
month_update:
week_update:
filter:
@@ -804,7 +867,6 @@ de:
index:
title:
all_time_most_downloaded: Am meisten Heruntergeladen in der gesamten Zeit
- total_downloads: Downloads insgesamt
total_gems: Gems insgesamt
total_users: Benutzer insgesamt
users:
@@ -870,8 +932,6 @@ de:
continue:
title:
notice_html:
- copied:
- copy:
saved:
webauthn_credential:
confirm_delete:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index f78235ae00f..d88cefe17e9 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -4,6 +4,7 @@ en:
copied: Copied!
copy_to_clipboard: Copy to clipboard
edit: Edit
+ hide: Hide
verification_expired: The verification has expired. Please verify again.
feed_latest: RubyGems.org | Latest Gems
feed_subscribed: RubyGems.org | Subscribed Gems
@@ -26,12 +27,17 @@ en:
locale_name: English
none: None
not_found: Not Found
+ forbidden: Forbidden
+ api_gem_not_found: This gem could not be found
api_key_forbidden: The API key doesn't have access
+ api_key_soft_deleted: An invalid API key cannot be used. Please delete it and create a new one.
+ api_key_insufficient_scope: This API key cannot perform the specified action on this gem.
please_sign_up: Access Denied. Please sign up for an account at https://rubygems.org
please_sign_in: Please sign in to continue.
otp_incorrect: Your OTP code is incorrect. Please check it and retry.
otp_missing: You have enabled multifactor authentication but no OTP code provided. Please fill it and retry.
sign_in: Sign in
+ sign_out: Sign out
sign_up: Sign up
dependency_list: Show all transitive dependencies
multifactor_authentication: Multi-factor authentication
@@ -39,8 +45,10 @@ en:
this_rubygem_could_not_be_found: This rubygem could not be found.
time_ago: "%{duration} ago"
title: RubyGems.org
- update: Update
+ total_downloads: Total downloads
try_again: Something went wrong. Please try again.
+ update: Update
+ view_all: View all
advanced_search: Advanced Search
authenticate: Authenticate
helpers:
@@ -66,6 +74,14 @@ en:
full_name: Full name
handle: Username
password: Password
+ ownership/role:
+ owner: Owner
+ maintainer: Maintainer
+ membership/role:
+ owner: Owner
+ admin: Admin
+ maintainer: Maintainer
+ outside_contributor: Outside Contributor
api_key:
oidc_api_key_role: OIDC API Key Role
oidc/id_token:
@@ -86,11 +102,26 @@ en:
attributes:
expires_at:
inclusion: "must be in the future"
+ organization:
+ attributes:
+ handle:
+ invalid: "must start with a letter and can only contain letters, numbers, underscores, and dashes"
ownership:
attributes:
user_id:
already_confirmed: "is already an owner of this gem"
already_invited: "is already invited to this gem"
+ ownership_request:
+ attributes:
+ user_id:
+ taken: "has already requested ownership"
+ existing: "is already an owner"
+ user:
+ attributes:
+ handle:
+ invalid: "must start with a letter and can only contain letters, numbers, underscores, and dashes"
+ password:
+ bcrypt_length: is too long (maximum is 72 bytes)
version:
attributes:
gem_full_name:
@@ -161,6 +192,7 @@ en:
save_key: "Note that we won't be able to show the key to you again. New API key:"
mfa: MFA
expiration: Expiration
+ update_owner: Update Owner
new:
new_api_key: New API key
reset:
@@ -182,8 +214,9 @@ en:
migrating_link_text: migrating
mine: My Gems
my_subscriptions: Subscriptions
- no_owned_html: You haven't pushed any gems yet. Perhaps check out the guides on %{creating_link} a gem or %{migrating_link} a gem from RubyForge.
+ no_owned_html: You haven't pushed any gems yet. Perhaps check out the guide on %{creating_link} a gem.
no_subscriptions_html: You're not subscribed to any gems yet. Visit a %{gem_link} to subscribe to one!
+ organizations: Organizations
title: Dashboard
dependencies:
show:
@@ -212,6 +245,7 @@ en:
blog: Blog
contribute: Contribute
data: Data
+ docs: Docs
designed_by: Designed by
discussion_forum: Discuss
gems_served_by: Gems served by
@@ -226,6 +260,9 @@ en:
statistics: Stats
status: Status
supported_by: Supported by
+ supported_by_html: |
+ RubyGems.org
+ is supported by
tested_by: Tested by
tracking_by: Tracking by
uptime: Uptime
@@ -233,14 +270,18 @@ en:
secured_by: Secured by
looking_for_maintainers: maintainers wanted
header:
+ profile: Profile
dashboard: Dashboard
settings: Settings
edit_profile: Edit profile
search_gem_html: Search Gems…
- sign_in: Sign in
- sign_out: Sign out
- sign_up: Sign up
mfa_banner_html: 🎉 We now support security devices! Improve your account security by setting up a new device. [Learn more](link to blog post)!
+ breadcrumbs:
+ home: Home
+ dashboard: Dashboard
+ settings: Settings
+ org_name: "Org: %{name}"
+ subscriptions: Subscriptions
mailer:
confirm_your_email: Please confirm your email address with the link sent to your email.
confirmation_subject: Please confirm your email address with %{host}
@@ -318,6 +359,12 @@ en:
title: OWNER REMOVED
subtitle: Hi %{user_handle}!
body_html: You were removed as an owner for %{gem} gem on %{host} by %{remover}.
+ owner_updated:
+ subject: Your role was updated for %{gem} gem
+ title: OWNER ROLE UPDATED
+ subtitle: Hi %{user_handle}!
+ body_html: Your role was updated to %{role} for %{gem} gem.
+ body_text: Your role was updated to %{role} for %{gem} gem.
ownerhip_request_closed:
title: OWNERSHIP REQUEST
subtitle: Hi %{handle}!
@@ -360,6 +407,8 @@ en:
gem_html: This webhook was previously called when %{gem} was pushed.
gem_trusted_publisher_added:
title: TRUSTED PUBLISHER ADDED
+ admin_manual:
+ title: MESSAGE FROM RUBYGEMS.ORG ADMINS
news:
show:
title: New Releases — All Gems
@@ -388,10 +437,6 @@ en:
title: RubyGems.org Data Dumps
download:
title: Download RubyGems
- faq:
- title: FAQ
- migrate:
- title: Migrate gems
security:
title: Security
sponsors:
@@ -427,12 +472,27 @@ en:
strong_mfa_level_required_html: For protection of your account and your gems, you are required to change your MFA level to "UI and gem signin" or "UI and API". Please read our blog post for more details.
strong_mfa_level_recommended: For protection of your account and your gems, we encourage you to change your MFA level to "UI and gem signin" or "UI and API". Your account will be required to have MFA enabled on one of these levels in the future.
setup_webauthn_html: 🎉 We now support security devices! Improve your account security by setting up a new device. Learn more!
+ api:
+ mfa_required: Gem requires MFA enabled; You do not have MFA enabled yet.
+ mfa_required_not_yet_enabled: |
+ [ERROR] For protection of your account and your gems, you are required to set up multi-factor authentication at https://rubygems.org/totp/new.
+
+ Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
+ mfa_required_weak_level_enabled: |
+ [ERROR] For protection of your account and your gems, you are required to change your MFA level to 'UI and gem signin' or 'UI and API' at https://rubygems.org/settings/edit.
+
+ Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
+ mfa_recommended_not_yet_enabled: |
+ [WARNING] For protection of your account and gems, we encourage you to set up multi-factor authentication at https://rubygems.org/totp/new.
+ Your account will be required to have MFA enabled in the future.
+ mfa_recommended_weak_level_enabled: |
+ [WARNING] For protection of your account and gems, we encourage you to change your multi-factor authentication level to 'UI and gem signin' or 'UI and API' at https://rubygems.org/settings/edit.
+ Your account will be required to have MFA enabled on one of these levels in the future.
recovery:
- copied: "[ copied ]"
continue: Continue
title: Recovery codes
- copy: "[ copy ]"
saved: I acknowledge that I have saved my recovery codes.
+ confirm_dialog: Leave without copying recovery codes?
note_html: "Please copy and save these recovery codes. You can use these codes to login and reset your MFA if your lose your authentication device. Each code can be used once."
already_generated: You should have already saved your recovery codes.
update:
@@ -496,7 +556,13 @@ en:
failed_verification:
title: Error - Verification Failed
close_browser: Please close this browser and try again.
+ organization_onboardings:
+ confirm:
+ success: Organization onboarded successfully!
owners:
+ edit:
+ role: Role
+ title: Edit Owner
confirm:
confirmed_email: You were added as an owner to %{gem} gem
token_expired: The confirmation token has expired. Please try resending the token from the gem page.
@@ -508,6 +574,8 @@ en:
confirmed_at: CONFIRMED AT
added_by: ADDED BY
action: ACTION
+ role: ROLE
+ role_field: Role
email_field: Email / Handle
submit_button: Add Owner
info: add or remove owners
@@ -518,6 +586,9 @@ en:
resent_notice: A confirmation mail has been re-sent to your email
create:
success_notice: "%{handle} was added as an unconfirmed owner. Ownership access will be enabled after the user clicks on the confirmation mail sent to their email."
+ update:
+ update_current_user_role: Can't update your own Role
+ success_notice: "%{handle} was succesfully updated."
destroy:
removed_notice: "%{owner_name} was removed from the owners successfully"
failed_notice: Can't remove the only owner of the gem
@@ -601,6 +672,7 @@ en:
rubygems:
aside:
downloads_for_this_version: For this version
+ gem_version_age: Version Released
required_ruby_version: Required Ruby Version
required_rubygems_version: Required Rubygems Version
requires_mfa: New versions require MFA
@@ -646,6 +718,7 @@ en:
sha_256_checksum: SHA 256 checksum
signature_period: Signature validity period
expired: expired
+ provenance_header: Provenance
version_navigation:
previous_version: ← Previous version
next_version: Next version →
@@ -687,7 +760,7 @@ en:
updated: Updated
yanked: Yanked
show:
- subtitle: for %{query}
+ subtitle_html: for %{query}
month_update: Updated last month (%{count})
week_update: Updated last week (%{count})
filter: "Filter:"
@@ -707,7 +780,6 @@ en:
index:
title: Stats
all_time_most_downloaded: All Time Most Downloaded
- total_downloads: Total downloads
total_gems: Total gems
total_users: Total users
users:
@@ -773,8 +845,6 @@ en:
continue: Continue
title: You have successfully added a security device
notice_html: 'Please copy and paste these recovery codes. You can use these codes to login if you lose your security device. Each code can be used once.'
- copied: "[ copied ]"
- copy: "[ copy ]"
saved: I acknowledge that I have saved my recovery codes.
webauthn_credential:
confirm_delete: "Credential deleted"
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 17af540ada5..ee7e6a0d528 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -4,6 +4,7 @@ es:
copied: "¡Copiado!"
copy_to_clipboard: Copiar al portapapeles
edit: Editar
+ hide: Ocultar
verification_expired:
feed_latest: RubyGems.org | Gemas más recientes
feed_subscribed: RubyGems.org | Suscripciones a gemas
@@ -26,13 +27,18 @@ es:
locale_name: Español
none: Ninguno
not_found: No encontrado
+ forbidden:
+ api_gem_not_found:
api_key_forbidden: La clave API no tiene acceso
+ api_key_soft_deleted:
+ api_key_insufficient_scope:
please_sign_up: Acceso denegado. Por favor regístrate en https://rubygems.org
please_sign_in: Por favor, ingresa en tu cuenta para continuar.
otp_incorrect: Tu código OTP es incorrecto. Por favor verifícalo y prueba nuevamente.
otp_missing: Activaste autenticación con múltiples factores, pero no suministraste
un código OTP. Por favor ingrésalo y prueba nuevamente.
sign_in: Ingresa
+ sign_out: Salir
sign_up: Regístrate
dependency_list: Mostrar toda la cadena de dependencias
multifactor_authentication: Autenticación de múltiples factores
@@ -40,8 +46,10 @@ es:
this_rubygem_could_not_be_found: Esta gema no pudo encontrarse.
time_ago: hace %{duration}
title: RubyGems.org
- update: Actualizar
+ total_downloads: Total de descargas
try_again: Algo salió mal. Por favor inténtalo nuevamente.
+ update: Actualizar
+ view_all: Ver todo
advanced_search: Búsqueda avanzada
authenticate: Autenticar
helpers:
@@ -67,6 +75,14 @@ es:
full_name: Nombre completo
handle: Usuario
password: Contraseña
+ ownership/role:
+ owner:
+ maintainer:
+ membership/role:
+ owner:
+ admin:
+ maintainer:
+ outside_contributor:
api_key:
oidc_api_key_role:
oidc/id_token:
@@ -89,11 +105,26 @@ es:
attributes:
expires_at:
inclusion:
+ organization:
+ attributes:
+ handle:
+ invalid:
ownership:
attributes:
user_id:
already_confirmed: ya es propietario de esta gema
already_invited: ya ha sido invitado a esta gema
+ ownership_request:
+ attributes:
+ user_id:
+ taken:
+ existing:
+ user:
+ attributes:
+ handle:
+ invalid:
+ password:
+ bcrypt_length:
version:
attributes:
gem_full_name:
@@ -167,6 +198,7 @@ es:
clave de API:'
mfa: AMF
expiration:
+ update_owner:
new:
new_api_key: Nueva clave de API
reset:
@@ -191,10 +223,11 @@ es:
migrating_link_text: migrar
mine: Mis Gemas
my_subscriptions: Suscripciones
- no_owned_html: Todavía no publicaste ninguna gema. Prueba a ver las guías sobre
- cómo %{creating_link} o %{migrating_link} una gema en rubygems.org.
+ no_owned_html: Todavía no publicaste ninguna gema. Prueba a ver la guía sobre
+ cómo %{creating_link}.
no_subscriptions_html: Aún no te suscribiste a ninguna gema. ¡Visita la %{gem_link}
para suscribirte!
+ organizations:
title: Dashboard
dependencies:
show:
@@ -225,6 +258,7 @@ es:
blog: Blog
contribute: Contribuye
data: Datos
+ docs: Documentación
designed_by: Diseñado por
discussion_forum: Foro de Discusión
gems_served_by: Distribuida por
@@ -239,6 +273,9 @@ es:
statistics: Estadísticas
status: Estado
supported_by: Con el apoyo de
+ supported_by_html: |
+ RubyGems.org
+ es respaldado por
tested_by: Probado por
tracking_by: Seguimiento con
uptime: Uptime
@@ -246,14 +283,18 @@ es:
secured_by: Protegido por
looking_for_maintainers: Se buscan mantenedores/as
header:
+ profile: Perfil
dashboard: Dashboard
settings: Configuración
- edit_profile: Editar perfil
+ edit_profile: Editar
search_gem_html: Buscar gemas…
- sign_in: Acceso
- sign_out: Salir
- sign_up: Registro
mfa_banner_html:
+ breadcrumbs:
+ home: Inicio
+ dashboard: Dashboard
+ settings: Configuración
+ org_name: 'Org: %{name}'
+ subscriptions: Suscripciones
mailer:
confirm_your_email: Por favor confirma tu dirección de correo con el enlace enviado.
confirmation_subject: Por favor confirma tu dirección de correo con %{host}
@@ -351,6 +392,12 @@ es:
subtitle: "¡Hola %{handle}!"
body_html: Has sido eliminado como propietario/a de la gema %{gem}
en %{host} por %{remover}.
+ owner_updated:
+ subject:
+ title:
+ subtitle:
+ body_html:
+ body_text:
ownerhip_request_closed:
title: CANDIDATURA A PROPIETARIO
subtitle: "¡Hola %{handle}!"
@@ -401,6 +448,8 @@ es:
gem_html: Este webhook se ejecutaba antes cuando se subía %{gem}.
gem_trusted_publisher_added:
title:
+ admin_manual:
+ title:
news:
show:
title: Nuevos lanzamientos — Todas las Gemas
@@ -444,10 +493,6 @@ es:
title: Volcado de datos de RubyGems.org
download:
title: Descargar RubyGems
- faq:
- title: Preguntas Frecuentes
- migrate:
- title: Migrar Gemas
security:
title: Seguridad
sponsors:
@@ -496,12 +541,17 @@ es:
setup_webauthn_html: "\U0001F389 ¡Ahora soportamos dispositivos de seguridad!
Aumenta la seguridad de tu cuenta configurando
un nuevo dispositivo."
+ api:
+ mfa_required:
+ mfa_required_not_yet_enabled:
+ mfa_required_weak_level_enabled:
+ mfa_recommended_not_yet_enabled:
+ mfa_recommended_weak_level_enabled:
recovery:
- copied: "[ copiado ]"
continue: Continuar
title: Códigos de recuperación
- copy: "[ copiar ]"
saved: Declaro haber guardado mis códigos de recuperación.
+ confirm_dialog:
note_html: Por favor copia y guarda
estos códigos de recuperación. Puedes usar estos códigos para acceder y restablecer
tu autenticación de múltiples factores si pierdes tu dispositivo. Cada código
@@ -578,7 +628,13 @@ es:
failed_verification:
title: Error - Falló la verification
close_browser: Por favor cierra este navegador e inténtalo de nuevo.
+ organization_onboardings:
+ confirm:
+ success:
owners:
+ edit:
+ role:
+ title:
confirm:
confirmed_email: Has sido añadido/a como propietario de la gema %{gem}
token_expired: El token de confirmación ha expirado. Por favor intenta reenviar
@@ -591,6 +647,8 @@ es:
confirmed_at: CONFIRMADO
added_by: AÑADIDO POR
action: ACCIÓN
+ role:
+ role_field:
email_field: Correo / Usuario
submit_button: Añadir a propietarios
info: añadir o eliminar propietarios
@@ -603,6 +661,9 @@ es:
success_notice: Se ha añadido a %{handle} como propietario sin confirmar. Su
acceso como propietario se activará cuando haga click en el mensaje de confirmación
que se le ha enviado a su correo
+ update:
+ update_current_user_role:
+ success_notice:
destroy:
removed_notice: "%{owner_name} eliminado con éxito de la lista de propietarios"
failed_notice: No se puede eliminar al único propietario de una gema
@@ -670,7 +731,8 @@ es:
%{unconfirmed_email}
enter_password: Por favor introduce tu contraseña
optional_full_name: Opcional. Será mostrado en tu perfil público
- optional_twitter_username: Usuario de X opcional. Será mostrado en tu perfil público
+ optional_twitter_username: Usuario de X opcional. Será mostrado en tu perfil
+ público
twitter_username: Usuario
title: Editar perfil
delete:
@@ -708,6 +770,7 @@ es:
rubygems:
aside:
downloads_for_this_version: Para esta versión
+ gem_version_age: Versión publicada
required_ruby_version: Versión de Ruby requerida
required_rubygems_version: Versión de Rubygems requerida
requires_mfa: Nuevas versiones requieren AMF
@@ -756,6 +819,7 @@ es:
sha_256_checksum: SHA 256 checksum
signature_period: Periodo de validez de la firma
expired: Expirado
+ provenance_header:
version_navigation:
previous_version: "← Versión anterior"
next_version: Siguiente versión →
@@ -809,7 +873,7 @@ es:
updated: Actualizada
yanked: Borrada
show:
- subtitle: para %{query}
+ subtitle_html: para %{query}
month_update: Actualizadas en el último mes (%{count})
week_update: Actualizadas en la última semana (%{count})
filter: 'Filtro:'
@@ -830,7 +894,6 @@ es:
index:
title: Estadísticas
all_time_most_downloaded: Más descargadas
- total_downloads: Total de descargas
total_gems: Gemas totales
total_users: Usuarios totales
users:
@@ -912,8 +975,6 @@ es:
estos códigos de recuperación. Puedes utilizar estos códigos para acceder
si pierdes tu dispositivo de seguridad. Cada código solo se puede usar una
vez.
- copied: "[ copiado ]"
- copy: "[ copiar ]"
saved: Declaro haber guardado mis códigos de recuperación.
webauthn_credential:
confirm_delete: Credencial borrada
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index a309c89e946..612d974d890 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -4,6 +4,7 @@ fr:
copied: Copié!
copy_to_clipboard: Copier
edit: Modification
+ hide: Cacher
verification_expired:
feed_latest: RubyGems.org | Derniers Gems
feed_subscribed: RubyGems.org | Gems abonnés
@@ -27,12 +28,17 @@ fr:
locale_name: Français
none:
not_found: Introuvable
+ forbidden:
+ api_gem_not_found:
api_key_forbidden:
+ api_key_soft_deleted:
+ api_key_insufficient_scope:
please_sign_up: Accès refusé. Inscrivez-vous sur https://rubygems.org
please_sign_in:
otp_incorrect:
otp_missing:
sign_in: Connexion
+ sign_out: Déconnexion
sign_up: Inscription
dependency_list:
multifactor_authentication: Authentification Multifacteur
@@ -40,8 +46,10 @@ fr:
this_rubygem_could_not_be_found: Rubygem introuvable.
time_ago: il y a %{duration}
title: RubyGems.org
- update: Mise à jour
+ total_downloads: Total de téléchargements
try_again: Une erreur est survenue. Veuillez réessayer.
+ update: Mise à jour
+ view_all: Voir tout
advanced_search: Recherche avancée
authenticate:
helpers:
@@ -67,6 +75,14 @@ fr:
full_name: Nom complet
handle: Pseudonyme
password: Mot de passe
+ ownership/role:
+ owner:
+ maintainer:
+ membership/role:
+ owner:
+ admin:
+ maintainer:
+ outside_contributor:
api_key:
oidc_api_key_role:
oidc/id_token:
@@ -87,11 +103,26 @@ fr:
attributes:
expires_at:
inclusion:
+ organization:
+ attributes:
+ handle:
+ invalid:
ownership:
attributes:
user_id:
already_confirmed:
already_invited:
+ ownership_request:
+ attributes:
+ user_id:
+ taken:
+ existing:
+ user:
+ attributes:
+ handle:
+ invalid:
+ password:
+ bcrypt_length:
version:
attributes:
gem_full_name:
@@ -162,6 +193,7 @@ fr:
save_key:
mfa:
expiration:
+ update_owner:
new:
new_api_key:
reset:
@@ -183,10 +215,11 @@ fr:
migrating_link_text: migration
mine: Mes Gems
my_subscriptions: Abonnements
- no_owned_html: Vous n'avez pas encore publié de gem. Vous pouvez vérifier les
- guides de %{creating_link} ou de %{migrating_link} de gems depuis RubyForge.
+ no_owned_html: Vous n'avez pas encore publié de gem. Vous pouvez vérifier le
+ guide de %{creating_link}.
no_subscriptions_html: Vous n'avez pas encore d'abonnements. Visitez %{gem_link}
pour vous y abonner !
+ organizations:
title: Dashboard
dependencies:
show:
@@ -217,6 +250,7 @@ fr:
blog: Blog
contribute: Contribuer
data: Données
+ docs: Documentation
designed_by: Design par
discussion_forum: Discussions
gems_served_by: Gems mis à disposition par
@@ -231,6 +265,9 @@ fr:
statistics: Stats
status: Statut
supported_by: Soutenu par
+ supported_by_html: |
+ RubyGems.org
+ est soutenu par
tested_by: Testé par
tracking_by: Tracking par
uptime: Uptime
@@ -238,14 +275,18 @@ fr:
secured_by:
looking_for_maintainers:
header:
+ profile: Profil
dashboard: Dashboard
- settings:
- edit_profile:
+ settings: Paramètres
+ edit_profile: Modifier le profil
search_gem_html: Recherche de Gems…
- sign_in: Connexion
- sign_out: Déconnexion
- sign_up: Inscription
mfa_banner_html:
+ breadcrumbs:
+ home: Accueil
+ dashboard: Dashboard
+ settings: Paramètres
+ org_name: 'Org: %{name}'
+ subscriptions: Abonnements
mailer:
confirm_your_email: Veuillez confirmer votre adresse email avec le lien qui vous
a été envoyé par email.
@@ -330,6 +371,12 @@ fr:
title:
subtitle:
body_html:
+ owner_updated:
+ subject:
+ title:
+ subtitle:
+ body_html:
+ body_text:
ownerhip_request_closed:
title:
subtitle:
@@ -366,6 +413,8 @@ fr:
gem_html:
gem_trusted_publisher_added:
title:
+ admin_manual:
+ title:
news:
show:
title: Nouvelles Versions - Toutes les Gems
@@ -409,10 +458,6 @@ fr:
title:
download:
title:
- faq:
- title:
- migrate:
- title:
security:
title:
sponsors:
@@ -447,12 +492,17 @@ fr:
strong_mfa_level_required_html:
strong_mfa_level_recommended:
setup_webauthn_html:
+ api:
+ mfa_required:
+ mfa_required_not_yet_enabled:
+ mfa_required_weak_level_enabled:
+ mfa_recommended_not_yet_enabled:
+ mfa_recommended_weak_level_enabled:
recovery:
- copied:
continue: Continuer
title: Codes de récupération
- copy:
saved:
+ confirm_dialog:
note_html:
already_generated:
update:
@@ -517,7 +567,13 @@ fr:
failed_verification:
title:
close_browser:
+ organization_onboardings:
+ confirm:
+ success:
owners:
+ edit:
+ role:
+ title:
confirm:
confirmed_email:
token_expired:
@@ -529,6 +585,8 @@ fr:
confirmed_at:
added_by:
action:
+ role:
+ role_field:
email_field:
submit_button:
info:
@@ -539,6 +597,9 @@ fr:
resent_notice:
create:
success_notice:
+ update:
+ update_current_user_role:
+ success_notice:
destroy:
removed_notice:
failed_notice:
@@ -635,6 +696,7 @@ fr:
rubygems:
aside:
downloads_for_this_version: Pour cette version
+ gem_version_age: Version publiée
required_ruby_version: Version de Ruby requise
required_rubygems_version:
requires_mfa:
@@ -680,6 +742,7 @@ fr:
sha_256_checksum: Total de contrôle SHA 256
signature_period:
expired:
+ provenance_header:
version_navigation:
previous_version: "← Version précédente"
next_version: Version suivante →
@@ -734,7 +797,7 @@ fr:
updated: Mis à jour
yanked:
show:
- subtitle: pour %{query}
+ subtitle_html: pour %{query}
month_update:
week_update:
filter:
@@ -754,7 +817,6 @@ fr:
index:
title:
all_time_most_downloaded: Gems les plus téléchargés
- total_downloads: Total de téléchargements
total_gems: Total de gems
total_users: Total d'utilisateurs
users:
@@ -820,8 +882,6 @@ fr:
continue:
title:
notice_html:
- copied:
- copy:
saved:
webauthn_credential:
confirm_delete:
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 63d23c96f36..017c949cf28 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -4,6 +4,7 @@ ja:
copied: コピー完了!
copy_to_clipboard: クリップボードにコピー
edit: 編集
+ hide: 非表示
verification_expired: 検証が期限切れです。もう一度検証してください。
feed_latest: RubyGems.org | 最新のgemの一覧
feed_subscribed: RubyGems.org | 購読したgemの一覧
@@ -20,12 +21,17 @@ ja:
locale_name: 日本語
none: なし
not_found: 見つかりませんでした
+ forbidden:
+ api_gem_not_found:
api_key_forbidden: APIキーにアクセス権がありません
+ api_key_soft_deleted:
+ api_key_insufficient_scope:
please_sign_up: アクセスが拒否されました。https://rubygems.org でアカウント登録を行ってください。
please_sign_in: サインインしてお進みください。
otp_incorrect: OTPのコードが正しくありません。ご確認の上再試行してください。
otp_missing: 多要素認証が有効化済みですがOTPのコードが与えられませんでした。入力の上再試行してください。
sign_in: サインイン
+ sign_out: サインアウト
sign_up: 新規登録
dependency_list: 全ての推移的な依存関係を表示
multifactor_authentication: 多要素認証
@@ -33,8 +39,10 @@ ja:
this_rubygem_could_not_be_found: お探しのrubygemは見つかりませんでした。
time_ago: "%{duration}前"
title: RubyGems.org
- update: 更新
+ total_downloads: 累計ダウンロード数
try_again: エラーが発生しました。再試行してください。
+ update: 更新
+ view_all: 全て表示
advanced_search: 高度な検索
authenticate: 認証
helpers:
@@ -60,6 +68,14 @@ ja:
full_name: フルネーム
handle: ユーザー名
password: パスワード
+ ownership/role:
+ owner:
+ maintainer:
+ membership/role:
+ owner:
+ admin:
+ maintainer:
+ outside_contributor:
api_key:
oidc_api_key_role: OIDC APIキーのロール
oidc/id_token:
@@ -80,11 +96,26 @@ ja:
attributes:
expires_at:
inclusion: 未来でなければなりません
+ organization:
+ attributes:
+ handle:
+ invalid:
ownership:
attributes:
user_id:
already_confirmed: は既にこのgemの所有者です
already_invited: は既にこのgemに招待されています
+ ownership_request:
+ attributes:
+ user_id:
+ taken:
+ existing:
+ user:
+ attributes:
+ handle:
+ invalid:
+ password:
+ bcrypt_length:
version:
attributes:
gem_full_name:
@@ -155,6 +186,7 @@ ja:
save_key: キーを二度と表示できなくなるためご注意ください。新しいAPIキー:
mfa: MFA
expiration: 期限
+ update_owner:
new:
new_api_key: 新しいAPIキー
reset:
@@ -176,8 +208,9 @@ ja:
migrating_link_text: 移行
mine: 自分のgem
my_subscriptions: 購読
- no_owned_html: まだgemを公開していません。よろしければgemの%{creating_link}またはRubyForgeからの%{migrating_link}ガイドをご覧ください。
+ no_owned_html: まだgemを公開していません。よろしければgemの%{creating_link}ガイドをご覧ください。
no_subscriptions_html: まだgemを購読していません。%{gem_link}から何か購読してみてください。
+ organizations:
title: ダッシュボード
dependencies:
show:
@@ -206,6 +239,7 @@ ja:
blog: ブログ
contribute: 貢献
data: データ
+ docs: ドキュメント
designed_by: 設計
discussion_forum: 議論
gems_served_by: gemの提供
@@ -220,6 +254,9 @@ ja:
statistics: 統計
status: 状態
supported_by: 協賛
+ supported_by_html: |
+ RubyGems.org
+ は以下から支援を受けています
tested_by: テスト
tracking_by: 追跡
uptime: 稼働時間
@@ -227,15 +264,19 @@ ja:
secured_by: セキュリティ
looking_for_maintainers: メンテナの募集
header:
+ profile: プロフィール
dashboard: ダッシュボード
settings: 設定
edit_profile: プロフィールを編集
search_gem_html: gemを検索...
- sign_in: サインイン
- sign_out: サインアウト
- sign_up: 新規登録
mfa_banner_html: "\U0001F389 セキュリティ機器に対応しました!新しい機器を設定してアカウントのセキュリティを向上しましょう。[詳細はこちら](link
to blog post)!"
+ breadcrumbs:
+ home: ホーム
+ dashboard: ダッシュボード
+ settings: 設定
+ org_name: 組織:%{name}
+ subscriptions: 購読
mailer:
confirm_your_email: Eメールに送信されたリンクからEメールアドレスを確認してください。
confirmation_subject: "%{host}に登録されたメールアドレスを確認してください"
@@ -318,6 +359,12 @@ ja:
subtitle: こんにちは、%{user_handle}!
body_html: あなたは%{remover}によって%{host}上の%{gem}
gemの所有者から削除されました。
+ owner_updated:
+ subject:
+ title:
+ subtitle:
+ body_html:
+ body_text:
ownerhip_request_closed:
title: 所有権の申請
subtitle: こんにちは、%{handle}!
@@ -360,6 +407,8 @@ ja:
gem_html: このwebhookは以前%{gem}がプッシュされたときに呼ばれました。
gem_trusted_publisher_added:
title: 信頼できる発行元が追加されました
+ admin_manual:
+ title:
news:
show:
title: 新しいリリース - 全てのgem
@@ -389,10 +438,6 @@ ja:
title: RubyGems.orgのデータのダンプ
download:
title: RubyGemsをダウンロード
- faq:
- title: FAQ
- migrate:
- title: gemを移行する
security:
title: セキュリティ
sponsors:
@@ -428,12 +473,17 @@ ja:
strong_mfa_level_recommended: アカウントとgemの保護のため、MFAの水準を「UIとgemのサインイン」または「UIとAPI」に変更することをお勧めします。将来のアカウントはこれらの水準のどちらか1つにMFAが有効になっていることが必須になります。
setup_webauthn_html: "\U0001F389 セキュリティ機器に対応しました!新しい機器を設定してアカウントのセキュリティを向上しましょう。詳細はこちら!"
+ api:
+ mfa_required:
+ mfa_required_not_yet_enabled:
+ mfa_required_weak_level_enabled:
+ mfa_recommended_not_yet_enabled:
+ mfa_recommended_weak_level_enabled:
recovery:
- copied: "[ コピーしました ]"
continue: 続ける
title: 復旧コード
- copy: "[ コピーする ]"
saved: 復旧コードを保存したことを確認しました。
+ confirm_dialog:
note_html: これらの復旧コードをコピー及び保存してください。認証機器を紛失した場合にこれらのコードを使ってログインしMFAをリセットできます。各コードは1回使えます。
already_generated: 既に復旧コードを保存したはずです。
update:
@@ -497,7 +547,13 @@ ja:
failed_verification:
title: エラー - 検証が失敗しました
close_browser: このブラウザを閉じて再試行してください。
+ organization_onboardings:
+ confirm:
+ success:
owners:
+ edit:
+ role:
+ title:
confirm:
confirmed_email: "%{gem} gemの所有者として追加されました"
token_expired: 確認トークンが期限切れです。gemのページからトークンを再送してみてください。
@@ -509,6 +565,8 @@ ja:
confirmed_at: 確定日時
added_by: 追加者
action: 操作
+ role:
+ role_field:
email_field: Eメール / ハンドル名
submit_button: 所有者を追加
info: 所有者を追加または削除する
@@ -519,6 +577,9 @@ ja:
resent_notice: 確定メールがEメールに再送されました。
create:
success_notice: "%{handle}が未確定の所有者として追加されました。当該ユーザーがEメールに送られた確定メールでクリックした後に所有権アクセスが有効になります。"
+ update:
+ update_current_user_role:
+ success_notice:
destroy:
removed_notice: "%{owner_name}は所有者から正常に削除されました"
failed_notice: gemの唯一の所有者は削除できません。
@@ -604,6 +665,7 @@ ja:
rubygems:
aside:
downloads_for_this_version: このバージョンのみ
+ gem_version_age: このバージョンがリリースされたのは
required_ruby_version: 必要なRubyのバージョン
required_rubygems_version: 必要なRubyGemsのバージョン
requires_mfa: 新しいバージョンはMFAを必要とします
@@ -650,6 +712,7 @@ ja:
sha_256_checksum: SHA 256チェックサム
signature_period: シグネチャの有効期限
expired: 期限切れ
+ provenance_header:
version_navigation:
previous_version: "←前のバージョン"
next_version: 次のバージョン→
@@ -695,7 +758,7 @@ ja:
updated: 更新日
yanked: ヤンク済み
show:
- subtitle: "%{query}の検索結果"
+ subtitle_html: "%{query}の検索結果"
month_update: 先月更新(%{count}件)
week_update: 先週更新(%{count}件)
filter: 絞り込み:
@@ -715,7 +778,6 @@ ja:
index:
title: 統計
all_time_most_downloaded: 累計最多ダウンロード数
- total_downloads: 累計ダウンロード数
total_gems: gem総数
total_users: ユーザー総数
users:
@@ -782,8 +844,6 @@ ja:
continue: 続ける
title: セキュリティ機器を正常に追加しました。
notice_html: これらの復旧コードをコピー&ペーストしてください。セキュリティ機器を紛失した場合にこれらのコードを使ってログインできます。各コードは1度使えます。
- copied: "[ コピーしました ]"
- copy: "[ コピーする ]"
saved: 復旧コードを保存したことを確認しました。
webauthn_credential:
confirm_delete: 認証情報が削除されました
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 269bb17ba8a..e290c8869b8 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -1,9 +1,10 @@
---
nl:
credentials_required:
- copied:
- copy_to_clipboard:
+ copied: Gekopieerd
+ copy_to_clipboard: Kopieer naar klembord
edit: Wijzig
+ hide: Verberg
verification_expired:
feed_latest: RubyGems.org | Nieuwste Gems
feed_subscribed: RubyGems.org | Geabonneerde Gems
@@ -19,12 +20,17 @@ nl:
locale_name: Nederlands
none: Geen
not_found: Niet gevonden
+ forbidden:
+ api_gem_not_found:
api_key_forbidden:
+ api_key_soft_deleted:
+ api_key_insufficient_scope:
please_sign_up: Toegang geweigerd. Schrijf je in voor een account op https://rubygems.org
please_sign_in:
otp_incorrect:
otp_missing:
sign_in: Inloggen
+ sign_out: Uitloggen
sign_up: Registreren
dependency_list:
multifactor_authentication: Multifactor authenticatie
@@ -32,8 +38,10 @@ nl:
this_rubygem_could_not_be_found: De gem kon niet gevonden worden.
time_ago: "%{duration} geleden"
title: RubyGems.org
- update: Wijzig
+ total_downloads:
try_again: Er is iets fout gegaan, probeer het opnieuw.
+ update: Wijzig
+ view_all: Bekijk alles
advanced_search: Uitgebreid zoeken
authenticate:
helpers:
@@ -59,6 +67,14 @@ nl:
full_name: Voor-en achternaam
handle: Gebruikersnaam
password: Wachtwoord
+ ownership/role:
+ owner:
+ maintainer:
+ membership/role:
+ owner:
+ admin:
+ maintainer:
+ outside_contributor:
api_key:
oidc_api_key_role:
oidc/id_token:
@@ -79,11 +95,26 @@ nl:
attributes:
expires_at:
inclusion:
+ organization:
+ attributes:
+ handle:
+ invalid:
ownership:
attributes:
user_id:
already_confirmed:
already_invited:
+ ownership_request:
+ attributes:
+ user_id:
+ taken:
+ existing:
+ user:
+ attributes:
+ handle:
+ invalid:
+ password:
+ bcrypt_length:
version:
attributes:
gem_full_name:
@@ -154,6 +185,7 @@ nl:
save_key:
mfa:
expiration:
+ update_owner:
new:
new_api_key:
reset:
@@ -176,10 +208,10 @@ nl:
mine: Mijn Gems
my_subscriptions: Mijn abonnementen
no_owned_html: Je hebt nog geen gems gepubliceerd. Bekijk de instructies voor
- het %{creating_link} van een gem of het %{migrating_link} van een gem vanaf
- RubyForge.
+ het %{creating_link} van een gem.
no_subscriptions_html: Je bent nog niet geabonneerd op een of meerdere gems.
Bezoek een %{gem_link} en abonneer je erop.
+ organizations:
title: Dashboard
dependencies:
show:
@@ -209,6 +241,7 @@ nl:
blog: Blog
contribute: Bijdragen
data: Data
+ docs: Documentatie
designed_by: Ontwerp
discussion_forum: Forum
gems_served_by:
@@ -223,6 +256,9 @@ nl:
statistics: Statistieken
status: Status
supported_by: Ondersteuning
+ supported_by_html: |
+ RubyGems.org
+ wordt ondersteund door
tested_by: Getest door
tracking_by: Tracking
uptime: Uptime
@@ -230,14 +266,18 @@ nl:
secured_by:
looking_for_maintainers:
header:
+ profile: Profiel
dashboard: Dashboard
- settings:
- edit_profile:
+ settings: Instellingen
+ edit_profile: Wijzig profiel
search_gem_html: Gems Zoeken…
- sign_in: Inloggen
- sign_out: Uitloggen
- sign_up: Registreren
mfa_banner_html:
+ breadcrumbs:
+ home: Startpagina
+ dashboard: Dashboard
+ settings: Instellingen
+ org_name: 'Organisatie: %{name}'
+ subscriptions: Abonnementen
mailer:
confirm_your_email:
confirmation_subject:
@@ -315,6 +355,12 @@ nl:
title:
subtitle:
body_html:
+ owner_updated:
+ subject:
+ title:
+ subtitle:
+ body_html:
+ body_text:
ownerhip_request_closed:
title:
subtitle:
@@ -351,6 +397,8 @@ nl:
gem_html:
gem_trusted_publisher_added:
title:
+ admin_manual:
+ title:
news:
show:
title:
@@ -395,10 +443,6 @@ nl:
title:
download:
title:
- faq:
- title:
- migrate:
- title:
security:
title:
sponsors:
@@ -432,12 +476,17 @@ nl:
strong_mfa_level_required_html:
strong_mfa_level_recommended:
setup_webauthn_html:
+ api:
+ mfa_required:
+ mfa_required_not_yet_enabled:
+ mfa_required_weak_level_enabled:
+ mfa_recommended_not_yet_enabled:
+ mfa_recommended_weak_level_enabled:
recovery:
- copied:
continue:
title:
- copy:
saved:
+ confirm_dialog:
note_html:
already_generated:
update:
@@ -498,7 +547,13 @@ nl:
failed_verification:
title:
close_browser:
+ organization_onboardings:
+ confirm:
+ success:
owners:
+ edit:
+ role:
+ title:
confirm:
confirmed_email:
token_expired:
@@ -510,6 +565,8 @@ nl:
confirmed_at:
added_by:
action:
+ role:
+ role_field:
email_field:
submit_button:
info:
@@ -520,6 +577,9 @@ nl:
resent_notice:
create:
success_notice:
+ update:
+ update_current_user_role:
+ success_notice:
destroy:
removed_notice:
failed_notice:
@@ -603,6 +663,7 @@ nl:
rubygems:
aside:
downloads_for_this_version: Voor deze versie
+ gem_version_age: Versie vrijgegeven
required_ruby_version: Required Ruby Version
required_rubygems_version: Required Rubygems Version
requires_mfa:
@@ -648,6 +709,7 @@ nl:
sha_256_checksum: SHA 256 checksum
signature_period:
expired:
+ provenance_header:
version_navigation:
previous_version:
next_version:
@@ -689,7 +751,7 @@ nl:
updated:
yanked:
show:
- subtitle: voor %{query}
+ subtitle_html: voor %{query}
month_update:
week_update:
filter:
@@ -709,7 +771,6 @@ nl:
index:
title:
all_time_most_downloaded:
- total_downloads:
total_gems:
total_users:
users:
@@ -775,8 +836,6 @@ nl:
continue:
title:
notice_html:
- copied:
- copy:
saved:
webauthn_credential:
confirm_delete:
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 57d2269b7f7..ce8a03c68ae 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -1,9 +1,10 @@
---
pt-BR:
credentials_required:
- copied:
- copy_to_clipboard:
+ copied: Copiado
+ copy_to_clipboard: Copiar
edit: Editar
+ hide: Esconder
verification_expired:
feed_latest: RubyGems.org | Últimas Gems
feed_subscribed: RubyGems.org | Gems do seu Feed
@@ -25,13 +26,18 @@ pt-BR:
locale_name: Português do Brasil
none: Nenhum
not_found: Não encontrado
+ forbidden:
+ api_gem_not_found:
api_key_forbidden:
+ api_key_soft_deleted:
+ api_key_insufficient_scope:
please_sign_up: Acesso Negado. Por favor se registre em https://rubygems.org
please_sign_in: Por favor, faça login em sua conta para continuar.
otp_incorrect: Seu código OTP está incorreto. Por favor, verifique-o e tente novamente.
otp_missing: Você habilitou a autenticação multifator mas nenhum código OTP foi
fornecido. Por favor, preencha-o e tente novamente.
sign_in: Fazer Login
+ sign_out: Sair
sign_up: Cadastrar
dependency_list: Mostrar todas as dependências
multifactor_authentication: Autenticação Multifator
@@ -39,8 +45,10 @@ pt-BR:
this_rubygem_could_not_be_found: Não foi possível localizar esta rubygem
time_ago: "%{duration} atrás"
title: RubyGems.org
- update: Atualizar
+ total_downloads: Total de downloads
try_again: Algo deu errado. Por favor, tente novamente.
+ update: Atualizar
+ view_all: Ver tudo
advanced_search: Busca avançada
authenticate:
helpers:
@@ -66,6 +74,14 @@ pt-BR:
full_name: Nome completo
handle: Usuário
password: Senha
+ ownership/role:
+ owner:
+ maintainer:
+ membership/role:
+ owner:
+ admin:
+ maintainer:
+ outside_contributor:
api_key:
oidc_api_key_role:
oidc/id_token:
@@ -86,11 +102,26 @@ pt-BR:
attributes:
expires_at:
inclusion:
+ organization:
+ attributes:
+ handle:
+ invalid:
ownership:
attributes:
user_id:
already_confirmed:
already_invited:
+ ownership_request:
+ attributes:
+ user_id:
+ taken:
+ existing:
+ user:
+ attributes:
+ handle:
+ invalid:
+ password:
+ bcrypt_length:
version:
attributes:
gem_full_name:
@@ -161,6 +192,7 @@ pt-BR:
save_key:
mfa:
expiration:
+ update_owner:
new:
new_api_key:
reset:
@@ -183,9 +215,10 @@ pt-BR:
mine: Minhas Gems
my_subscriptions: Observando
no_owned_html: Você ainda não enviou nenhuma gem. Verifique os guias sobre %{creating_link}
- uma gem ou %{migrating_link} uma gem do RubyForge.
+ uma gem.
no_subscriptions_html: Você ainda não está observando nenhuma gem. Visite uma
%{gem_link} para poder observá-la!
+ organizations:
title: Painel de Controle
dependencies:
show:
@@ -209,11 +242,12 @@ pt-BR:
layouts:
application:
footer:
- about: Sobre o Rubygems
+ about: Sobre
api: API
blog: Blog
contribute: Como Contribuir
data: Dump de Dados
+ docs: Documentação
designed_by: Design
discussion_forum: Fóruns
gems_served_by: Provisionamento
@@ -228,6 +262,9 @@ pt-BR:
statistics: Estatísticas
status: Status
supported_by: Patrocínio
+ supported_by_html: |
+ RubyGems.org
+ é apoiado por
tested_by: Testado por
tracking_by: Coleta de Dados
uptime: Uptime
@@ -235,14 +272,18 @@ pt-BR:
secured_by:
looking_for_maintainers:
header:
+ profile: Perfil
dashboard: Painel de Controle
settings: Configurações da Conta
edit_profile: Editar Perfil
search_gem_html: Buscar Gems…
- sign_in: Fazer Login
- sign_out: Sair
- sign_up: Cadastrar
mfa_banner_html:
+ breadcrumbs:
+ home: Início
+ dashboard: Painel de Controle
+ settings: Configurações da Conta
+ org_name: 'Org: %{name}'
+ subscriptions: Observando
mailer:
confirm_your_email: Por favor, confirme seu endereço de email através do link
que enviamos para o seu endereço de email.
@@ -327,6 +368,12 @@ pt-BR:
title:
subtitle:
body_html:
+ owner_updated:
+ subject:
+ title:
+ subtitle:
+ body_html:
+ body_text:
ownerhip_request_closed:
title:
subtitle:
@@ -363,6 +410,8 @@ pt-BR:
gem_html:
gem_trusted_publisher_added:
title:
+ admin_manual:
+ title:
news:
show:
title: Novos Releases - Todas as Gems
@@ -406,10 +455,6 @@ pt-BR:
title:
download:
title:
- faq:
- title:
- migrate:
- title:
security:
title:
sponsors:
@@ -443,12 +488,17 @@ pt-BR:
strong_mfa_level_required_html:
strong_mfa_level_recommended:
setup_webauthn_html:
+ api:
+ mfa_required:
+ mfa_required_not_yet_enabled:
+ mfa_required_weak_level_enabled:
+ mfa_recommended_not_yet_enabled:
+ mfa_recommended_weak_level_enabled:
recovery:
- copied:
continue:
title:
- copy:
saved:
+ confirm_dialog:
note_html:
already_generated:
update:
@@ -509,7 +559,13 @@ pt-BR:
failed_verification:
title:
close_browser:
+ organization_onboardings:
+ confirm:
+ success:
owners:
+ edit:
+ role:
+ title:
confirm:
confirmed_email:
token_expired:
@@ -521,6 +577,8 @@ pt-BR:
confirmed_at:
added_by:
action:
+ role:
+ role_field:
email_field:
submit_button:
info:
@@ -531,6 +589,9 @@ pt-BR:
resent_notice:
create:
success_notice:
+ update:
+ update_current_user_role:
+ success_notice:
destroy:
removed_notice:
failed_notice:
@@ -614,6 +675,7 @@ pt-BR:
rubygems:
aside:
downloads_for_this_version: Desta versão
+ gem_version_age: Versão lançada
required_ruby_version: Versão Requerida do Ruby
required_rubygems_version: Versão Requerida do RubyGems
requires_mfa:
@@ -661,6 +723,7 @@ pt-BR:
sha_256_checksum: SHA 256 checksum
signature_period:
expired:
+ provenance_header:
version_navigation:
previous_version:
next_version:
@@ -712,7 +775,7 @@ pt-BR:
updated:
yanked:
show:
- subtitle: para %{query}
+ subtitle_html: para %{query}
month_update:
week_update:
filter:
@@ -732,7 +795,6 @@ pt-BR:
index:
title: Estatísticas
all_time_most_downloaded: Mais baixadas de todos os tempos
- total_downloads: Total de downloads
total_gems: Total de gems
total_users: Total de usuários
users:
@@ -798,8 +860,6 @@ pt-BR:
continue:
title:
notice_html:
- copied:
- copy:
saved:
webauthn_credential:
confirm_delete:
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index e52330c68de..0e707dafd0c 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -4,6 +4,7 @@ zh-CN:
copied: 已复制!
copy_to_clipboard: 复制到剪贴板
edit: 编辑
+ hide: 隐藏
verification_expired:
feed_latest: RubyGems.org | 最新的 Gem
feed_subscribed: RubyGems.org | 订阅的 Gem
@@ -21,12 +22,17 @@ zh-CN:
locale_name: 简体中文
none: 无
not_found: 未找到
+ forbidden:
+ api_gem_not_found:
api_key_forbidden: API 密钥没有访问权限
+ api_key_soft_deleted:
+ api_key_insufficient_scope:
please_sign_up: 拒绝访问。请在 https://rubygems.org 上注册一个账号。
please_sign_in: 请先登录以继续
otp_incorrect: 您的 OTP 码不正确。请检查后重试。
otp_missing: 您已启用多因素验证,但是没有提供 OTP 码。请输入后重试。
sign_in: 登录
+ sign_out: 退出
sign_up: 注册
dependency_list: 显示所有传递性依赖
multifactor_authentication: 多因素验证
@@ -34,8 +40,10 @@ zh-CN:
this_rubygem_could_not_be_found: 未找到这个 Gem
time_ago: "%{duration} 前"
title: RubyGems.org
- update: 更新
+ total_downloads: 下载总量
try_again: 出了点儿问题。请重试。
+ update: 更新
+ view_all: 查看全部
advanced_search: 高级搜索
authenticate: 身份认证
helpers:
@@ -61,6 +69,14 @@ zh-CN:
full_name: 全名
handle: 用户名
password: 密码
+ ownership/role:
+ owner:
+ maintainer:
+ membership/role:
+ owner:
+ admin:
+ maintainer:
+ outside_contributor:
api_key:
oidc_api_key_role:
oidc/id_token:
@@ -81,11 +97,26 @@ zh-CN:
attributes:
expires_at:
inclusion:
+ organization:
+ attributes:
+ handle:
+ invalid:
ownership:
attributes:
user_id:
already_confirmed:
already_invited:
+ ownership_request:
+ attributes:
+ user_id:
+ taken:
+ existing:
+ user:
+ attributes:
+ handle:
+ invalid:
+ password:
+ bcrypt_length:
version:
attributes:
gem_full_name:
@@ -156,6 +187,7 @@ zh-CN:
save_key: 请注意在此之后我们不会再次向您显示该密钥。新的 API 密钥为:
mfa: 多因素验证
expiration:
+ update_owner:
new:
new_api_key: 生成新 API 密钥
reset:
@@ -177,9 +209,9 @@ zh-CN:
migrating_link_text: 迁移
mine: 我的 Gem
my_subscriptions: 订阅
- no_owned_html: 您还没有发布过任何 Gem。可以看看 %{creating_link} 指南,或从 RubyForge %{migrating_link}
- 一个 Gem。
+ no_owned_html: 您还没有发布过任何 Gem。可以看看 %{creating_link} 指南。
no_subscriptions_html: 您没有订阅任何 Gem,访问 %{gem_link} 订阅一个!
+ organizations:
title: 仪表盘
dependencies:
show:
@@ -208,6 +240,7 @@ zh-CN:
blog: 博客
contribute: 贡献
data: 数据
+ docs: 文档
designed_by: 设计
discussion_forum: 讨论
gems_served_by: 服务
@@ -222,6 +255,9 @@ zh-CN:
statistics: 统计
status: 状态
supported_by: 支持
+ supported_by_html: |
+ RubyGems.org
+ 由以下支持
tested_by: 测试
tracking_by: 追踪
uptime: 服务运行时间
@@ -229,14 +265,18 @@ zh-CN:
secured_by: 安全保护
looking_for_maintainers: 招募维护者
header:
+ profile: 个人信息
dashboard: 仪表盘
settings: 设置
edit_profile: 编辑个人信息
search_gem_html: 搜索 Gem ……
- sign_in: 登录
- sign_out: 退出
- sign_up: 注册
mfa_banner_html:
+ breadcrumbs:
+ home: 首页
+ dashboard: 仪表盘
+ settings: 设置
+ org_name: 组织:%{name}
+ subscriptions: 订阅
mailer:
confirm_your_email: 请在发送到您的邮件中点击链接,确认您的邮箱地址。
confirmation_subject: 请确认您的邮箱地址
@@ -320,6 +360,12 @@ zh-CN:
subtitle: 你好啊,%{user_handle}!
body_html: 您在 RubyGems.org 上对 Gem %{gem}
的业主身份已被 %{remover} 移除
+ owner_updated:
+ subject:
+ title:
+ subtitle:
+ body_html:
+ body_text:
ownerhip_request_closed:
title: 所有权申请
subtitle: 你好啊,%{hand}!
@@ -365,6 +411,8 @@ zh-CN:
被推送时被调用。
gem_trusted_publisher_added:
title:
+ admin_manual:
+ title:
news:
show:
title: 新的发布 — 所有 Gem
@@ -397,10 +445,6 @@ zh-CN:
title: RubyGems.org 数据转储
download:
title: 下载 RubyGems
- faq:
- title: 常问问题
- migrate:
- title: 迁移 Gem
security:
title: 安全
sponsors:
@@ -436,12 +480,17 @@ zh-CN:
strong_mfa_level_recommended: 为了保护您的帐户和您的 Gem,我们建议您将您的多因素验证级别更改为 "UI and gem signin"
或 "UI and API"。在将来,您的帐户将被要求在其中某一个级别上启用多因素验证。
setup_webauthn_html:
+ api:
+ mfa_required:
+ mfa_required_not_yet_enabled:
+ mfa_required_weak_level_enabled:
+ mfa_recommended_not_yet_enabled:
+ mfa_recommended_weak_level_enabled:
recovery:
- copied: "[ 已复制 ]"
continue: 继续
title: 恢复码
- copy: "[ 复制 ]"
saved: 我声明我已经保存了我的恢复码。
+ confirm_dialog:
note_html: 请 复制并保存 这些恢复码。如果您丢失了身份验证设备,您可以使用这些恢复码登录并重置您的多因素验证配置。每个恢复码只能使用一次。
already_generated: 您应该已经保存了您的恢复码。
update:
@@ -503,7 +552,13 @@ zh-CN:
failed_verification:
title: 错误 — 验证失败
close_browser: 请关闭该浏览器并重试。
+ organization_onboardings:
+ confirm:
+ success:
owners:
+ edit:
+ role:
+ title:
confirm:
confirmed_email: 您已被添加为 Gem %{gem} 的业主之一
token_expired: 确认令牌已过期。请尝试从该 Gem 页面重新发送令牌。
@@ -515,6 +570,8 @@ zh-CN:
confirmed_at: 批准于
added_by: 添加
action: 行为
+ role:
+ role_field:
email_field: Email / Handle
submit_button: 添加业主
info: 添加或移除业主
@@ -525,6 +582,9 @@ zh-CN:
resent_notice: 一封确认邮件已被重新发送到您的邮箱中
create:
success_notice: "%{handle} 已添加为还未经批准的业主。在用户点击发送到其邮箱的批准邮件后,所有权访问才将被启用。"
+ update:
+ update_current_user_role:
+ success_notice:
destroy:
removed_notice: "%{owner_name} 已成功从业主中移除"
failed_notice: 不能删除该 Gem 的唯一业主
@@ -611,6 +671,7 @@ zh-CN:
rubygems:
aside:
downloads_for_this_version: 这个版本
+ gem_version_age: 版本发布
required_ruby_version: 需要的 Ruby 版本
required_rubygems_version: 需要的 RubyGems 版本
requires_mfa: 新的版本需要开启多因素验证
@@ -656,6 +717,7 @@ zh-CN:
sha_256_checksum: SHA 256 校验和
signature_period: 签名有效期
expired: 过期
+ provenance_header:
version_navigation:
previous_version: "← 以前的版本"
next_version: 接下来的版本 →
@@ -701,7 +763,7 @@ zh-CN:
updated: 更新
yanked: 撤回
show:
- subtitle: "%{query}"
+ subtitle_html: "%{query}"
month_update: 上个月更新 (%{count})
week_update: 上周更新 (%{count})
filter: 过滤:
@@ -721,7 +783,6 @@ zh-CN:
index:
title: 统计数据
all_time_most_downloaded: 至今最多下载
- total_downloads: 下载总量
total_gems: Gem 总数
total_users: 用户总数
users:
@@ -789,8 +850,6 @@ zh-CN:
continue: 继续
title: 您已成功添加一个安全设备
notice_html: 请 复制并粘贴这些恢复码。如果您丢失了安全设备,您可以使用这些恢复码登录。每个恢复码码只能使用一次。
- copied: "[ 已复制 ]"
- copy: "[ 复制 ]"
saved: 我确认我已经保存了我的恢复码。
webauthn_credential:
confirm_delete: 凭证已删除
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 0ce6860de28..c0962e266f1 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -4,6 +4,7 @@ zh-TW:
copied: 已複製
copy_to_clipboard: 複製
edit: 編輯
+ hide: 隱藏
verification_expired:
feed_latest: RubyGems.org | 最新 Gems
feed_subscribed: RubyGems.org | 訂閱 Gems
@@ -20,12 +21,17 @@ zh-TW:
locale_name: 正體中文
none: 無
not_found: 沒有找到
+ forbidden:
+ api_gem_not_found:
api_key_forbidden: API 金鑰沒有存取權限
+ api_key_soft_deleted:
+ api_key_insufficient_scope:
please_sign_up: 存取遭拒。請先在 https://rubygems.org 上註冊帳號
please_sign_in: 請登入以繼續。
otp_incorrect: 您的 OTP 碼不正確。請檢查後再試一次。
otp_missing: 您已啟用多重要素驗證,但是沒有輸入 OTP 碼。請輸入後再試一次。
sign_in: 登入
+ sign_out: 登出
sign_up: 註冊
dependency_list:
multifactor_authentication: 多重要素驗證
@@ -33,8 +39,10 @@ zh-TW:
this_rubygem_could_not_be_found: 找不到此 rubygem。
time_ago: "%{duration} 前"
title: RubyGems.org
- update: 更新
+ total_downloads: 總下載次數
try_again: 發生錯誤。請再試一次。
+ update: 更新
+ view_all: 查看全部
advanced_search: 進階搜尋
authenticate: 驗證
helpers:
@@ -60,6 +68,14 @@ zh-TW:
full_name: 全名
handle: 帳號
password: 密碼
+ ownership/role:
+ owner:
+ maintainer:
+ membership/role:
+ owner:
+ admin:
+ maintainer:
+ outside_contributor:
api_key:
oidc_api_key_role: OIDC API 金鑰角色
oidc/id_token:
@@ -80,11 +96,26 @@ zh-TW:
attributes:
expires_at:
inclusion:
+ organization:
+ attributes:
+ handle:
+ invalid:
ownership:
attributes:
user_id:
already_confirmed: 已是此 Gem 的擁有者
already_invited: 已獲邀加入此 Gem
+ ownership_request:
+ attributes:
+ user_id:
+ taken:
+ existing:
+ user:
+ attributes:
+ handle:
+ invalid:
+ password:
+ bcrypt_length:
version:
attributes:
gem_full_name:
@@ -155,6 +186,7 @@ zh-TW:
save_key: 請注意,我們無法再次顯示您的 API 金鑰。新 API 金鑰:
mfa: MFA
expiration:
+ update_owner:
new:
new_api_key: 新 API 金鑰
reset:
@@ -176,9 +208,9 @@ zh-TW:
migrating_link_text: Gem 轉移
mine: 我的 Gems
my_subscriptions: 我的訂閱
- no_owned_html: 您尚未發佈任何 Gem。可以閱讀 %{creating_link} 教學,或參考 %{migrating_link} 教學來將您的
- Gem 從 RubyForge 遷移過來。
+ no_owned_html: 您尚未發佈任何 Gem。可以閱讀 %{creating_link} 教學。
no_subscriptions_html: 您還沒有訂閱過 Gem,前往 %{gem_link} 來訂閱!
+ organizations:
title: 控制台
dependencies:
show:
@@ -207,6 +239,7 @@ zh-TW:
blog: 部落格
contribute: 貢獻
data: 資料
+ docs: 文件
designed_by: 設計
discussion_forum: 討論群組
gems_served_by: 服務
@@ -221,6 +254,9 @@ zh-TW:
statistics: 統計
status: 狀態
supported_by: 支持
+ supported_by_html: |
+ RubyGems.org
+ 由以下支持
tested_by: 測試
tracking_by: 追蹤
uptime: 上線時間
@@ -228,14 +264,18 @@ zh-TW:
secured_by:
looking_for_maintainers: 徵求維護者
header:
+ profile: 個人檔案
dashboard: 儀表板
settings: 設定
edit_profile: 編輯個人檔案
search_gem_html: 搜尋 Gems…
- sign_in: 登入
- sign_out: 登出
- sign_up: 註冊
mfa_banner_html: "\U0001F389 我們現在支援安全裝置了!設定新的裝置來提升您的帳號安全。[了解詳情](部落格文章連結)!"
+ breadcrumbs:
+ home: 首頁
+ dashboard: 控制台
+ settings: 設定
+ org_name: 組織:%{name}
+ subscriptions: 訂閱
mailer:
confirm_your_email: 已寄送連結,請點擊連結來確認您的電子郵件地址。
confirmation_subject: "%{host} 電子郵件地址確認"
@@ -315,6 +355,12 @@ zh-TW:
title:
subtitle: 嗨 %{user_handle}!
body_html:
+ owner_updated:
+ subject:
+ title:
+ subtitle:
+ body_html:
+ body_text:
ownerhip_request_closed:
title: 所有權請求
subtitle: 嗨 %{handle}!
@@ -357,6 +403,8 @@ zh-TW:
被推送的時候被呼叫。
gem_trusted_publisher_added:
title:
+ admin_manual:
+ title:
news:
show:
title: 最新發佈
@@ -391,10 +439,6 @@ zh-TW:
title:
download:
title: 下載 RubyGems
- faq:
- title: 常見問題
- migrate:
- title: 轉移 Gems
security:
title: 安全性
sponsors:
@@ -431,12 +475,17 @@ zh-TW:
或 "使用者介面和 API"。未來我們將強制要求所有帳號將設為上述 MFA 等級。
setup_webauthn_html: "\U0001F389 我們現在支援安全裝置了!設定新的裝置來提升您的帳號安全。了解詳情!"
+ api:
+ mfa_required:
+ mfa_required_not_yet_enabled:
+ mfa_required_weak_level_enabled:
+ mfa_recommended_not_yet_enabled:
+ mfa_recommended_weak_level_enabled:
recovery:
- copied: "[ 已複製 ]"
continue: 繼續
title: 復原碼
- copy: "[ 複製 ]"
saved:
+ confirm_dialog:
note_html:
already_generated:
update:
@@ -498,7 +547,13 @@ zh-TW:
failed_verification:
title: 錯誤 - 驗證失敗
close_browser: 請關閉此瀏覽器並重試。
+ organization_onboardings:
+ confirm:
+ success:
owners:
+ edit:
+ role:
+ title:
confirm:
confirmed_email:
token_expired: 確認權杖已過期。請從 Gem 頁面嘗試重新傳送權杖。
@@ -510,6 +565,8 @@ zh-TW:
confirmed_at: 確認於
added_by: 新增者
action: 操作
+ role:
+ role_field:
email_field:
submit_button: 新增擁有者
info: 新增或移除擁有者
@@ -520,6 +577,9 @@ zh-TW:
resent_notice:
create:
success_notice: "%{handle} 已以未確認擁有者的身分加入。所有權存取將在使用者點擊傳送到他們的電子郵件地址的確認信後啟用。"
+ update:
+ update_current_user_role:
+ success_notice:
destroy:
removed_notice:
failed_notice: 無法移除 Gem 的唯一擁有者
@@ -605,6 +665,7 @@ zh-TW:
rubygems:
aside:
downloads_for_this_version: 這個版本
+ gem_version_age: 版本发布
required_ruby_version: Ruby 版本需求
required_rubygems_version: RubyGems 版本需求
requires_mfa: 新版本需要 MFA
@@ -650,6 +711,7 @@ zh-TW:
sha_256_checksum: SHA 256 總和檢查碼
signature_period: 簽名有效期
expired: 已過期
+ provenance_header:
version_navigation:
previous_version: "← 上一版本"
next_version: 下一版本 →
@@ -691,7 +753,7 @@ zh-TW:
updated: 更新於
yanked: 移除於
show:
- subtitle: "%{query}"
+ subtitle_html: "%{query}"
month_update: 於最近一個月更新 (%{count})
week_update: 於最近一週更新 (%{count})
filter:
@@ -711,7 +773,6 @@ zh-TW:
index:
title: 統計資料
all_time_most_downloaded: 歷史下載次數排行
- total_downloads: 總下載次數
total_gems: Gems 總數
total_users: 總使用者數量
users:
@@ -780,8 +841,6 @@ zh-TW:
continue: 繼續
title: 您已成功新增安全裝置
notice_html:
- copied: "[ 已複製 ]"
- copy: "[ 複製 ]"
saved:
webauthn_credential:
confirm_delete: 已刪除認證
diff --git a/config/puma.rb b/config/puma.rb
index cbc576cdb8f..06bcbfdb6d8 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -2,20 +2,32 @@
# are invoked here are part of Puma's configuration DSL. For more information
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
-# Puma can serve each request in a thread from an internal thread pool.
-# The `threads` method setting takes two numbers: a minimum and maximum.
-# Any libraries that use thread pools should be configured to match
-# the maximum value specified for Puma. Default is set to 5 threads for minimum
-# and maximum; this matches the default thread size of Active Record.
-max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
-min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
-threads min_threads_count, max_threads_count
-
-require "concurrent"
+# Puma starts a configurable number of processes (workers) and each process
+# serves each request in a thread from an internal thread pool.
+#
+# The ideal number of threads per worker depends both on how much time the
+# application spends waiting for IO operations and on how much you wish to
+# to prioritize throughput over latency.
+#
+# As a rule of thumb, increasing the number of threads will increase how much
+# traffic a given process can handle (throughput), but due to CRuby's
+# Global VM Lock (GVL) it has diminishing returns and will degrade the
+# response time (latency) of the application.
+#
+# The default is set to 3 threads as it's deemed a decent compromise between
+# throughput and latency for the average Rails application.
+#
+# Any libraries that use a connection pool or another resource pool should
+# be configured to provide at least as many connections as the number of
+# threads. This includes Active Record's `pool` parameter in `database.yml`.
+threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
+threads threads_count, threads_count
rails_env = ENV.fetch("RAILS_ENV") { "development" }
production_like = !%w[development test].include?(rails_env) # rubocop:disable Rails/NegateInclude
+require "concurrent"
+
if production_like
# Specifies that the worker count should equal the number of processors in production.
worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count })
@@ -39,8 +51,9 @@
# Specifies the `environment` that Puma will run in.
environment rails_env
-# Specifies the `pidfile` that Puma will use.
-pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
+# Specify the PID file. Defaults to tmp/pids/server.pid in development.
+# In other environments, only set the PID file if requested.
+pidfile ENV["PIDFILE"] if ENV["PIDFILE"]
before_fork do
sleep 1
diff --git a/config/routes.rb b/config/routes.rb
index fd5871e8c0c..e99f9cd795c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -87,11 +87,14 @@
delete :yank, to: "deletions#create"
end
constraints rubygem_id: Patterns::ROUTE_PATTERN do
- resource :owners, only: %i[show create destroy]
+ resource :owners, only: %i[show create edit update destroy]
resources :trusted_publishers, controller: 'oidc/rubygem_trusted_publishers', only: %i[index create destroy show]
end
end
+ resources :attestations, only: :show, format: /json/,
+ constraints: { id: Patterns::ROUTE_PATTERN }
+
resource :activity, only: [], format: /json|yaml/ do
collection do
get :latest
@@ -157,6 +160,7 @@
get :advanced
end
resource :dashboard, only: :show, constraints: { format: /html|atom/ }
+ resources :subscriptions, only: :index
resources :profiles, only: :show
get "profile/me", to: "profiles#me", as: :my_profile
resource :multifactor_auth, only: %i[update] do
@@ -211,7 +215,7 @@
get '/dependencies', to: 'dependencies#show', constraints: { format: /json|html/ }
end
resources :reverse_dependencies, only: %i[index]
- resources :owners, only: %i[index destroy create], param: :handle do
+ resources :owners, only: %i[index destroy edit update create], param: :handle do
get 'confirm', to: 'owners#confirm', as: :confirm, on: :collection
get 'resend_confirmation', to: 'owners#resend_confirmation', as: :resend_confirmation, on: :collection
end
@@ -255,6 +259,8 @@
get 'verify', to: 'sessions#verify', as: :verify
post 'authenticate', to: 'sessions#authenticate', as: :authenticate
post 'webauthn_authenticate', to: 'sessions#webauthn_authenticate', as: :webauthn_authenticate
+
+ get 'development_log_in_as/:user_id', to: 'sessions#development_log_in_as' if Gemcutter::ENABLE_DEVELOPMENT_LOG_IN
end
resources :users, only: %i[new create]
@@ -263,6 +269,26 @@
delete '/sign_out' => 'sessions#destroy', as: 'sign_out'
get '/sign_up' => 'users#new', as: 'sign_up' if Clearance.configuration.allow_sign_up?
+
+ namespace :organizations, as: :organization do
+ get "onboarding", to: redirect("/organizations/onboarding/name")
+ delete "onboarding", to: "onboarding#destroy"
+
+ namespace :onboarding do
+ get "name", to: "name#new"
+ post "name", to: "name#create"
+
+ get "gems", to: "gems#edit"
+ patch "gems", to: "gems#update"
+
+ get "users", to: "users#edit"
+ patch "users", to: "users#update"
+
+ get "confirm", to: "confirm#edit"
+ patch "confirm", to: "confirm#update"
+ end
+ end
+ resources :organizations, only: %i[show], constraints: { id: Patterns::ROUTE_PATTERN }
end
################################################################################
@@ -331,7 +357,7 @@
get ':provider/callback', to: 'oauth#create'
get 'failure', to: 'oauth#failure'
- get 'development_log_in_as/:admin_github_user_id', to: 'oauth#development_log_in_as' if Gemcutter::ENABLE_DEVELOPMENT_ADMIN_LOG_IN
+ get 'development_log_in_as/:admin_github_user_id', to: 'oauth#development_log_in_as' if Gemcutter::ENABLE_DEVELOPMENT_LOG_IN
end
################################################################################
diff --git a/config/tailwind.config.js b/config/tailwind.config.js
index 7366febac45..f82dab740ee 100644
--- a/config/tailwind.config.js
+++ b/config/tailwind.config.js
@@ -3,7 +3,6 @@ const defaultTheme = require("tailwindcss/defaultTheme");
/** @type {import('tailwindcss').Config} */
module.exports = {
mode: "jit",
- prefix: "tw-", // While we still have non-tailwind classes
content: [
"./app/views/**/*.rb",
"./app/components/**/*rb",
@@ -14,18 +13,241 @@ module.exports = {
],
theme: {
extend: {
+ screens: {
+ 'sm': '480px' // Below this is a phone in portrait mode
+ },
fontFamily: {
- sans: [
- "Helvetica Neue",
- "Helvetica",
- "Arial",
- ...defaultTheme.fontFamily.sans,
- ],
+ sans: ['"Titillium Web"', ...defaultTheme.fontFamily.sans],
+ mono: ['"Fira Code"', ...defaultTheme.fontFamily.mono],
+ },
+ fontSize: {
+ lg: ['1.4375rem', '2.156rem'], // Base/01 23px, 34.5px
+ base: ['1.1875rem', '1.781rem'], // Base/02 19px, 28.5px
+ sm: ['1.0000rem', '1.500rem'], // Base/03 16px, 24px
+ xs: ['0.8750rem', '1.313rem'], // Base/04 14px, 21px
+
+ d1: ['3.1875rem', '3.825rem'], // Display/01 51px, 61.2px
+ d2: ['2.9375rem', '3.525rem'], // Display/02 47px, 56.4px
+ d3: ['2.6875rem', '3.225rem'], // Display/03 43px, 51.6px
+ h1: ['2.4375rem', '2.925rem'], // Header/01 39px, 46.8px
+ h2: ['2.1875rem', '2.625rem'], // Header/02 35px, 42px
+ h3: ['1.8750rem', '2.325rem'], // Header/03 31px, 37.2px
+ h4: ['1.6875rem', '2.025rem'], // Header/04 27px, 32.4px
+ b1: ['1.4375rem', '2.156rem'], // Base/01 23px, 34.5px
+ b2: ['1.1875rem', '1.781rem'], // Base/02 19px, 28.5px
+ b3: ['1.0000rem', '1.500rem'], // Base/03 16px, 24px
+ b4: ['0.8750rem', '1.313rem'], // Base/04 14px, 21px
+ c1: ['1.6875rem', '2.025rem'], // Code/01 27px, 32.4px
+ c2: ['1.4375rem', '1.725rem'], // Code/02 23px, 27.6px
+ c3: ['1.1250rem', '1.350rem'], // Code/03 18px, 21.6px
+ c4: ['1.0000rem', '1.200rem'], // Code/04 16px, 19.2px
},
colors: {
- "rubygems-red": "#e74c3c",
- blackish: "#141c22",
- grayish: "#c1c4ca",
+ transparent: 'transparent',
+ current: 'currentColor',
+ 'white': '#FFFFFF',
+ 'black': '#000000',
+ 'red': { // Warn / Failure
+ 100: '#FFEEF1',
+ 200: '#FFC4CD',
+ 300: '#FF9CB0',
+ 400: '#FF0E3B',
+ 500: '#E4002B',
+ 600: '#BA0023',
+ 700: '#970019',
+ 800: '#730012',
+ 900: '#58000A',
+ },
+ 'orange': { // Primary
+ '050': '#FFF8F1',
+ 100: '#FFF0EC',
+ 200: '#FFC6AD',
+ 300: '#FFA983',
+ 400: '#FF7539',
+ 500: '#F74C27',
+ DEFAULT: '#F74C27', // 500 brand primary
+ 600: '#E04300',
+ 700: '#AD2F14',
+ 800: '#761A05',
+ 900: '#581A0C',
+ 950: '#3A1007',
+ },
+ 'yellow': { // Alert
+ 100: '#FFFBF7',
+ 200: '#FFF4EA',
+ 300: '#FFE4BB',
+ 400: '#FFC772',
+ 500: '#FFAB2D',
+ 600: '#D38C22',
+ 700: '#A66D17',
+ 800: '#7A4E0C',
+ 900: '#4D2E00',
+ },
+ 'green': { // Success
+ 100: '#F1FFFE',
+ 200: '#E1FFFC',
+ 300: '#C9FFF9',
+ 400: '#06B8B9',
+ 500: '#05A3A7',
+ 600: '#03858B',
+ 700: '#006770',
+ 800: '#004F56',
+ 900: '#00373B',
+ },
+ 'blue': { // Info
+ 100: '#F3F9FF',
+ 200: '#E1F1FF',
+ 300: '#92C0F4',
+ 400: '#76ADEC',
+ 500: '#6999D2',
+ 600: '#5B86B8',
+ 700: '#3F699A',
+ 800: '#234C7D',
+ 900: '#113765',
+ 950: '#06264C',
+ },
+ 'neutral': {
+ // WHITE
+ // Light
+ // background (general)
+ // secondary search bar background
+ // Dark
+ // primary text
+ // primary icons
+ '000': '#FFFFFF',
+
+ // neutral-050
+ // Light
+ // work canvas
+ // form input
+ // Dark (not used)
+ '050': '#FBFBFB',
+
+ // neutral-100
+ // Light
+ // primary search bar bg
+ // Dark (not used)
+ '100': '#EEF1F3',
+
+ // neutral-200
+ // Light
+ // tab hover
+ // pagination hover
+ // neutral toasts
+ // neutral badges
+ // neutral labels (dropdown)
+ // low performance indicators
+ // Dark
+ // primary text hover
+ '200': '#E3E7EA',
+
+ // neutral-300
+ // Light
+ // disabled buttons
+ // disabled form input
+ // list dividing line
+ // tab dividing line
+ // outside line default
+ // search bar
+ // dropdown
+ // chips
+ // Dark (not used)
+ '300': '#D7DEE3',
+
+ // neutral-400
+ // Light (not used)
+ // Dark
+ // secondary text
+ // disabled button text
+ // inactive search bar
+ // nav breadcrumbs
+ // secondary icons
+ '400': '#C6CED5',
+
+ // neutral-500
+ // Light
+ // outside stroke
+ // general containers
+ // neutral toasts
+ // outside stroke active states
+ // search bar
+ // drop downs
+ // chips
+ // Dark (not used)
+ '500': '#AEB8C1',
+
+ // neutral-600
+ // Light
+ // secondary text
+ // disabled button text
+ // inactive search bar
+ // nav breadcrumbs
+ // search result gem description captions
+ // text counters [ Gems 4 ]
+ // sort by - not active
+ // secondary icons
+ // Dark
+ // secondary text
+ // search result gem description captions
+ // text counters [ Gems 4 ]
+ // sort by - not active
+ //
+ '600': '#6C7583',
+
+ // neutral-700
+ // Light (not used)
+ // Dark
+ // disabled buttons
+ // disabled form input
+ // list dividing line
+ // tab dividing line
+ // outside line default
+ // search bar
+ // dropdown
+ // chips
+ // outside stroke
+ // general containers
+ // neutral toasts
+ // outside stroke active states
+ // search bar
+ // drop downs
+ // chips
+ '700': '#434B59',
+
+ // neutral-800
+ // Light
+ // primary text
+ // primary icons
+ // Dark
+ // tab hover
+ // pagination hover
+ // neutral toasts
+ // neutral badges
+ // neutral labels (dropdown)
+ // low performance indicators
+ '800': '#333A45',
+
+ // neutral-900
+ // Light (not used)
+ // Dark
+ // primary search bar background
+ '900': '#222831',
+
+ // neutral-950
+ // Light (not used)
+ // Dark
+ // work canvas
+ // form input
+ '950': '#16191E',
+
+ // BLACK
+ // Light
+ // primary text hover
+ // Dark
+ // background (general)
+ // secondary search bar background
+ '1000': '#000000',
+ },
},
},
},
@@ -36,6 +258,5 @@ module.exports = {
require("@tailwindcss/container-queries"),
],
corePlugins: {
- preflight: false,
},
};
diff --git a/db/migrate/20240630025625_create_organizations.rb b/db/migrate/20240630025625_create_organizations.rb
new file mode 100644
index 00000000000..66e7228c92d
--- /dev/null
+++ b/db/migrate/20240630025625_create_organizations.rb
@@ -0,0 +1,13 @@
+class CreateOrganizations < ActiveRecord::Migration[7.1]
+ def change
+ create_table :organizations do |t|
+ t.string :handle, limit: 40
+ t.string :name, limit: 255
+ t.timestamp :deleted_at
+
+ t.timestamps
+
+ t.index "lower(handle)", unique: true
+ end
+ end
+end
diff --git a/db/migrate/20240630025804_create_memberships.rb b/db/migrate/20240630025804_create_memberships.rb
new file mode 100644
index 00000000000..ee39a70ece8
--- /dev/null
+++ b/db/migrate/20240630025804_create_memberships.rb
@@ -0,0 +1,12 @@
+class CreateMemberships < ActiveRecord::Migration[7.1]
+ def change
+ create_table :memberships do |t|
+ t.belongs_to :user, null: false, foreign_key: true
+ t.belongs_to :organization, null: false, foreign_key: true
+ t.timestamp :confirmed_at, default: nil
+
+ t.timestamps
+ t.index %i[user_id organization_id], unique: true
+ end
+ end
+end
diff --git a/db/migrate/20240712003336_create_organization_events.rb b/db/migrate/20240712003336_create_organization_events.rb
new file mode 100644
index 00000000000..03cb1b95eaa
--- /dev/null
+++ b/db/migrate/20240712003336_create_organization_events.rb
@@ -0,0 +1,14 @@
+class CreateOrganizationEvents < ActiveRecord::Migration[7.0]
+ def change
+ create_table :events_organization_events do |t|
+ t.string :tag, null: false, index: true
+ t.string :trace_id, null: true
+ t.references :organization, null: false, foreign_key: true
+ t.references :ip_address, null: true, foreign_key: true
+ t.references :geoip_info, null: true, foreign_key: true
+ t.jsonb :additional
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240722182907_create_good_job_execution_duration.rb b/db/migrate/20240722182907_create_good_job_execution_duration.rb
new file mode 100644
index 00000000000..fef37f07bc1
--- /dev/null
+++ b/db/migrate/20240722182907_create_good_job_execution_duration.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateGoodJobExecutionDuration < ActiveRecord::Migration[7.1]
+ def change
+ reversible do |dir|
+ dir.up do
+ # Ensure this incremental update migration is idempotent
+ # with monolithic install migration.
+ return if connection.column_exists?(:good_job_executions, :duration)
+ end
+ end
+
+ add_column :good_job_executions, :duration, :interval
+ end
+end
diff --git a/db/migrate/20240802151324_add_role_to_ownership.rb b/db/migrate/20240802151324_add_role_to_ownership.rb
new file mode 100644
index 00000000000..236a6040c4d
--- /dev/null
+++ b/db/migrate/20240802151324_add_role_to_ownership.rb
@@ -0,0 +1,5 @@
+class AddRoleToOwnership < ActiveRecord::Migration[7.1]
+ def change
+ add_column :ownerships, :role, :integer, null: false, default: 70 # Access::OWNER
+ end
+end
diff --git a/db/migrate/20240917042436_add_role_to_memberships.rb b/db/migrate/20240917042436_add_role_to_memberships.rb
new file mode 100644
index 00000000000..1a1d311325a
--- /dev/null
+++ b/db/migrate/20240917042436_add_role_to_memberships.rb
@@ -0,0 +1,5 @@
+class AddRoleToMemberships < ActiveRecord::Migration[7.1]
+ def change
+ add_column :memberships, :role, :integer, null: false, default: 50 # Access::MAINTAINER
+ end
+end
diff --git a/db/migrate/20241007193740_create_attestations.rb b/db/migrate/20241007193740_create_attestations.rb
new file mode 100644
index 00000000000..c0f49e4c22d
--- /dev/null
+++ b/db/migrate/20241007193740_create_attestations.rb
@@ -0,0 +1,11 @@
+class CreateAttestations < ActiveRecord::Migration[7.2]
+ def change
+ create_table :attestations do |t|
+ t.belongs_to :version, null: false, foreign_key: true
+ t.jsonb :body
+ t.string :media_type
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20241023052946_create_organization_onboardings.rb b/db/migrate/20241023052946_create_organization_onboardings.rb
new file mode 100644
index 00000000000..90474326c49
--- /dev/null
+++ b/db/migrate/20241023052946_create_organization_onboardings.rb
@@ -0,0 +1,16 @@
+class CreateOrganizationOnboardings < ActiveRecord::Migration[7.2]
+ def change
+ create_table :organization_onboardings do |t|
+ t.string :status, null: false
+ t.string :name_type, null: false
+ t.string :organization_name, null: false
+ t.string :organization_handle, null: false
+ t.text :error, null: true
+ t.integer :rubygems, array: true, default: [], null: true
+ t.datetime :onboarded_at, null: true
+ t.integer :created_by_id, null: false
+ t.integer :onboarded_organization_id, null: true
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20241023053140_add_organization_foreign_keyto_ruby_gems.rb b/db/migrate/20241023053140_add_organization_foreign_keyto_ruby_gems.rb
new file mode 100644
index 00000000000..27359ca9772
--- /dev/null
+++ b/db/migrate/20241023053140_add_organization_foreign_keyto_ruby_gems.rb
@@ -0,0 +1,8 @@
+class AddOrganizationForeignKeytoRubyGems < ActiveRecord::Migration[7.2]
+ disable_ddl_transaction!
+
+ def change
+ add_reference :rubygems, :organization, null: true, index: { algorithm: :concurrently }
+ add_foreign_key :rubygems, :organizations, column: :organization_id, on_delete: :nullify, validate: false
+ end
+end
diff --git a/db/migrate/20241023053210_validate_add_organization_foreign_keyto_ruby_gems.rb b/db/migrate/20241023053210_validate_add_organization_foreign_keyto_ruby_gems.rb
new file mode 100644
index 00000000000..00d7ae1d13f
--- /dev/null
+++ b/db/migrate/20241023053210_validate_add_organization_foreign_keyto_ruby_gems.rb
@@ -0,0 +1,5 @@
+class ValidateAddOrganizationForeignKeytoRubyGems < ActiveRecord::Migration[7.2]
+ def change
+ validate_foreign_key :rubygems, :organizations
+ end
+end
diff --git a/db/migrate/20241104065953_add_organization_onboarding_invites.rb b/db/migrate/20241104065953_add_organization_onboarding_invites.rb
new file mode 100644
index 00000000000..9e0d98cefde
--- /dev/null
+++ b/db/migrate/20241104065953_add_organization_onboarding_invites.rb
@@ -0,0 +1,11 @@
+class AddOrganizationOnboardingInvites < ActiveRecord::Migration[7.2]
+ def change
+ create_table :organization_onboarding_invites do |t|
+ t.references :organization_onboarding, null: false, foreign_key: true
+ t.references :user, null: false, foreign_key: true
+ t.string :role, null: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e1bf4f13690..99235afcc75 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2024_05_22_185717) do
+ActiveRecord::Schema[7.2].define(version: 2024_11_04_065953) do
# These are extensions that must be enabled in order to support this database
enable_extension "hstore"
enable_extension "pgcrypto"
@@ -56,6 +56,15 @@
t.check_constraint "scopes IS NOT NULL", name: "api_keys_scopes_null"
end
+ create_table "attestations", force: :cascade do |t|
+ t.bigint "version_id", null: false
+ t.jsonb "body"
+ t.string "media_type"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["version_id"], name: "index_attestations_on_version_id"
+ end
+
create_table "audits", force: :cascade do |t|
t.string "auditable_type", null: false
t.bigint "auditable_id", null: false
@@ -94,6 +103,21 @@
t.index ["version_id"], name: "index_dependencies_on_version_id"
end
+ create_table "events_organization_events", force: :cascade do |t|
+ t.string "tag", null: false
+ t.string "trace_id"
+ t.bigint "organization_id", null: false
+ t.bigint "ip_address_id"
+ t.bigint "geoip_info_id"
+ t.jsonb "additional"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["geoip_info_id"], name: "index_events_organization_events_on_geoip_info_id"
+ t.index ["ip_address_id"], name: "index_events_organization_events_on_ip_address_id"
+ t.index ["organization_id"], name: "index_events_organization_events_on_organization_id"
+ t.index ["tag"], name: "index_events_organization_events_on_tag"
+ end
+
create_table "events_rubygem_events", force: :cascade do |t|
t.string "tag", null: false
t.string "trace_id"
@@ -187,6 +211,7 @@
t.integer "error_event", limit: 2
t.text "error_backtrace", array: true
t.uuid "process_id"
+ t.interval "duration"
t.index ["active_job_id", "created_at"], name: "index_good_job_executions_on_active_job_id_and_created_at"
t.index ["process_id", "created_at"], name: "index_good_job_executions_on_process_id_and_created_at"
end
@@ -315,6 +340,18 @@
t.index ["task_name", "status", "created_at"], name: "index_maintenance_tasks_runs", order: { created_at: :desc }
end
+ create_table "memberships", force: :cascade do |t|
+ t.bigint "user_id", null: false
+ t.bigint "organization_id", null: false
+ t.datetime "confirmed_at", precision: nil
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.integer "role", default: 50, null: false
+ t.index ["organization_id"], name: "index_memberships_on_organization_id"
+ t.index ["user_id", "organization_id"], name: "index_memberships_on_user_id_and_organization_id", unique: true
+ t.index ["user_id"], name: "index_memberships_on_user_id"
+ end
+
create_table "oidc_api_key_roles", force: :cascade do |t|
t.bigint "oidc_provider_id", null: false
t.bigint "user_id", null: false
@@ -382,6 +419,39 @@
t.index ["repository_owner", "repository_name", "repository_owner_id", "workflow_filename", "environment"], name: "index_oidc_trusted_publisher_github_actions_claims", unique: true
end
+ create_table "organization_onboarding_invites", force: :cascade do |t|
+ t.bigint "organization_onboarding_id", null: false
+ t.bigint "user_id", null: false
+ t.string "role"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["organization_onboarding_id"], name: "idx_on_organization_onboarding_id_e5b08868fb"
+ t.index ["user_id"], name: "index_organization_onboarding_invites_on_user_id"
+ end
+
+ create_table "organization_onboardings", force: :cascade do |t|
+ t.string "status", null: false
+ t.string "name_type", null: false
+ t.string "organization_name", null: false
+ t.string "organization_handle", null: false
+ t.text "error"
+ t.integer "rubygems", default: [], array: true
+ t.datetime "onboarded_at"
+ t.integer "created_by_id", null: false
+ t.integer "onboarded_organization_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ create_table "organizations", force: :cascade do |t|
+ t.string "handle", limit: 40
+ t.string "name", limit: 255
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index "lower((handle)::text)", name: "index_organizations_on_lower_handle", unique: true
+ end
+
create_table "ownership_calls", force: :cascade do |t|
t.bigint "rubygem_id"
t.bigint "user_id"
@@ -419,6 +489,7 @@
t.boolean "owner_notifier", default: true, null: false
t.integer "authorizer_id"
t.boolean "ownership_request_notifier", default: true, null: false
+ t.integer "role", default: 70, null: false
t.index ["rubygem_id"], name: "index_ownerships_on_rubygem_id"
t.index ["user_id", "rubygem_id"], name: "index_ownerships_on_user_id_and_rubygem_id", unique: true
end
@@ -428,10 +499,12 @@
t.datetime "created_at", precision: nil
t.datetime "updated_at", precision: nil
t.boolean "indexed", default: false, null: false
+ t.bigint "organization_id"
t.index "regexp_replace(upper((name)::text), '[_-]'::text, ''::text, 'g'::text)", name: "dashunderscore_typos_idx"
t.index "upper((name)::text) varchar_pattern_ops", name: "index_rubygems_upcase"
t.index ["indexed"], name: "index_rubygems_on_indexed"
t.index ["name"], name: "index_rubygems_on_name", unique: true
+ t.index ["organization_id"], name: "index_rubygems_on_organization_id"
end
create_table "sendgrid_events", force: :cascade do |t|
@@ -580,7 +653,11 @@
end
add_foreign_key "api_key_rubygem_scopes", "api_keys", name: "api_key_rubygem_scopes_api_key_id_fk"
+ add_foreign_key "attestations", "versions"
add_foreign_key "audits", "admin_github_users", name: "audits_admin_github_user_id_fk"
+ add_foreign_key "events_organization_events", "geoip_infos"
+ add_foreign_key "events_organization_events", "ip_addresses"
+ add_foreign_key "events_organization_events", "organizations"
add_foreign_key "events_rubygem_events", "geoip_infos"
add_foreign_key "events_rubygem_events", "ip_addresses"
add_foreign_key "events_rubygem_events", "rubygems"
@@ -589,12 +666,16 @@
add_foreign_key "events_user_events", "users"
add_foreign_key "ip_addresses", "geoip_infos"
add_foreign_key "linksets", "rubygems", name: "linksets_rubygem_id_fk"
+ add_foreign_key "memberships", "organizations"
+ add_foreign_key "memberships", "users"
add_foreign_key "oidc_api_key_roles", "oidc_providers"
add_foreign_key "oidc_api_key_roles", "users"
add_foreign_key "oidc_id_tokens", "api_keys"
add_foreign_key "oidc_id_tokens", "oidc_api_key_roles"
add_foreign_key "oidc_pending_trusted_publishers", "users"
add_foreign_key "oidc_rubygem_trusted_publishers", "rubygems"
+ add_foreign_key "organization_onboarding_invites", "organization_onboardings"
+ add_foreign_key "organization_onboarding_invites", "users"
add_foreign_key "ownership_calls", "rubygems", name: "ownership_calls_rubygem_id_fk"
add_foreign_key "ownership_calls", "users", name: "ownership_calls_user_id_fk"
add_foreign_key "ownership_requests", "ownership_calls", name: "ownership_requests_ownership_call_id_fk"
@@ -602,6 +683,7 @@
add_foreign_key "ownership_requests", "users", column: "approver_id", name: "ownership_requests_approver_id_fk"
add_foreign_key "ownership_requests", "users", name: "ownership_requests_user_id_fk"
add_foreign_key "ownerships", "users", on_delete: :cascade
+ add_foreign_key "rubygems", "organizations", on_delete: :nullify
add_foreign_key "versions", "api_keys", column: "pusher_api_key_id"
add_foreign_key "versions", "rubygems", name: "versions_rubygem_id_fk"
add_foreign_key "web_hooks", "users", name: "web_hooks_user_id_fk"
diff --git a/db/seeds.rb b/db/seeds.rb
index 2b84f60671e..7af1eed39f6 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -26,6 +26,7 @@
).find_or_create_by!(email: "gem-requester@example.com")
User.create_with(
+ handle: "gem-security",
email_confirmed: true,
password:
).find_or_create_by!(email: "security@rubygems.org")
@@ -103,7 +104,9 @@
user.web_hooks.find_or_create_by!(url: "https://example.com/rubygem0", rubygem: rubygem0)
user.web_hooks.find_or_create_by!(url: "http://example.com/all", rubygem: nil)
-author.api_keys.find_or_create_by!(hashed_key: "securehashedkey", name: "api key", scopes: %i[push_rubygem])
+author.api_keys.create_with(
+ hashed_key: Digest::SHA256.hexdigest("gem-author-key")
+).find_or_create_by!(name: "api key", scopes: %i[push_rubygem])
Admin::GitHubUser.create_with(
is_admin: true,
@@ -297,6 +300,11 @@
environment: "deploy"
).rubygem_trusted_publishers.find_or_create_by!(rubygem: rubygem0)
+rubygem0.versions.find_by(full_name: "rubygem0-1.0.0").attestations.find_or_create_by!(
+ media_type: Sigstore::BundleType::BUNDLE_0_3.media_type,
+ body: JSON.parse(Rails.root.join("test", "gems", "sigstore-1.0.0.gem.sigstore.json").read)
+)
+
author.oidc_pending_trusted_publishers.create_with(
expires_at: 100.years.from_now
).find_or_create_by!(
diff --git a/docker-compose.yml b/docker-compose.yml
index 3bcc4b47463..f2c7e57bb24 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,25 +1,26 @@
services:
db:
- image: postgres:13.14
+ image: index.docker.io/library/postgres@sha256:e0892b968fb80d181a96f18bfef0a8a1693c2430fb2bc7392e65a53057eaa303 # 13.14
ports:
- "5432:5432"
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
downloads-db:
- image: timescale/timescaledb:2.15.1-pg16
+ image: index.docker.io/timescale/timescaledb@sha256:2e3a19fa4624addcb2bb8d37dfe2fee9e12597537b057a742c68aa226ed77da5 # 2.15.1-pg16
ports:
- "5434:5432"
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
cache:
- image: memcached:1.4.39
+ image: index.docker.io/library/memcached@sha256:f4504742a8fb03c3ac0cd172e1c1d2277117629f8d21d52f78307121ddc3de5f # 1.4.39
ports:
- "11211:11211"
search:
- image: opensearchproject/opensearch:2.13.0
+ image: index.docker.io/opensearchproject/opensearch@sha256:2e954ff0e8c9d0f4868b4818150b3aecc92fbb0cc4a24d00dace38ada227291d # 2.13.0
environment:
- discovery.type=single-node
- DISABLE_SECURITY_PLUGIN=true
+ - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
healthcheck:
@@ -35,12 +36,13 @@ services:
timeout: 5s
retries: 6
search-console:
- image: opensearchproject/opensearch-dashboards:2.13.0
+ image: index.docker.io/opensearchproject/opensearch-dashboards@sha256:d8f4442da4d0cb44865a5eab01c9eb9f00769e2d5f053d21e3ff3c64a50fc6ec # 2.13.0
ports:
- "5601:5601"
environment:
- 'OPENSEARCH_HOSTS=["http://search:9200"]'
- "DISABLE_SECURITY_DASHBOARDS_PLUGIN=true"
+ - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
healthcheck:
test:
[
@@ -53,5 +55,5 @@ services:
search:
condition: service_healthy
toxiproxy:
- image: ghcr.io/shopify/toxiproxy:2.5.0
+ image: ghcr.io/shopify/toxiproxy@sha256:927c797a2115a193ae3a527e5a36782b938419904ac6706ca0efa029ebea58cb # 2.5.0
network_mode: "host"
diff --git a/lib/access.rb b/lib/access.rb
new file mode 100644
index 00000000000..e86093fb79b
--- /dev/null
+++ b/lib/access.rb
@@ -0,0 +1,27 @@
+module Access
+ MAINTAINER = 50
+ ADMIN = 60
+ OWNER = 70
+
+ DEFAULT_ROLE = "owner".freeze
+
+ ROLES = {
+ "maintainer" => MAINTAINER,
+ "owner" => OWNER,
+ "admin" => ADMIN
+ }.with_indifferent_access.freeze
+
+ def self.flag_for_role(role)
+ ROLES.fetch(role)
+ end
+
+ def self.with_minimum_role(role)
+ Range.new(flag_for_role(role), nil)
+ end
+
+ def self.role_for_flag(flag)
+ ROLES.key(flag)&.inquiry.tap do |role|
+ raise ArgumentError, "Unknown role flag: #{flag}" if role.blank?
+ end
+ end
+end
diff --git a/lib/admin/authorization_client.rb b/lib/admin/authorization_client.rb
index 6a95c709438..db6e81df77a 100644
--- a/lib/admin/authorization_client.rb
+++ b/lib/admin/authorization_client.rb
@@ -1,6 +1,6 @@
# This class is the same as the default pundit authorization client.
# It just adds the admin scope automatically so that Avo pundit policies can be kept separate.
-class Admin::AuthorizationClient < Avo::Services::AuthorizationClients::PunditClient
+class Admin::AuthorizationClient < Avo::Pro::Authorization::Clients::PunditClient
def authorize(user, record, action, policy_class: nil)
# After https://github.com/avo-hq/avo/pull/2827 lands, we can hopefully remove this hack
policy_class ||= Admin::GitHubUserPolicy if record == Admin::GitHubUser
diff --git a/lib/github_oauthable.rb b/lib/github_oauthable.rb
index 31d621bf4fb..04047d9d975 100644
--- a/lib/github_oauthable.rb
+++ b/lib/github_oauthable.rb
@@ -73,7 +73,7 @@ def log_in_as(user:, expires: 1.hour)
cookies.encrypted[admin_cookie_name] = {
value: user.id,
expires: expires,
- same_site: :lax
+ same_site: :strict
}
end
diff --git a/lib/name_format_validator.rb b/lib/name_format_validator.rb
index d8fae743d2c..fa59e37b185 100644
--- a/lib/name_format_validator.rb
+++ b/lib/name_format_validator.rb
@@ -2,14 +2,18 @@
class NameFormatValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- if value.class != String
- record.errors.add attribute, "must be a String"
- elsif !Patterns::LETTER_REGEXP.match?(value)
- record.errors.add attribute, "must include at least one letter"
- elsif !Patterns::NAME_PATTERN.match?(value)
- record.errors.add attribute, "can only include letters, numbers, dashes, and underscores"
- elsif Patterns::SPECIAL_CHAR_PREFIX_REGEXP.match?(value)
- record.errors.add attribute, "can not begin with a period, dash, or underscore"
- end
+ return record.errors.add attribute, "must be a String" if value.class != String
+
+ record.errors.add attribute, "must include at least one letter" if requires_letter? && !Patterns::LETTER_REGEXP.match?(value)
+ record.errors.add attribute, "can only include letters, numbers, dashes, and underscores" unless Patterns::NAME_PATTERN.match?(value)
+ record.errors.add attribute, "can not begin with a period, dash, or underscore" if Patterns::SPECIAL_CHAR_PREFIX_REGEXP.match?(value)
+ record.errors.add attribute, "can not end with a period, dash, or underscore" if Patterns::SPECIAL_CHAR_SUFFIX_REGEXP.match?(value)
+ record.errors.add attribute, "can not end with a common file extension" if Patterns::BANNED_EXTENSION_REGEXP.match?(value)
+ end
+
+ private
+
+ def requires_letter?
+ options.fetch(:requires_letter, true)
end
end
diff --git a/lib/patterns.rb b/lib/patterns.rb
index 4ac37857d77..4b3a2674340 100644
--- a/lib/patterns.rb
+++ b/lib/patterns.rb
@@ -1,16 +1,19 @@
module Patterns
extend ActiveSupport::Concern
- JAVA_HTTP_USER_AGENT = /^java/i
SPECIAL_CHARACTERS = ".-_".freeze
ALLOWED_CHARACTERS = "[A-Za-z0-9#{Regexp.escape(SPECIAL_CHARACTERS)}]+".freeze
- ROUTE_PATTERN = /#{ALLOWED_CHARACTERS}/
+ ROUTE_PATTERN = /#{ALLOWED_CHARACTERS}(? e
- errored += 1
- Rails.logger.error("[extraneous_dependencies:clean] skipping #{version.inspect} - #{e.message}")
- ensure
- processed += 1
- end
-
- Rails.logger.info("[extraneous_dependencies:clean] #{total_deleted_deps} dependencies deleted")
- Rails.logger.info("[extraneous_dependencies:clean] #{errored}/#{processed} errors")
- Rails.logger.info("[extraneous_dependencies:clean] #{mis_match_versions}/#{processed} version mismatches " \
- "(run_deps: #{run_mis_match}, dev_deps: #{dev_mis_match})")
- end
-end
diff --git a/lib/tasks/gemcutter.rake b/lib/tasks/gemcutter.rake
index 51987a3623c..c2e0c61935c 100644
--- a/lib/tasks/gemcutter.rake
+++ b/lib/tasks/gemcutter.rake
@@ -1,5 +1,3 @@
-require "tasks/helpers/gemcutter_tasks_helper"
-
namespace :gemcutter do
namespace :index do
desc "Update the index"
@@ -26,84 +24,6 @@ namespace :gemcutter do
end
end
- namespace :checksums do
- desc "Initialize missing checksums."
- task init: :environment do
- without_sha256 = Version.where(sha256: nil)
- mod = ENV["shard"]
- without_sha256.where("id % 4 = ?", mod.to_i) if mod
-
- total = without_sha256.count
- i = 0
- without_sha256.find_each do |version|
- GemcutterTaskshelper.recalculate_sha256!(version)
- i += 1
- print format("\r%.2f%% (%d/%d) complete", i.to_f / total * 100.0, i, total)
- end
- puts
- puts "Done."
- end
-
- desc "Check existing checksums."
- task check: :environment do
- failed = false
- i = 0
- total = Version.count
- Version.find_each do |version|
- actual_sha256 = GemcutterTaskshelper.recalculate_sha256(version.full_name)
- if actual_sha256 && version.sha256 != actual_sha256
- puts "#{version.full_name}.gem has sha256 '#{actual_sha256}', " \
- "but '#{version.sha256}' was expected."
- failed = true
- end
- i += 1
- print format("\r%.2f%% (%d/%d) complete", i.to_f / total * 100.0, i, total)
- end
- end
- end
-
- namespace :metadata do
- desc "Backfill old gem versions with metadata."
- task backfill: :environment do
- without_metadata = Version.where("metadata = ''")
- mod = ENV["shard"]
- without_metadata = without_metadata.where("id % 4 = ?", mod.to_i) if mod
-
- total = without_metadata.count
- i = 0
- puts "Total: #{total}"
- without_metadata.find_each do |version|
- GemcutterTaskshelper.recalculate_metadata!(version)
- i += 1
- print format("\r%.2f%% (%d/%d) complete", i.to_f / total * 100.0, i, total)
- end
- puts
- puts "Done."
- end
- end
-
- namespace :required_ruby_version do
- desc "Backfill gem versions with rubygems_version."
- task backfill: :environment do
- ActiveRecord::Base.logger.level = 1 if Rails.env.development?
-
- without_required_ruby_version = Version.where("created_at < '2014-03-21' and required_ruby_version is null and indexed = true")
- mod = ENV["shard"]
- without_required_ruby_version = without_required_ruby_version.where("id % 4 = ?", mod.to_i) if mod
-
- total = without_required_ruby_version.count
- i = 0
- puts "Total: #{total}"
- without_required_ruby_version.find_each do |version|
- GemcutterTaskshelper.assign_required_ruby_version!(version)
- i += 1
- print format("\r%.2f%% (%d/%d) complete", i.to_f / total * 100.0, i, total)
- end
- puts
- puts "Done."
- end
- end
-
namespace :typo do
desc "Add names to gem typo exception list\nUsage: rake gemcutter:typo:exception[,]"
task :exception, %i[name info] => %i[environment] do |_task, args|
diff --git a/lib/tasks/helpers/gemcutter_tasks_helper.rb b/lib/tasks/helpers/gemcutter_tasks_helper.rb
deleted file mode 100644
index 77477f5dc69..00000000000
--- a/lib/tasks/helpers/gemcutter_tasks_helper.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-module GemcutterTaskshelper
- module_function
-
- def recalculate_sha256(version_full_name)
- key = "gems/#{version_full_name}.gem"
- file = RubygemFs.instance.get(key)
- Digest::SHA2.base64digest(file) if file
- end
-
- def recalculate_sha256!(version)
- sha256 = recalculate_sha256(version.full_name)
- version.update(sha256: sha256)
- end
-
- def recalculate_metadata!(version)
- metadata = get_spec_attribute(version.full_name, "metadata")
- version.update(metadata: metadata || {})
- end
-
- def assign_required_ruby_version!(version)
- required_ruby_version = get_spec_attribute(version.full_name, "required_ruby_version")
-
- return if required_ruby_version.nil? || required_ruby_version.to_s == ">= 0"
- Rails.logger.info("[gemcutter:required_ruby_version:backfill] updating version: #{version.full_name} " \
- "with required_ruby_version: #{required_ruby_version}")
-
- version.update_column(:required_ruby_version, required_ruby_version.to_s)
- CompactIndexTasksHelper.update_last_checksum(version.rubygem, "gemcutter:required_ruby_version:backfill")
- end
-
- def get_spec_attribute(version_full_name, attribute_name)
- key = "quick/Marshal.4.8/#{version_full_name}.gemspec.rz"
- file = RubygemFs.instance.get(key)
- return nil unless file
- spec = Marshal.load(Gem::Util.inflate(file))
- spec.send(attribute_name)
- rescue StandardError => e
- Rails.logger.info("[gemcutter:required_ruby_version:backfill] could not get required_ruby_version for version: #{version_full_name} " \
- "error: #{e.inspect}")
- nil
- end
-end
diff --git a/lib/tasks/helpers/importmap_helper.rb b/lib/tasks/helpers/importmap_helper.rb
index 861bcc589ca..7c95a909829 100644
--- a/lib/tasks/helpers/importmap_helper.rb
+++ b/lib/tasks/helpers/importmap_helper.rb
@@ -9,43 +9,60 @@ class Packager < Importmap::Packager
self.endpoint = URI("https://api.jspm.io/generate")
# Copied from https://github.com/rails/importmap-rails/pull/237
- def verify(package, url)
+ def verify(package, url, verbose: false)
ensure_vendor_directory_exists
- return unless vendored_package_path(package).file?
- verify_vendored_package(package, url)
+ unless vendored_package_path(package).file?
+ raise ImportmapHelper::VerifyError, "Pinned #{package}#{extract_package_version_from(url)} does not exist in vendor/javascript"
+ end
+ verify_vendored_package(package, url, verbose:)
end
- def verify_vendored_package(package, url)
+ def verify_vendored_package(package, url, verbose: false)
vendored_body = vendored_package_path(package).read.strip
- remote_body = load_package_file(package, url).strip
+ vendored_body = vendored_body.lines[2..].join if vendored_body.start_with?("//") # remove the importmap-rails comment
+ remote_body = load_package_file(url).strip
return true if vendored_body == remote_body
- raise ImportmapHelper::VerifyError, "Vendored #{package}#{extract_package_version_from(url)} does not match remote #{url}"
+ verbose_error = verbose ? verbose_diff(remote_body, vendored_body) : " (run with VERBOSE=true for diff)"
+ raise ImportmapHelper::VerifyError, "Vendored #{package}#{extract_package_version_from(url)} does not match remote #{url}#{verbose_error}"
end
- def load_package_file(package, url)
+ def load_package_file(url)
response = Net::HTTP.get_response(URI(url))
if response.code == "200"
- format_vendored_package(package, url, response.body)
+ format_vendored_package(response.body)
else
handle_failure_response(response)
end
end
- def format_vendored_package(package, url, source)
- formatted = +""
- if Gem::Version.new(Importmap::VERSION) > Gem::Version.new("2.0.1")
- formatted.concat "// #{package}#{extract_package_version_from(url)} downloaded from #{url}\n\n"
+ def format_vendored_package(source)
+ remove_sourcemap_comment_from(source).force_encoding("UTF-8")
+ end
+
+ def save_vendored_package(package, url, source)
+ File.open(vendored_package_path(package), "w+") do |vendored_package|
+ vendored_package.write "// #{package}#{extract_package_version_from(url)} downloaded from #{url}\n\n"
+
+ vendored_package.write remove_sourcemap_comment_from(source).force_encoding("UTF-8")
end
- formatted.concat remove_sourcemap_comment_from(source).force_encoding("UTF-8")
- formatted
end
- def save_vendored_package(package, _url, source)
- File.write(vendored_package_path(package), source)
+ def verbose_diff(remote_body, vendored_body)
+ require "diff/lcs"
+ diffs = Diff::LCS.sdiff(remote_body.split("\n"), vendored_body.split("\n"))
+ out = "\n\nDiff:\n- Remote\n+ Vendored\n\n"
+ out + diffs.map do |diff|
+ case diff.action
+ when "-" then "- #{diff.old_element}"
+ when "!" then "- #{diff.old_element}\n+ #{diff.new_element}"
+ when "+" then "+ #{diff.new_element}"
+ when "=" then " #{diff.old_element}"
+ end
+ end.join("\n")
end
public :vendored_package_path
diff --git a/lib/tasks/importmap.rake b/lib/tasks/importmap.rake
index e941b84d6c2..22d9283516c 100644
--- a/lib/tasks/importmap.rake
+++ b/lib/tasks/importmap.rake
@@ -6,11 +6,18 @@ require "tasks/helpers/importmap_helper"
namespace :importmap do
desc "Verify downloaded packages in vendor/javascript"
task :verify do # rubocop:disable Rails/RakeEnvironment
- all_files = Rails.root.glob("vendor/javascript/*.js").map { |p| p.relative_path_from(Rails.root) }
- all_files.delete(Pathname.new("vendor/javascript/github-buttons.js")) || raise("importmap:verify expected github-buttons.js not found")
- all_files.delete(Pathname.new("vendor/javascript/webauthn-json.js")) || raise("importmap:verify expected webauthn-json.js not found")
+ options = { env: "production", from: "jspm.io" }
+ importmap_path = "config/importmap.rb"
+ vendor_pathname = Rails.root.join("vendor/javascript")
+ all_files = vendor_pathname.glob("*.js").map { |p| p.relative_path_from(Rails.root) }
+ manually_vendored_files = ["github-buttons.js", "webauthn-json.js"]
- npm = Importmap::Npm.new(Rails.root.join("config/importmap.rb"))
+ manually_vendored_files.each do |filename|
+ path = vendor_pathname.join(filename).relative_path_from(Rails.root)
+ all_files.delete(path) || raise("importmap:verify expected #{path} not found")
+ end
+
+ npm = Importmap::Npm.new(importmap_path)
packages = npm.packages_with_versions.map do |p, v|
v.blank? ? p : [p, v].join("@")
@@ -18,12 +25,15 @@ namespace :importmap do
puts "Verifying packages in vendor/javascript"
- packager = ImportmapHelper::Packager.new
+ packager = ImportmapHelper::Packager.new(
+ importmap_path,
+ vendor_path: vendor_pathname.relative_path_from(Rails.root)
+ )
- if (imports = packager.import(*packages, env: "production", from: "jspm.io"))
+ if (imports = packager.import(*packages, **options))
imports.each do |package, url|
puts %(Verifying "#{package}" download from #{url})
- packager.verify(package, url)
+ packager.verify(package, url, verbose: ENV["VERBOSE"])
path = packager.vendored_package_path(package)
puts %(Verified "#{package}" at #{path})
all_files.delete path
@@ -45,4 +55,27 @@ namespace :importmap do
exit 1
end
end
+
+ desc "Re-download all packages in the importmap with the same versions"
+ task pristine: :environment do
+ options = { env: "production", from: "jspm.io" }
+ npm = Importmap::Npm.new(Rails.root.join("config/importmap.rb"))
+
+ packages = npm.packages_with_versions.map do |p, v|
+ v.blank? ? p : [p, v].join("@")
+ end
+
+ puts "Downloading pristine packages from #{options[:from]} to vendor/javascript"
+
+ packager = ImportmapHelper::Packager.new
+
+ if (imports = packager.import(*packages, env: options[:env], from: options[:from]))
+ imports.each do |package, url|
+ puts %(Downloading pinned "#{package}" to #{packager.vendor_path}/#{package}.js from #{url})
+ packager.download(package, url)
+ end
+ else
+ puts "Couldn't find any packages in #{packages.inspect} on #{options[:from]}"
+ end
+ end
end
diff --git a/public/robots.txt b/public/robots.txt
index 1fab05ef6e9..a1bc7a1368b 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -1,3 +1,4 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
User-agent: *
Disallow: /downloads/
Disallow: /gems?letter=*
diff --git a/script/build_docker.sh b/script/build_docker.sh
index 11ed8f8fcc0..55157c18c01 100755
--- a/script/build_docker.sh
+++ b/script/build_docker.sh
@@ -22,6 +22,8 @@ docker buildx build --cache-from=type=local,src=/tmp/.buildx-cache \
--tag "$DOCKER_TAG" \
--build-arg RUBYGEMS_VERSION="$RUBYGEMS_VERSION" \
--build-arg REVISION="$GITHUB_SHA" \
+ --build-arg BUNDLE_WITH="$([ -n "${BUNDLE_PACKAGER__DEV}" ] && echo "avo")" \
+ --secret id=BUNDLE_PACKAGER__DEV \
.
# This is a ruby script we run to ensure that all dependencies are configured properly in
@@ -66,6 +68,7 @@ fi
pusher_arn="arn:aws:iam::048268392960:role/rubygems-ecr-pusher"
caller_arn="$(aws sts get-caller-identity --output text --query Arn || true)"
+set +x
[[ "$caller_arn" == "$pusher_arn" ]] ||
[[ "$caller_arn" == "arn:aws:sts::048268392960:assumed-role/rubygems-ecr-pusher/GitHubActions" ]] ||
export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" \
@@ -75,6 +78,7 @@ caller_arn="$(aws sts get-caller-identity --output text --query Arn || true)"
--query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" \
--output text)) ||
true
+set -x
if [[ -z "${AWS_SESSION_TOKEN}" ]]; then
echo "Skipping push since no AWS session token was found"
diff --git a/script/dev b/script/dev
index a58371a0a8f..2406a5181c6 100755
--- a/script/dev
+++ b/script/dev
@@ -2,10 +2,11 @@
export GITHUB_KEY="op://bq25xfqpafelzdxwqu3mdq6rza/zsq3oitss3rbo4njqpjfd5r5cm/GITHUB_KEY"
export GITHUB_SECRET="op://bq25xfqpafelzdxwqu3mdq6rza/zsq3oitss3rbo4njqpjfd5r5cm/GITHUB_SECRET"
-export AVO_LICENSE_KEY="op://bq25xfqpafelzdxwqu3mdq6rza/netp2leghd7zqdxlkp5asy67d4/license key"
+export AVO_LICENSE_KEY="op://bq25xfqpafelzdxwqu3mdq6rza/y32yshzph7shehubizpzphv6oe/license key"
export DATADOG_CSP_API_KEY="op://bq25xfqpafelzdxwqu3mdq6rza/eisktguwm3pcfkwm7avqamdnxq/credential"
export HOOK_RELAY_ACCOUNT_ID="op://bq25xfqpafelzdxwqu3mdq6rza/s3fs2zznjssijxouugddmwnuma/username"
export HOOK_RELAY_HOOK_ID="op://bq25xfqpafelzdxwqu3mdq6rza/s3fs2zznjssijxouugddmwnuma/credential"
+export HOOK_RELAY_API_KEY="op://bq25xfqpafelzdxwqu3mdq6rza/s3fs2zznjssijxouugddmwnuma/API Key"
export LAUNCH_DARKLY_SDK_KEY="op://bq25xfqpafelzdxwqu3mdq6rza/3o73k6fcenzrbspwcm4g4j5z2a/credential"
-exec op run --account 5MS5DHTO5NH7BAHTSIGT5NOAIE -- "${@}"
+exec op run --no-masking --account 5MS5DHTO5NH7BAHTSIGT5NOAIE -- "${@}"
diff --git a/script/update-rubygems b/script/update-rubygems
new file mode 100755
index 00000000000..fe2677126c5
--- /dev/null
+++ b/script/update-rubygems
@@ -0,0 +1,78 @@
+#!/usr/bin/env ruby
+
+unless ARGV.empty?
+ puts "Usage: #{$PROGRAM_NAME}"
+ exit 1
+end
+
+require "bundler"
+require "yaml"
+
+return unless (bundler_version = Bundler.self_manager.send(:resolve_update_version_from, ">= 0")&.version)
+rubygems_version = Gem::Version.new(bundler_version.segments.tap { |s| s[0] += 1 }.join("."))
+
+def order_nodes(nodes)
+ methods = %i[start_line start_column end_line end_column]
+ nodes.sort_by { |node| methods.map { |k| node.send(k) } }.reverse
+end
+
+def find_nodes(node, path) # rubocop:disable Metrics
+ return [node] if path.empty?
+ head, *tail = path
+
+ raise "Expected to index #{path}, got #{node}" if node.scalar?
+
+ if head == "*"
+ if node.sequence?
+ return node.children.flat_map { |child| find_nodes(child, tail) }.compact
+ elsif node.mapping?
+ return node.children.each_slice(2).flat_map { |_, v| find_nodes(v, tail) }.compact
+ else
+ raise "Expected to index #{path}, got #{node}"
+ end
+ end
+
+ if node.document?
+ find_nodes(node.root, path)
+ elsif node.sequence?
+ head, expected_value = head.split("=", 2)
+ if expected_value
+ node.to_ruby.each_with_index.select { |h, _| h[head] == expected_value }.map(&:last).flat_map { |i| find_nodes(node.children[i], tail) }
+ else
+ find_nodes(node.children[head.to_i], tail)
+ end
+ elsif node.mapping?
+ node.children.each_slice(2).flat_map { |k, v| find_nodes(v, tail) if k.value == head && (!expected_value || expected_value == k.value) }.compact
+ else
+ raise "Expected to index #{path}, got #{node}"
+ end
+end
+
+def sub_yaml(file, path, value)
+ nodes = find_nodes YAML.parse_file(file), path
+ contents = File.read(file)
+ lines = contents.lines
+ order_nodes(nodes).each do |node|
+ raise "Expected single line node, got #{node}" if node.start_line != node.end_line
+ line = lines[node.start_line]
+ range = node.start_column..node.end_column.pred
+ raise "Expected range to be #{node.value.inspect}, is #{line[range].inspect}" unless YAML.load(line[range]) == node.value
+ line[range] = value
+ end
+ File.write(file, lines.join)
+end
+
+sub_yaml ".github/workflows/docker.yml", %w[jobs * env RUBYGEMS_VERSION], rubygems_version.to_s.inspect
+sub_yaml ".github/workflows/test.yml", %w[jobs * strategy matrix rubygems 0 version], rubygems_version.to_s.inspect
+
+ruby_version = File.read(".ruby-version").strip
+
+%w[Dockerfile .devcontainer/Dockerfile].each do |f|
+ File.write(f, File.read(f).sub(/(RUBY_VERSION=)[\d.]+/, "\\1#{ruby_version}"))
+end
+
+sub_yaml ".github/workflows/docker.yml", %w[jobs * env RUBY_VERSION], ruby_version.inspect
+sub_yaml ".github/workflows/test.yml", %w[jobs * strategy matrix ruby_version 0], ruby_version.inspect
+sub_yaml ".github/workflows/test.yml", %w[jobs * strategy matrix include * ruby_version], ruby_version.inspect
+
+system("bundle", "update", "--bundler=#{bundler_version}", exception: true)
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
index ff04317430c..c7f25952c16 100644
--- a/test/application_system_test_case.rb
+++ b/test/application_system_test_case.rb
@@ -2,5 +2,16 @@
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
include OauthHelpers
- driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
+ include AvoHelpers
+
+ if ENV["CAPYBARA_SERVER_PORT"]
+ served_by host: "rails-app", port: ENV["CAPYBARA_SERVER_PORT"]
+
+ driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400], options: {
+ browser: :remote,
+ url: "http://#{ENV['SELENIUM_HOST']}:4444"
+ }
+ else
+ driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
+ end
end
diff --git a/test/components/previews/oidc/trusted_publisher/github_action/form_component_preview.rb b/test/components/previews/oidc/trusted_publisher/github_action/form_component_preview.rb
index c02271a38fe..c2df0ed77c0 100644
--- a/test/components/previews/oidc/trusted_publisher/github_action/form_component_preview.rb
+++ b/test/components/previews/oidc/trusted_publisher/github_action/form_component_preview.rb
@@ -9,11 +9,11 @@ def default(factory: :oidc_rubygem_trusted_publisher, environment: nil, reposito
class Wrapper < Phlex::HTML
include Phlex::Rails::Helpers::FormWith
- extend Dry::Initializer
- option :form_object
+ extend PropInitializer::Properties
+ prop :form_object
def view_template
- form_with(model: form_object, url: "/") do |github_action_form|
+ form_with(model: @form_object, url: "/") do |github_action_form|
render OIDC::TrustedPublisher::GitHubAction::FormComponent.new(github_action_form:)
github_action_form.submit class: "form__submit", disabled: true
end
diff --git a/test/factories/api_key.rb b/test/factories/api_key.rb
index e7c1fc38de8..e3083e644d2 100644
--- a/test/factories/api_key.rb
+++ b/test/factories/api_key.rb
@@ -1,7 +1,9 @@
FactoryBot.define do
factory :api_key do
- transient { key { "12345" } }
- transient { rubygem { nil } }
+ transient do
+ sequence(:key, &:to_s)
+ rubygem { nil }
+ end
owner factory: %i[user]
name { "ci-key" }
@@ -14,5 +16,10 @@
after(:build) do |api_key, evaluator|
api_key.rubygem_id = evaluator.rubygem.id if evaluator.rubygem
end
+
+ trait :trusted_publisher do
+ owner factory: %i[oidc_trusted_publisher_github_action]
+ transient { key { SecureRandom.hex(4) } }
+ end
end
end
diff --git a/test/factories/attestations.rb b/test/factories/attestations.rb
new file mode 100644
index 00000000000..aa7dafc36d0
--- /dev/null
+++ b/test/factories/attestations.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :attestation do
+ version
+ media_type { Sigstore::BundleType::BUNDLE_0_3.media_type }
+ body factory: %i[sigstore_bundle]
+ end
+end
diff --git a/test/factories/events/organization_events.rb b/test/factories/events/organization_events.rb
new file mode 100644
index 00000000000..09893b5a748
--- /dev/null
+++ b/test/factories/events/organization_events.rb
@@ -0,0 +1,8 @@
+FactoryBot.define do
+ factory :events_organization_event, class: "Events::OrganizationEvent" do
+ tag { "organization:created" }
+ organization
+ ip_address
+ additional { nil }
+ end
+end
diff --git a/test/factories/memberships.rb b/test/factories/memberships.rb
new file mode 100644
index 00000000000..0056afa4a28
--- /dev/null
+++ b/test/factories/memberships.rb
@@ -0,0 +1,20 @@
+FactoryBot.define do
+ factory :membership do
+ user
+ organization
+ confirmed_at { Time.zone.now }
+ role { :maintainer }
+
+ trait :maintainer do
+ role { :maintainer }
+ end
+
+ trait :owner do
+ role { :owner }
+ end
+
+ trait :admin do
+ role { :admin }
+ end
+ end
+end
diff --git a/test/factories/organization_onboarding_invites.rb b/test/factories/organization_onboarding_invites.rb
new file mode 100644
index 00000000000..73c7f4779b4
--- /dev/null
+++ b/test/factories/organization_onboarding_invites.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :organization_onboarding_invite do
+ organization_onboarding { association(:organization_onboarding) }
+ user { association(:user) }
+ role { "owner" }
+ end
+end
diff --git a/test/factories/organization_onboardings.rb b/test/factories/organization_onboardings.rb
new file mode 100644
index 00000000000..7de553f8d3b
--- /dev/null
+++ b/test/factories/organization_onboardings.rb
@@ -0,0 +1,51 @@
+FactoryBot.define do
+ factory :organization_onboarding do
+ transient do
+ approved_invites { [] }
+ authorizer { create(:user) } # rubocop:disable FactoryBot/FactoryAssociationWithStrategy
+ namesake_rubygem { create(:rubygem) } # rubocop:disable FactoryBot/FactoryAssociationWithStrategy
+ end
+
+ created_by { association(:user) }
+
+ organization_name { namesake_rubygem.name }
+ organization_handle { namesake_rubygem.name }
+
+ rubygems do
+ [namesake_rubygem.id]
+ end
+
+ after(:build) do |organization_onboarding, evaluator|
+ Ownership.find_or_create_by(
+ user: organization_onboarding.created_by,
+ rubygem: evaluator.namesake_rubygem,
+ authorizer: evaluator.authorizer,
+ role: "owner"
+ )
+ evaluator.approved_invites.each do |invitee|
+ organization_onboarding.invites.find_or_initialize_by(user: invitee[:user]).role = invitee[:role]
+ end
+ end
+
+ trait :completed do
+ status { :completed }
+ onboarded_at { Time.zone.now }
+ end
+
+ trait :failed do
+ error { "Failed to onboard" }
+ status { :failed }
+ end
+
+ trait :user do
+ name_type { "gem" } # temporarily set to gem during release when username is disabled
+
+ # organization_name { "Rubygems" }
+ # organization_handle { created_by.handle }
+
+ # rubygems do
+ # []
+ # end
+ end
+ end
+end
diff --git a/test/factories/organizations.rb b/test/factories/organizations.rb
new file mode 100644
index 00000000000..6a7697e236b
--- /dev/null
+++ b/test/factories/organizations.rb
@@ -0,0 +1,36 @@
+FactoryBot.define do
+ factory :organization do
+ transient do
+ owners { [] }
+ admins { [] }
+ maintainers { [] }
+ rubygems { [] }
+ end
+
+ handle
+ name
+ deleted_at { nil }
+
+ after(:create) do |organization, evaluator|
+ evaluator.owners.each do |user|
+ create(:membership, user: user, organization: organization, role: :owner)
+ end
+
+ evaluator.admins.each do |user|
+ create(:membership, user: user, organization: organization, role: :admin)
+ end
+
+ evaluator.maintainers.each do |user|
+ create(:membership, user: user, organization: organization, role: :maintainer)
+ end
+
+ evaluator.rubygems.each do |rubygem|
+ rubygem.update(organization: organization)
+ end
+ end
+
+ trait :with_members do
+ memberships { build_list(:membership, 2) }
+ end
+ end
+end
diff --git a/test/factories/ownership.rb b/test/factories/ownership.rb
index 196cb80382a..9931e2754e2 100644
--- a/test/factories/ownership.rb
+++ b/test/factories/ownership.rb
@@ -4,8 +4,14 @@
user
confirmed_at { Time.current }
authorizer { association :user }
+ role { :owner }
+
trait :unconfirmed do
confirmed_at { nil }
end
+
+ trait :maintainer do
+ role { :maintainer }
+ end
end
end
diff --git a/test/factories/rubygem.rb b/test/factories/rubygem.rb
index 78224af58d9..285e833a735 100644
--- a/test/factories/rubygem.rb
+++ b/test/factories/rubygem.rb
@@ -2,6 +2,7 @@
factory :rubygem do
transient do
owners { [] }
+ maintainers { [] }
number { nil }
downloads { 0 }
end
@@ -18,7 +19,11 @@
after(:create) do |rubygem, evaluator|
evaluator.owners.each do |owner|
- create(:ownership, rubygem: rubygem, user: owner)
+ create(:ownership, rubygem: rubygem, user: owner, role: :owner)
+ end
+
+ evaluator.maintainers.each do |maintainer|
+ create(:ownership, rubygem: rubygem, user: maintainer, role: :maintainer)
end
create(:version, rubygem: rubygem, number: evaluator.number) if evaluator.number
diff --git a/test/factories/sigstore.rb b/test/factories/sigstore.rb
new file mode 100644
index 00000000000..4bb3636866f
--- /dev/null
+++ b/test/factories/sigstore.rb
@@ -0,0 +1,36 @@
+FactoryBot.define do
+ factory :sigstore_x509_certificate, class: "Sigstore::Common::V1::X509Certificate" do
+ transient do
+ x509_certificate factory: %i[x509_certificate key_usage github_actions_fulcio]
+ end
+ initialize_with do
+ cert = new
+ cert.raw_bytes = x509_certificate.to_der
+ cert
+ end
+ to_create { |instance| instance }
+ end
+ factory :sigstore_verification_material, class: "Sigstore::Bundle::V1::VerificationMaterial" do
+ certificate factory: %i[sigstore_x509_certificate]
+ tlog_entries { [build(:sigstore_tlog_entry)] }
+ to_create { |instance| instance }
+ end
+ factory :sigstore_checkpoint, class: "Sigstore::Rekor::V1::Checkpoint" do
+ to_create { |instance| instance }
+ end
+ factory :sigstore_inclusion_proof, class: "Sigstore::Rekor::V1::InclusionProof" do
+ checkpoint factory: %i[sigstore_checkpoint]
+ to_create { |instance| instance }
+ end
+ factory :sigstore_tlog_entry, class: "Sigstore::Rekor::V1::TransparencyLogEntry" do
+ sequence(:log_index)
+ inclusion_proof factory: %i[sigstore_inclusion_proof]
+ to_create { |instance| instance }
+ end
+
+ factory :sigstore_bundle, class: "Sigstore::Bundle::V1::Bundle" do
+ media_type { Sigstore::BundleType::BUNDLE_0_3.media_type }
+ verification_material factory: %i[sigstore_verification_material]
+ to_create { |instance| instance }
+ end
+end
diff --git a/test/factories/user.rb b/test/factories/user.rb
index 8e7460601b5..29bd04cb235 100644
--- a/test/factories/user.rb
+++ b/test/factories/user.rb
@@ -47,5 +47,10 @@
mfa_level { User.mfa_levels["ui_and_gem_signin"] }
mfa_recovery_codes { %w[aaa bbb ccc] }
end
+
+ trait :blocked do
+ email { "security+locked-#{SecureRandom.hex(4)}@rubygems.org" }
+ blocked_email { "test@example.com" }
+ end
end
end
diff --git a/test/factories/x509.rb b/test/factories/x509.rb
new file mode 100644
index 00000000000..bdf3b452f3a
--- /dev/null
+++ b/test/factories/x509.rb
@@ -0,0 +1,78 @@
+FactoryBot.define do
+ factory :x509_certificate, class: "OpenSSL::X509::Certificate" do
+ subject { OpenSSL::X509::Name.parse("/DC=org/DC=example/CN=Test") }
+ issuer { subject }
+ version { 2 }
+ serial { 1 }
+ not_before { 1.day.ago }
+ not_after { 1.year.from_now }
+ public_key { OpenSSL::PKey::EC.generate("prime256v1") }
+ transient do
+ extension_factory { OpenSSL::X509::ExtensionFactory.new }
+ end
+
+ trait :key_usage do
+ after(:build) do |cert, ctx|
+ cert.add_extension(ctx.extension_factory.create_ext("keyUsage", "digitalSignature", true))
+ cert.add_extension(ctx.extension_factory.create_ext("2.5.29.37", "critical,DER:30:0A:06:08:2B:06:01:05:05:07:03:03"))
+ end
+ end
+
+ trait :github_actions_fulcio do
+ after(:build) do |cert, ctx|
+ {
+ "1.3.6.1.4.1.57264.1.1" =>
+ "https://token.actions.githubusercontent.com",
+ "1.3.6.1.4.1.57264.1.2" =>
+ "release",
+ "1.3.6.1.4.1.57264.1.3" =>
+ "f106999a2210a9a17b32b172f95518859a85ffed",
+ "1.3.6.1.4.1.57264.1.4" =>
+ "Release",
+ "1.3.6.1.4.1.57264.1.5" =>
+ "sigstore/sigstore-ruby",
+ "1.3.6.1.4.1.57264.1.6" =>
+ "refs/tags/v0.1.1",
+ "1.3.6.1.4.1.57264.1.8" =>
+ "https://token.actions.githubusercontent.com",
+ "1.3.6.1.4.1.57264.1.9" =>
+ ".Xhttps://github.com/sigstore/sigstore-ruby/.github/workflows/release.yml@refs/tags/v0.1.1",
+ "1.3.6.1.4.1.57264.1.10" =>
+ ".(f106999a2210a9a17b32b172f95518859a85ffed",
+ "1.3.6.1.4.1.57264.1.11" =>
+ ".githubHosted",
+ "1.3.6.1.4.1.57264.1.12" =>
+ ".)https://github.com/sigstore/sigstore-ruby",
+ "1.3.6.1.4.1.57264.1.13" =>
+ ".(f106999a2210a9a17b32b172f95518859a85ffed",
+ "1.3.6.1.4.1.57264.1.14" =>
+ "..refs/tags/v0.1.1",
+ "1.3.6.1.4.1.57264.1.15" =>
+ "..766398650",
+ "1.3.6.1.4.1.57264.1.16" =>
+ "..https://github.com/sigstore",
+ "1.3.6.1.4.1.57264.1.17" =>
+ "..71096353",
+ "1.3.6.1.4.1.57264.1.18" =>
+ ".Xhttps://github.com/sigstore/sigstore-ruby/.github/workflows/release.yml@refs/tags/v0.1.1",
+ "1.3.6.1.4.1.57264.1.19" =>
+ ".(f106999a2210a9a17b32b172f95518859a85ffed",
+ "1.3.6.1.4.1.57264.1.20" =>
+ "..release",
+ "1.3.6.1.4.1.57264.1.21" =>
+ ".Mhttps://github.com/sigstore/sigstore-ruby/actions/runs/11446323187/attempts/1",
+ "1.3.6.1.4.1.57264.1.22" =>
+ "..public"
+ }.each do |oid, value|
+ cert.add_extension(ctx.extension_factory.create_ext(oid, "ASN1:UTF8String:#{value}", false))
+ end
+ end
+ end
+
+ after(:build) do |cert, ctx|
+ cert.sign(ctx.public_key, OpenSSL::Digest.new("SHA256"))
+ end
+
+ to_create { |instance| instance }
+ end
+end
diff --git a/test/functional/api/v1/api_keys_controller_test.rb b/test/functional/api/v1/api_keys_controller_test.rb
index 62066783a9b..b0ba00d58e5 100644
--- a/test/functional/api/v1/api_keys_controller_test.rb
+++ b/test/functional/api/v1/api_keys_controller_test.rb
@@ -469,12 +469,7 @@ def self.should_expect_otp_for_update
should "deny access" do
assert_response :forbidden
- mfa_error = <<~ERROR.chomp
- [ERROR] For protection of your account and your gems, you are required to set up multi-factor authentication \
- at https://rubygems.org/totp/new.
-
- Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
- ERROR
+ mfa_error = I18n.t("multifactor_auths.api.mfa_required_not_yet_enabled").chomp
assert_match mfa_error, @response.body
end
@@ -488,12 +483,7 @@ def self.should_expect_otp_for_update
should "deny access" do
assert_response :forbidden
- mfa_error = <<~ERROR.chomp
- [ERROR] For protection of your account and your gems, you are required to change your MFA level to 'UI and gem signin' or 'UI and API' \
- at https://rubygems.org/settings/edit.
-
- Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
- ERROR
+ mfa_error = I18n.t("multifactor_auths.api.mfa_required_weak_level_enabled").chomp
assert_match mfa_error, @response.body
end
diff --git a/test/functional/api/v1/deletions_controller_test.rb b/test/functional/api/v1/deletions_controller_test.rb
index 2bd2d47a6e0..9be60bb1bea 100644
--- a/test/functional/api/v1/deletions_controller_test.rb
+++ b/test/functional/api/v1/deletions_controller_test.rb
@@ -189,12 +189,7 @@ class Api::V1::DeletionsControllerTest < ActionController::TestCase
should respond_with :forbidden
should "show error message" do
- mfa_error = <<~ERROR.chomp
- [ERROR] For protection of your account and your gems, you are required to set up multi-factor authentication \
- at https://rubygems.org/totp/new.
-
- Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
- ERROR
+ mfa_error = I18n.t("multifactor_auths.api.mfa_required_not_yet_enabled").chomp
assert_includes @response.body, mfa_error
end
@@ -209,12 +204,7 @@ class Api::V1::DeletionsControllerTest < ActionController::TestCase
should respond_with :forbidden
should "show error message" do
- mfa_error = <<~ERROR.chomp
- [ERROR] For protection of your account and your gems, you are required to change your MFA level to 'UI and gem signin' or 'UI and API' \
- at https://rubygems.org/settings/edit.
-
- Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
- ERROR
+ mfa_error = I18n.t("multifactor_auths.api.mfa_required_weak_level_enabled").chomp
assert_includes @response.body, mfa_error
end
@@ -290,12 +280,7 @@ class Api::V1::DeletionsControllerTest < ActionController::TestCase
delete :create, params: { gem_name: gem[:name], version: gem[:version] }
assert_response gem[:deletion_status]
- mfa_warning = <<~WARN.chomp
-
-
- [WARNING] For protection of your account and gems, we encourage you to set up multi-factor authentication \
- at https://rubygems.org/totp/new. Your account will be required to have MFA enabled in the future.
- WARN
+ mfa_warning = "\n\n#{I18n.t('multifactor_auths.api.mfa_recommended_not_yet_enabled')}".chomp
assert_includes @response.body, mfa_warning
end
@@ -312,13 +297,7 @@ class Api::V1::DeletionsControllerTest < ActionController::TestCase
delete :create, params: { gem_name: gem[:name], version: gem[:version] }
assert_response gem[:deletion_status]
- mfa_warning = <<~WARN.chomp
-
-
- [WARNING] For protection of your account and gems, we encourage you to change your multi-factor authentication \
- level to 'UI and gem signin' or 'UI and API' at https://rubygems.org/settings/edit. \
- Your account will be required to have MFA enabled on one of these levels in the future.
- WARN
+ mfa_warning = "\n\n#{I18n.t('multifactor_auths.api.mfa_recommended_weak_level_enabled')}".chomp
assert_includes @response.body, mfa_warning
end
@@ -336,9 +315,8 @@ class Api::V1::DeletionsControllerTest < ActionController::TestCase
delete :create, params: { gem_name: gem[:name], version: gem[:version] }
assert_response gem[:deletion_status]
- mfa_warning = "[WARNING] For protection of your account and gems"
-
- refute_includes @response.body, mfa_warning
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_not_yet_enabled").chomp
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_weak_level_enabled").chomp
end
end
end
@@ -354,9 +332,8 @@ class Api::V1::DeletionsControllerTest < ActionController::TestCase
delete :create, params: { gem_name: gem[:name], version: gem[:version] }
assert_response gem[:deletion_status]
- mfa_warning = "[WARNING] For protection of your account and gems"
-
- refute_includes @response.body, mfa_warning
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_not_yet_enabled").chomp
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_weak_level_enabled").chomp
end
end
end
@@ -607,7 +584,7 @@ class Api::V1::DeletionsControllerTest < ActionController::TestCase
should respond_with :forbidden
should "return body that starts with denied access message" do
- assert @response.body.start_with?("The API key doesn't have access")
+ assert_equal "This API key cannot perform the specified action on this gem.", @response.body
end
end
end
diff --git a/test/functional/api/v1/owners_controller_test.rb b/test/functional/api/v1/owners_controller_test.rb
index 6a2369bc2ca..e1164f85882 100644
--- a/test/functional/api/v1/owners_controller_test.rb
+++ b/test/functional/api/v1/owners_controller_test.rb
@@ -2,6 +2,7 @@
class Api::V1::OwnersControllerTest < ActionController::TestCase
include ActiveJob::TestHelper
+ include ActionMailer::TestHelper
def self.should_respond_to(format)
should "route GET show with #{format.to_s.upcase}" do
@@ -401,12 +402,8 @@ def self.should_respond_to(format)
post :create, params: { rubygem_id: @rubygem.slug, email: email }
assert_equal 403, @response.status
- mfa_error = <<~ERROR.chomp
- [ERROR] For protection of your account and your gems, you are required to set up multi-factor authentication \
- at https://rubygems.org/totp/new.
+ mfa_error = I18n.t("multifactor_auths.api.mfa_required_not_yet_enabled").chomp
- Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
- ERROR
assert_includes @response.body, mfa_error
end
end
@@ -422,12 +419,8 @@ def self.should_respond_to(format)
post :create, params: { rubygem_id: @rubygem.slug, email: email }
assert_equal 403, @response.status
- mfa_error = <<~ERROR.chomp
- [ERROR] For protection of your account and your gems, you are required to change your MFA level to 'UI and gem signin' or 'UI and API' \
- at https://rubygems.org/settings/edit.
+ mfa_error = I18n.t("multifactor_auths.api.mfa_required_weak_level_enabled").chomp
- Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
- ERROR
assert_includes @response.body, mfa_error
end
end
@@ -473,12 +466,7 @@ def self.should_respond_to(format)
should "include mfa setup warning" do
@emails.each do |email|
post :create, params: { rubygem_id: @rubygem.slug, email: email }
- mfa_warning = <<~WARN.chomp
-
-
- [WARNING] For protection of your account and gems, we encourage you to set up multi-factor authentication \
- at https://rubygems.org/totp/new. Your account will be required to have MFA enabled in the future.
- WARN
+ mfa_warning = "\n\n#{I18n.t('multifactor_auths.api.mfa_recommended_not_yet_enabled')}".chomp
assert_includes @response.body, mfa_warning
end
@@ -493,13 +481,7 @@ def self.should_respond_to(format)
should "include change mfa level warning" do
@emails.each do |email|
post :create, params: { rubygem_id: @rubygem.slug, email: email }
- mfa_warning = <<~WARN.chomp
-
-
- [WARNING] For protection of your account and gems, we encourage you to change your multi-factor authentication \
- level to 'UI and gem signin' or 'UI and API' at https://rubygems.org/settings/edit. \
- Your account will be required to have MFA enabled on one of these levels in the future.
- WARN
+ mfa_warning = "\n\n#{I18n.t('multifactor_auths.api.mfa_recommended_weak_level_enabled')}".chomp
assert_includes @response.body, mfa_warning
end
@@ -514,9 +496,9 @@ def self.should_respond_to(format)
should "not include MFA warnings" do
@emails.each do |email|
post :create, params: { rubygem_id: @rubygem.slug, email: email }
- mfa_warning = "[WARNING] For protection of your account and gems"
- refute_includes @response.body, mfa_warning
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_not_yet_enabled").chomp
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_weak_level_enabled").chomp
end
end
end
@@ -530,13 +512,46 @@ def self.should_respond_to(format)
should "not include mfa warnings" do
@emails.each do |email|
post :create, params: { rubygem_id: @rubygem.slug, email: email }
- mfa_warning = "[WARNING] For protection of your account and gems"
- refute_includes @response.body, mfa_warning
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_not_yet_enabled").chomp
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_weak_level_enabled").chomp
end
end
end
end
+
+ context "when not supplying a role" do
+ should "set a default role" do
+ post :create, params: { rubygem_id: @rubygem.slug, email: @second_user.display_id }
+
+ assert_equal 200, @response.status
+ assert_predicate Ownership.find_by(user: @second_user, rubygem: @rubygem), :owner?
+ end
+ end
+
+ context "given a role" do
+ should "set the role for the given user" do
+ post :create, params: { rubygem_id: @rubygem.slug, email: @second_user.display_id, role: :maintainer }
+
+ assert_equal 200, @response.status
+ assert_predicate Ownership.find_by(user: @second_user, rubygem: @rubygem), :maintainer?
+ end
+ end
+
+ context "when given an invalid role" do
+ should "raise an error" do
+ post :create, params: { rubygem_id: @rubygem.slug, email: @second_user.display_id, role: :invalid }
+
+ assert_equal 422, @response.status
+ assert_equal "Role is not included in the list", @response.body
+ end
+
+ should "not create the ownership" do
+ post :create, params: { rubygem_id: @rubygem.slug, email: @second_user.email, role: :invalid }
+
+ assert_nil @rubygem.ownerships.find_by(user: @second_user)
+ end
+ end
end
context "without add owner api key scope" do
@@ -550,8 +565,8 @@ def self.should_respond_to(format)
should respond_with :forbidden
- should "return body that starts with denied access message" do
- assert @response.body.start_with?("The API key doesn't have access")
+ should "return body with denied access message" do
+ assert_equal "This API key cannot perform the specified action on this gem.", @response.body
end
end
end
@@ -784,12 +799,8 @@ def self.should_respond_to(format)
delete :destroy, params: { rubygem_id: @rubygem.slug, email: email }
assert_equal 403, response.status
- mfa_error = <<~ERROR.chomp
- [ERROR] For protection of your account and your gems, you are required to set up multi-factor authentication \
- at https://rubygems.org/totp/new.
+ mfa_error = I18n.t("multifactor_auths.api.mfa_required_not_yet_enabled").chomp
- Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
- ERROR
assert_includes @response.body, mfa_error
end
end
@@ -805,12 +816,8 @@ def self.should_respond_to(format)
delete :destroy, params: { rubygem_id: @rubygem.slug, email: email }
assert_equal 403, @response.status
- mfa_error = <<~ERROR.chomp
- [ERROR] For protection of your account and your gems, you are required to change your MFA level to 'UI and gem signin' or 'UI and API' \
- at https://rubygems.org/settings/edit.
+ mfa_error = I18n.t("multifactor_auths.api.mfa_required_weak_level_enabled").chomp
- Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
- ERROR
assert_includes @response.body, mfa_error
end
end
@@ -856,12 +863,7 @@ def self.should_respond_to(format)
should "include mfa setup warning" do
@emails.each do |email|
delete :destroy, params: { rubygem_id: @rubygem.slug, email: email }
- mfa_warning = <<~WARN.chomp
-
-
- [WARNING] For protection of your account and gems, we encourage you to set up multi-factor authentication \
- at https://rubygems.org/totp/new. Your account will be required to have MFA enabled in the future.
- WARN
+ mfa_warning = "\n\n#{I18n.t('multifactor_auths.api.mfa_recommended_not_yet_enabled')}".chomp
assert_includes @response.body, mfa_warning
end
@@ -876,13 +878,7 @@ def self.should_respond_to(format)
should "include change mfa level warning" do
@emails.each do |email|
delete :destroy, params: { rubygem_id: @rubygem.slug, email: email }
- mfa_warning = <<~WARN.chomp
-
-
- [WARNING] For protection of your account and gems, we encourage you to change your multi-factor authentication \
- level to 'UI and gem signin' or 'UI and API' at https://rubygems.org/settings/edit. \
- Your account will be required to have MFA enabled on one of these levels in the future.
- WARN
+ mfa_warning = "\n\n#{I18n.t('multifactor_auths.api.mfa_recommended_weak_level_enabled')}".chomp
assert_includes @response.body, mfa_warning
end
@@ -897,9 +893,9 @@ def self.should_respond_to(format)
should "not include mfa warnings" do
@emails.each do |email|
delete :destroy, params: { rubygem_id: @rubygem.slug, email: email }
- mfa_warning = "[WARNING] For protection of your account and gems"
- refute_includes @response.body, mfa_warning
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_not_yet_enabled").chomp
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_weak_level_enabled").chomp
end
end
end
@@ -913,9 +909,9 @@ def self.should_respond_to(format)
should "not include mfa warnings" do
@emails.each do |email|
delete :destroy, params: { rubygem_id: @rubygem.slug, email: email }
- mfa_warning = "[WARNING] For protection of your account and gems"
- refute_includes @response.body, mfa_warning
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_not_yet_enabled").chomp
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_weak_level_enabled").chomp
end
end
end
@@ -933,8 +929,8 @@ def self.should_respond_to(format)
should respond_with :forbidden
- should "return body that starts with denied access message" do
- assert @response.body.start_with?("The API key doesn't have access")
+ should "return body that has the denied access message" do
+ assert_equal "This API key cannot perform the specified action on this gem.", @response.body
end
end
end
@@ -965,4 +961,80 @@ def self.should_respond_to(format)
assert_equal "This rubygem could not be found.", @response.body
end
+
+ should "route PUT /api/v1/gems/rubygem/owners.yaml" do
+ route = { controller: "api/v1/owners",
+ action: "update",
+ rubygem_id: "rails",
+ format: "yaml" }
+
+ assert_recognizes(route, path: "/api/v1/gems/rails/owners.yaml", method: :put)
+ end
+
+ context "on PATCH to owner gem" do
+ setup do
+ @owner = create(:user)
+ @maintainer = create(:user)
+ @rubygem = create(:rubygem, owners: [@owner])
+
+ @api_key = create(:api_key, key: "12223", scopes: %i[update_owner], owner: @owner, rubygem: @rubygem)
+ @request.env["HTTP_AUTHORIZATION"] = "12223"
+ end
+
+ should "set the maintainer to a lower access level" do
+ ownership = create(:ownership, user: @maintainer, rubygem: @rubygem, role: :owner)
+
+ patch :update, params: { rubygem_id: @rubygem.slug, email: @maintainer.email, role: :maintainer }
+
+ assert_response :success
+ assert_predicate ownership.reload, :maintainer?
+ assert_enqueued_email_with OwnersMailer, :owner_updated, params: { ownership: ownership }
+ end
+
+ context "when the current user is changing their own role" do
+ should "forbid changing the role" do
+ patch :update, params: { rubygem_id: @rubygem.slug, email: @owner.email, role: :maintainer }
+
+ ownership = @rubygem.ownerships.find_by(user: @owner)
+
+ assert_response :forbidden
+ assert_predicate ownership.reload, :owner?
+ end
+ end
+
+ context "when the role is invalid" do
+ should "return a bad request response with the error message" do
+ ownership = create(:ownership, user: @maintainer, rubygem: @rubygem, role: :maintainer)
+
+ patch :update, params: { rubygem_id: @rubygem.slug, email: @maintainer.email, role: :invalid }
+
+ assert_response :unprocessable_entity
+ assert_equal "Role is not included in the list", @response.body
+ assert_predicate ownership.reload, :maintainer?
+ end
+ end
+
+ context "when the owner is not found" do
+ context "when the update is authorized" do
+ should "return a not found response" do
+ patch :update, params: { rubygem_id: @rubygem.slug, email: "notauser", role: :owner }
+
+ assert_response :not_found
+ assert_equal "Owner could not be found.", @response.body
+ end
+ end
+
+ context "when the update is not authorized" do
+ should "return a forbidden response" do
+ @api_key = create(:api_key, key: "99999", scopes: %i[push_rubygem], owner: @owner)
+ @request.env["HTTP_AUTHORIZATION"] = "99999"
+
+ patch :update, params: { rubygem_id: @rubygem.slug, email: "notauser", role: :owner }
+
+ assert_response :forbidden
+ assert_equal "This API key cannot perform the specified action on this gem.", @response.body
+ end
+ end
+ end
+ end
end
diff --git a/test/functional/api/v1/profiles_controller_test.rb b/test/functional/api/v1/profiles_controller_test.rb
index 401046e4113..b39f0d32969 100644
--- a/test/functional/api/v1/profiles_controller_test.rb
+++ b/test/functional/api/v1/profiles_controller_test.rb
@@ -27,15 +27,6 @@ def assert_mfa_info_included(mfa_level)
assert_match mfa_level, @response.body
end
- def assert_warning_included(expected_warning)
- assert response_body.key?("warning")
- assert_match expected_warning, response_body["warning"].to_s
- end
-
- def refute_warning_included(expected_warning)
- refute_match expected_warning, response_body["warning"].to_s
- end
-
def refute_mfa_info_included(mfa_level)
refute response_body.key?("mfa")
refute_match mfa_level, @response.body
@@ -101,11 +92,9 @@ def refute_mfa_info_included(mfa_level)
context "when mfa is disabled" do
should "include warning" do
- expected_warning =
- "For protection of your account and gems, we encourage you to set up multi-factor authentication " \
- "at https://rubygems.org/totp/new. Your account will be required to have MFA enabled in the future."
+ expected_warning = I18n.t("multifactor_auths.api.mfa_recommended_not_yet_enabled").chomp
- assert_warning_included(expected_warning)
+ assert_includes response_body["warning"].to_s, expected_warning
end
end
@@ -117,12 +106,9 @@ def refute_mfa_info_included(mfa_level)
end
should "include warning" do
- expected_warning =
- "For protection of your account and gems, we encourage you to change your multi-factor authentication " \
- "level to 'UI and gem signin' or 'UI and API' at https://rubygems.org/settings/edit. " \
- "Your account will be required to have MFA enabled on one of these levels in the future."
+ expected_warning = I18n.t("multifactor_auths.api.mfa_recommended_weak_level_enabled").chomp
- assert_warning_included(expected_warning)
+ assert_includes response_body["warning"].to_s, expected_warning
end
end
@@ -133,10 +119,9 @@ def refute_mfa_info_included(mfa_level)
end
should "not include warning in user json" do
- unexpected_warning =
- "For protection of your account and gems"
+ unexpected_warning = "For protection of your account and gems"
- refute_warning_included(unexpected_warning)
+ refute_includes response_body["warning"].to_s, unexpected_warning
end
end
@@ -147,10 +132,9 @@ def refute_mfa_info_included(mfa_level)
end
should "not include warning" do
- unexpected_warning =
- "For protection of your account and gems"
+ unexpected_warning = "For protection of your account and gems"
- refute_warning_included(unexpected_warning)
+ refute_includes response_body["warning"].to_s, unexpected_warning
end
end
end
diff --git a/test/functional/api/v1/rubygems_controller_test.rb b/test/functional/api/v1/rubygems_controller_test.rb
index 8c644632e2e..cf60b05c4c4 100644
--- a/test/functional/api/v1/rubygems_controller_test.rb
+++ b/test/functional/api/v1/rubygems_controller_test.rb
@@ -560,12 +560,7 @@ def self.should_respond_to(format)
should respond_with :forbidden
should "show error message" do
- mfa_error = <<~ERROR.chomp
- [ERROR] For protection of your account and your gems, you are required to set up multi-factor authentication \
- at https://rubygems.org/totp/new.
-
- Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
- ERROR
+ mfa_error = I18n.t("multifactor_auths.api.mfa_required_not_yet_enabled").chomp
assert_includes @response.body, mfa_error
end
@@ -580,12 +575,7 @@ def self.should_respond_to(format)
should respond_with :forbidden
should "show error message" do
- mfa_error = <<~ERROR.chomp
- [ERROR] For protection of your account and your gems, you are required to change your MFA level to 'UI and gem signin' or 'UI and API' \
- at https://rubygems.org/settings/edit.
-
- Please read our blog post for more details (https://blog.rubygems.org/2022/08/15/requiring-mfa-on-popular-gems.html).
- ERROR
+ mfa_error = I18n.t("multifactor_auths.api.mfa_required_weak_level_enabled").chomp
assert_includes @response.body, mfa_error
end
@@ -600,7 +590,8 @@ def self.should_respond_to(format)
should respond_with :success
should "not show error message" do
- refute_includes @response.body, "For protection of your account and your gems"
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_required_not_yet_enabled").chomp
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_required_weak_level_enabled").chomp
end
end
@@ -614,7 +605,8 @@ def self.should_respond_to(format)
should respond_with :success
should "not show error message" do
- refute_includes @response.body, "For protection of your account and your gems"
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_required_not_yet_enabled").chomp
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_required_weak_level_enabled").chomp
end
end
end
@@ -630,14 +622,7 @@ def self.should_respond_to(format)
end
should "include mfa setup warning" do
- mfa_warning = <<~WARN.chomp
-
-
- [WARNING] For protection of your account and gems, we encourage you to set up multi-factor authentication \
- at https://rubygems.org/totp/new. Your account will be required to have MFA enabled in the future.
- WARN
-
- assert_includes @response.body, mfa_warning
+ assert_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_not_yet_enabled").chomp
end
end
@@ -648,15 +633,7 @@ def self.should_respond_to(format)
end
should "include change mfa level warning" do
- mfa_warning = <<~WARN.chomp
-
-
- [WARNING] For protection of your account and gems, we encourage you to change your multi-factor authentication \
- level to 'UI and gem signin' or 'UI and API' at https://rubygems.org/settings/edit. \
- Your account will be required to have MFA enabled on one of these levels in the future.
- WARN
-
- assert_includes @response.body, mfa_warning
+ assert_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_weak_level_enabled").chomp
end
end
@@ -669,9 +646,8 @@ def self.should_respond_to(format)
should respond_with :success
should "not include mfa warning" do
- mfa_warning = "[WARNING] For protection of your account and gems"
-
- refute_includes @response.body, mfa_warning
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_not_yet_enabled").chomp
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_weak_level_enabled").chomp
end
end
@@ -684,9 +660,8 @@ def self.should_respond_to(format)
should respond_with :success
should "not include mfa warning" do
- mfa_warning = "[WARNING] For protection of your account and gems"
-
- refute_includes @response.body, mfa_warning
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_not_yet_enabled").chomp
+ refute_includes @response.body, I18n.t("multifactor_auths.api.mfa_recommended_weak_level_enabled").chomp
end
end
end
@@ -786,8 +761,8 @@ def self.should_respond_to(format)
end
should respond_with :forbidden
- should "return body that starts with denied access message" do
- assert @response.body.start_with?("The API key doesn't have access")
+ should "return body that includes the denied access message" do
+ assert_includes @response.body, "This API key cannot perform the specified action on this gem."
end
end
end
diff --git a/test/functional/api/v1/timeframe_versions_controller_test.rb b/test/functional/api/v1/timeframe_versions_controller_test.rb
index b5953d74fdd..44f1d968546 100644
--- a/test/functional/api/v1/timeframe_versions_controller_test.rb
+++ b/test/functional/api/v1/timeframe_versions_controller_test.rb
@@ -52,6 +52,16 @@ class Api::V1::TimeframeVersionsControllerTest < ActionController::TestCase
assert_includes response.body, "iso8601"
end
+ should 'return a bad request with message when "to" is not primitive' do
+ get :index, format: :json, params: {
+ from: Time.zone.parse("2017-11-09").iso8601,
+ to: ["2017-11-12"]
+ }
+
+ assert_equal 400, response.status
+ assert_includes response.body, "iso8601"
+ end
+
should 'return a bad request with message when "from" is invalid' do
get :index, format: :json, params: {
from: "2017-11-09",
diff --git a/test/functional/api/v1/web_hooks_controller_test.rb b/test/functional/api/v1/web_hooks_controller_test.rb
index 583c16a3b08..306aed704b3 100644
--- a/test/functional/api/v1/web_hooks_controller_test.rb
+++ b/test/functional/api/v1/web_hooks_controller_test.rb
@@ -9,6 +9,10 @@ def self.should_not_find_it
end
end
+ setup do
+ NotifyWebHookJob.any_instance.stubs(:sleep)
+ end
+
context "with incorrect api key" do
context "no api key" do
should "forbid access when creating a web hook" do
@@ -83,7 +87,18 @@ def self.should_respond_to(format)
context "On POST to fire for all gems" do
setup do
- stub_request(:post, @url)
+ stub_request(:post, "https://api.hookrelay.dev/hooks///webhook_id-fire")
+ .with(headers: {
+ "Content-Type" => "application/json",
+ "HR_TARGET_URL" => @url,
+ "HR_MAX_ATTEMPTS" => "1"
+ }).to_return(status: 200, body: '{"id":"delivery-id"}', headers: { "Content-Type" => "application/json" })
+
+ stub_request(:get, "https://app.hookrelay.dev/api/v1/accounts//hooks//deliveries/delivery-id")
+ .to_return(status: 200, body: { "status" => "success", "responses" => [
+ { "code" => 200, "body" => "OK", "headers" => { "Content-Type" => "text/plain" } }
+ ] }.to_json, headers: { "Content-Type" => "application/json" })
+
post :fire, params: { gem_name: WebHook::GLOBAL_PATTERN,
url: @url }
end
@@ -98,7 +113,17 @@ def self.should_respond_to(format)
context "On POST to fire for all gems that fails" do
setup do
- stub_request(:post, @url).to_timeout
+ stub_request(:post, "https://api.hookrelay.dev/hooks///webhook_id-fire")
+ .with(headers: {
+ "Content-Type" => "application/json",
+ "HR_TARGET_URL" => @url,
+ "HR_MAX_ATTEMPTS" => "1"
+ }).to_return(status: 200, body: '{"id":"delivery-id"}', headers: { "Content-Type" => "application/json" })
+
+ stub_request(:get, "https://app.hookrelay.dev/api/v1/accounts//hooks//deliveries/delivery-id")
+ .to_return(status: 200, body: { "status" => "failure",
+"failure_reason" => "timed out" }.to_json, headers: { "Content-Type" => "application/json" })
+
post :fire, params: { gem_name: WebHook::GLOBAL_PATTERN,
url: @url }
end
@@ -132,7 +157,18 @@ def self.should_respond_to(format)
context "On POST to fire for a specific gem" do
setup do
- stub_request(:post, @url)
+ stub_request(:post, "https://api.hookrelay.dev/hooks///webhook_id-fire")
+ .with(headers: {
+ "Content-Type" => "application/json",
+ "HR_TARGET_URL" => @url,
+ "HR_MAX_ATTEMPTS" => "1"
+ }).to_return(status: 200, body: '{"id":"delivery-id"}', headers: { "Content-Type" => "application/json" })
+
+ stub_request(:get, "https://app.hookrelay.dev/api/v1/accounts//hooks//deliveries/delivery-id")
+ .to_return(status: 200, body: { "status" => "success", "responses" => [
+ { "code" => 200, "body" => "OK", "headers" => { "Content-Type" => "text/plain" } }
+ ] }.to_json, headers: { "Content-Type" => "application/json" })
+
post :fire, params: { gem_name: @rubygem.name,
url: @url }
end
@@ -145,7 +181,17 @@ def self.should_respond_to(format)
context "On POST to fire for a specific gem that fails" do
setup do
- stub_request(:post, @url).to_return(status: 404)
+ stub_request(:post, "https://api.hookrelay.dev/hooks///webhook_id-fire")
+ .with(headers: {
+ "Content-Type" => "application/json",
+ "HR_TARGET_URL" => @url,
+ "HR_MAX_ATTEMPTS" => "1"
+ }).to_return(status: 200, body: '{"id":"delivery-id"}', headers: { "Content-Type" => "application/json" })
+
+ stub_request(:get, "https://app.hookrelay.dev/api/v1/accounts//hooks//deliveries/delivery-id")
+ .to_return(status: 200, body: { "status" => "failure",
+"failure_reason" => "exceeded", "responses" => [{ "code" => 404 }] }.to_json, headers: { "Content-Type" => "application/json" })
+
post :fire, params: { gem_name: @rubygem.name,
url: @url }
end
diff --git a/test/functional/api_keys_controller_test.rb b/test/functional/api_keys_controller_test.rb
index b4c50f3e425..087bed7af79 100644
--- a/test/functional/api_keys_controller_test.rb
+++ b/test/functional/api_keys_controller_test.rb
@@ -223,7 +223,7 @@ class ApiKeysControllerTest < ActionController::TestCase
end
should "show error to user" do
- page.assert_text "Show dashboard scope must be enabled exclusively"
+ assert_text "Show dashboard scope must be enabled exclusively"
end
should "not update scope of test key" do
diff --git a/test/functional/owners_controller_test.rb b/test/functional/owners_controller_test.rb
index e2877971228..7cbc4e78dfd 100644
--- a/test/functional/owners_controller_test.rb
+++ b/test/functional/owners_controller_test.rb
@@ -39,11 +39,8 @@ class OwnersControllerTest < ActionController::TestCase
get :index, params: { rubygem_id: @rubygem.name }
end
- should respond_with :forbidden
-
- should "render forbidden message" do
- assert page.has_content?("forbidden")
- end
+ should redirect_to("gem info page") { rubygem_path(@rubygem.slug) }
+ should set_flash[:alert].to "Forbidden"
end
end
@@ -52,7 +49,7 @@ class OwnersControllerTest < ActionController::TestCase
context "with invalid handle" do
setup do
perform_enqueued_jobs only: ActionMailer::MailDeliveryJob do
- post :create, params: { handle: "no_user", rubygem_id: @rubygem.name }
+ post :create, params: { handle: "no_user", rubygem_id: @rubygem.name, role: :owner }
end
end
@@ -72,7 +69,7 @@ class OwnersControllerTest < ActionController::TestCase
context "with valid handle" do
setup do
@new_owner = create(:user)
- post :create, params: { handle: @new_owner.display_id, rubygem_id: @rubygem.name }
+ post :create, params: { handle: @new_owner.display_id, rubygem_id: @rubygem.name, role: :owner }
end
should redirect_to("ownerships index") { rubygem_owners_path(@rubygem.slug) }
@@ -132,7 +129,7 @@ class OwnersControllerTest < ActionController::TestCase
context "owner has enabled mfa" do
setup do
@user.enable_totp!(ROTP::Base32.random_base32, :ui_and_api)
- post :create, params: { handle: @new_owner.display_id, rubygem_id: @rubygem.name }
+ post :create, params: { handle: @new_owner.display_id, rubygem_id: @rubygem.name, role: :owner }
end
should redirect_to("ownerships index") { rubygem_owners_path(@rubygem.slug) }
@@ -144,6 +141,19 @@ class OwnersControllerTest < ActionController::TestCase
assert_equal expected_notice, flash[:notice]
end
end
+
+ context "with invalid role" do
+ setup do
+ @user.enable_totp!(ROTP::Base32.random_base32, :ui_and_api)
+ post :create, params: { handle: @new_owner.display_id, rubygem_id: @rubygem.name, role: :invalid }
+ end
+
+ should render_template :index
+
+ should "set alert notice flash" do
+ assert_equal "Role is not included in the list", flash[:alert]
+ end
+ end
end
end
@@ -154,7 +164,8 @@ class OwnersControllerTest < ActionController::TestCase
post :create, params: { handle: @other_user.display_id, rubygem_id: @rubygem.name }
end
- should respond_with :forbidden
+ should redirect_to("gem info page") { rubygem_path(@rubygem.slug) }
+ should set_flash[:alert].to "Forbidden"
should "not add other user as owner" do
refute_includes @rubygem.owners_including_unconfirmed, @other_user
@@ -179,6 +190,7 @@ class OwnersControllerTest < ActionController::TestCase
delete :destroy, params: { rubygem_id: @rubygem.name, handle: @second_user.display_id }
end
end
+
should redirect_to("ownership index") { rubygem_owners_path(@rubygem.slug) }
should "remove the ownership record" do
@@ -218,15 +230,10 @@ class OwnersControllerTest < ActionController::TestCase
delete :destroy, params: { rubygem_id: @rubygem.name, handle: @last_owner.display_id }
end
end
- should respond_with :forbidden
+ should set_flash.now[:alert].to "Can't remove the only owner of the gem"
should "not remove the ownership record" do
assert_includes @rubygem.owners_including_unconfirmed, @last_owner
- end
- should "should flash error" do
- assert_equal "Can't remove the only owner of the gem", flash[:alert]
- end
- should "not send email notifications about owner removal" do
assert_emails 0
end
end
@@ -280,7 +287,7 @@ class OwnersControllerTest < ActionController::TestCase
delete :destroy, params: { rubygem_id: @rubygem.name, handle: @last_owner.display_id }
end
- should respond_with :forbidden
+ should redirect_to("gem info page") { rubygem_path(@rubygem.slug) }
should "not remove user as owner" do
assert_includes @rubygem.owners, @last_owner
@@ -344,6 +351,113 @@ class OwnersControllerTest < ActionController::TestCase
end
end
end
+
+ context "on GET edit ownership" do
+ setup do
+ @owner = create(:user)
+ @maintainer = create(:user)
+ @rubygem = create(:rubygem, owners: [@owner, @maintainer])
+
+ verified_sign_in_as(@owner)
+ end
+
+ context "when editing another owner's role" do
+ setup do
+ get :edit, params: { rubygem_id: @rubygem.name, handle: @maintainer.display_id }
+ end
+
+ should respond_with :success
+ should render_template :edit
+ end
+
+ context "when editing your own role" do
+ setup do
+ get :edit, params: { rubygem_id: @rubygem.name, handle: @owner.display_id }
+ end
+
+ should redirect_to("gem info page") { rubygem_path(@rubygem.slug) }
+ should set_flash[:alert].to "Can't update your own Role"
+ end
+ end
+
+ context "on PATCH to update ownership" do
+ setup do
+ @owner = create(:user)
+ @maintainer = create(:user)
+ @rubygem = create(:rubygem, owners: [@owner, @maintainer])
+
+ verified_sign_in_as(@owner)
+ patch :update, params: { rubygem_id: @rubygem.name, handle: @maintainer.display_id, role: :maintainer }
+ end
+
+ should redirect_to("rubygem show") { rubygem_owners_path(@rubygem.slug) }
+
+ should "set success notice flash" do
+ assert_equal "#{@maintainer.name} was succesfully updated.", flash[:notice]
+ end
+
+ should "downgrade the ownership to a maintainer role" do
+ ownership = Ownership.find_by(rubygem: @rubygem, user: @maintainer)
+
+ assert_predicate ownership, :maintainer?
+ assert_enqueued_email_with OwnersMailer, :owner_updated, params: { ownership: ownership, authorizer: @owner }
+ end
+ end
+
+ context "when updating ownership without role" do
+ setup do
+ @owner = create(:user)
+ @maintainer = create(:user)
+ @rubygem = create(:rubygem, owners: [@owner, @maintainer])
+
+ verified_sign_in_as(@owner)
+ patch :update, params: { rubygem_id: @rubygem.name, handle: @maintainer.display_id }
+ end
+
+ should redirect_to("ownerships index") { rubygem_owners_path(@rubygem.slug) }
+
+ should "not update the role" do
+ ownership = Ownership.find_by(rubygem: @rubygem, user: @maintainer)
+
+ assert_predicate ownership, :owner?
+ end
+ end
+
+ context "when updating ownership with invalid role" do
+ setup do
+ @owner = create(:user)
+ @maintainer = create(:user)
+ @rubygem = create(:rubygem, owners: [@owner, @maintainer])
+
+ verified_sign_in_as(@owner)
+ patch :update, params: { rubygem_id: @rubygem.name, handle: @maintainer.display_id, role: :invalid }
+ end
+
+ should respond_with :unprocessable_content
+
+ should "set error flash message" do
+ assert_equal "Role is not included in the list", flash[:alert]
+ end
+ end
+
+ context "when updating the role of currently signed in user" do
+ setup do
+ @owner = create(:user)
+ @rubygem = create(:rubygem)
+ @ownership = create(:ownership, user: @owner, rubygem: @rubygem, role: :owner)
+
+ verified_sign_in_as(@owner)
+ patch :update, params: { rubygem_id: @rubygem.name, handle: @owner.display_id, role: :maintainer }
+ end
+
+ should "not update the ownership of the current user" do
+ assert_predicate @ownership.reload, :owner?
+ end
+
+ should "set notice flash message" do
+ assert_equal "Can't update your own Role", flash[:alert]
+ end
+ end
end
context "when logged in and unverified" do
@@ -366,7 +480,7 @@ class OwnersControllerTest < ActionController::TestCase
context "on POST to create ownership" do
setup do
@new_owner = create(:user)
- post :create, params: { handle: @new_owner.display_id, rubygem_id: @rubygem.name }
+ post :create, params: { handle: @new_owner.display_id, rubygem_id: @rubygem.name, role: :owner }
end
should redirect_to("sessions#verify") { verify_session_path }
@@ -386,9 +500,31 @@ class OwnersControllerTest < ActionController::TestCase
should redirect_to("sessions#verify") { verify_session_path }
should use_before_action(:redirect_to_verify)
- should "remove the ownership record" do
- assert_includes @rubygem.owners_including_unconfirmed, @second_user
+ should "not remove the ownership record" do
+ assert_includes @rubygem.owners, @second_user
+ end
+ end
+
+ context "on GET to edit" do
+ setup do
+ @second_user = create(:user)
+ @ownership = create(:ownership, :unconfirmed, rubygem: @rubygem, user: @second_user)
+ get :edit, params: { rubygem_id: @rubygem.name, handle: @second_user.display_id }
+ end
+
+ should redirect_to("sessions#verify") { verify_session_path }
+ should use_before_action(:redirect_to_verify)
+ end
+
+ context "on PATCH to update" do
+ setup do
+ @second_user = create(:user)
+ @ownership = create(:ownership, :unconfirmed, rubygem: @rubygem, user: @second_user, role: :owner)
+ patch :update, params: { rubygem_id: @rubygem.name, handle: @second_user.display_id, role: :maintainer }
end
+
+ should redirect_to("sessions#verify") { verify_session_path }
+ should use_before_action(:redirect_to_verify)
end
end
@@ -485,5 +621,27 @@ class OwnersControllerTest < ActionController::TestCase
assert redirect_to("sign in") { sign_in_path }
end
end
+
+ context "on EDIT to update owner" do
+ setup do
+ create(:ownership, rubygem: @rubygem, user: @user)
+ get :edit, params: { rubygem_id: @rubygem.name, handle: @user.display_id }
+ end
+
+ should "redirect to sign in path" do
+ assert redirect_to("sign in") { sign_in_path }
+ end
+ end
+
+ context "on PATCH to update owner" do
+ setup do
+ create(:ownership, rubygem: @rubygem, user: @user)
+ patch :update, params: { rubygem_id: @rubygem.name, handle: @user.display_id, role: :owner }
+ end
+
+ should "redirect to sign in path" do
+ assert redirect_to("sign in") { sign_in_path }
+ end
+ end
end
end
diff --git a/test/functional/ownership_calls_controller_test.rb b/test/functional/ownership_calls_controller_test.rb
index ec7d02b4aa5..e53d1e3e5d1 100644
--- a/test/functional/ownership_calls_controller_test.rb
+++ b/test/functional/ownership_calls_controller_test.rb
@@ -16,7 +16,17 @@ class OwnershipCallsControllerTest < ActionController::TestCase
@rubygem = create(:rubygem, owners: [@user], number: "1.0.0")
end
- context "user is owner of rubygem" do
+ context "user is owner of rubygem and verified" do
+ setup do
+ session[:verification] = 10.minutes.from_now
+ session[:verified_user] = @user.id
+ end
+
+ teardown do
+ session[:verification] = nil
+ session[:verified_user] = nil
+ end
+
context "with correct params" do
setup do
post :create, params: { rubygem_id: @rubygem.name, note: "short note" }
@@ -64,12 +74,28 @@ class OwnershipCallsControllerTest < ActionController::TestCase
end
end
+ context "user is owner and not verified" do
+ setup do
+ post :create, params: { rubygem_id: @rubygem.name, note: "short note" }
+ end
+
+ should redirect_to("verify page") { verify_session_path }
+ end
+
context "user is not owner of rubygem" do
setup do
- user = create(:user)
- sign_in_as(user)
+ @user = create(:user)
+ sign_in_as(@user)
+ session[:verification] = 10.minutes.from_now
+ session[:verified_user] = @user.id
post :create, params: { rubygem_id: @rubygem.name, note: "short note" }
end
+
+ teardown do
+ session[:verification] = nil
+ session[:verified_user] = nil
+ end
+
should respond_with :forbidden
should "not create a call" do
@@ -83,7 +109,17 @@ class OwnershipCallsControllerTest < ActionController::TestCase
@rubygem = create(:rubygem, owners: [@user], number: "1.0.0")
end
- context "user is owner of rubygem" do
+ context "user is owner of rubygem and verified" do
+ setup do
+ session[:verification] = 10.minutes.from_now
+ session[:verified_user] = @user.id
+ end
+
+ teardown do
+ session[:verification] = nil
+ session[:verified_user] = nil
+ end
+
context "ownership call exists" do
setup do
create(:ownership_call, rubygem: @rubygem, user: @user, status: "opened")
@@ -112,13 +148,31 @@ class OwnershipCallsControllerTest < ActionController::TestCase
end
end
+ context "user is owner and not verified" do
+ setup do
+ create(:ownership_call, rubygem: @rubygem, user: @user)
+ patch :close, params: { rubygem_id: @rubygem.name }
+ end
+
+ should redirect_to("verify page") { verify_session_path }
+ end
+
context "user is not owner of rubygem" do
setup do
- user = create(:user)
- sign_in_as(user)
+ @user = create(:user)
+ sign_in_as(@user)
+ session[:verification] = 10.minutes.from_now
+ session[:verified_user] = @user.id
+
create(:ownership_call, rubygem: @rubygem, user: @user)
patch :close, params: { rubygem_id: @rubygem.name }
end
+
+ teardown do
+ session[:verification] = nil
+ session[:verified_user] = nil
+ end
+
should respond_with :forbidden
should "not update status to close" do
@@ -214,6 +268,13 @@ class OwnershipCallsControllerTest < ActionController::TestCase
context "user has MFA set to strong level, expect normal behaviour" do
setup do
@user.enable_totp!(ROTP::Base32.random_base32, :ui_and_api)
+ session[:verification] = 10.minutes.from_now
+ session[:verified_user] = @user.id
+ end
+
+ teardown do
+ session[:verification] = nil
+ session[:verified_user] = nil
end
context "on GET to index" do
diff --git a/test/functional/ownership_requests_controller_test.rb b/test/functional/ownership_requests_controller_test.rb
index 980a93328d2..2a29093de1d 100644
--- a/test/functional/ownership_requests_controller_test.rb
+++ b/test/functional/ownership_requests_controller_test.rb
@@ -20,6 +20,7 @@ class OwnershipRequestsControllerTest < ActionController::TestCase
create(:ownership, user: @user, rubygem: @rubygem)
post :create, params: { rubygem_id: @rubygem.name, note: "small note" }
end
+
should respond_with :forbidden
should "not create ownership request" do
@@ -58,14 +59,17 @@ class OwnershipRequestsControllerTest < ActionController::TestCase
@rubygem = create(:rubygem, downloads: 2_000)
create(:version, rubygem: @rubygem, created_at: 2.years.ago, number: "1.0.0")
end
+
context "when user is owner" do
setup do
create(:ownership, user: @user, rubygem: @rubygem)
post :create, params: { rubygem_id: @rubygem.name, note: "small note" }
end
- should respond_with :forbidden
- should "not create ownership request" do
+ should redirect_to("adoptions index") { rubygem_adoptions_path(@rubygem.slug) }
+ should set_flash[:alert].to("User is already an owner")
+
+ should "not create ownership call" do
assert_nil @rubygem.ownership_requests.find_by(user: @user)
end
end
@@ -76,11 +80,8 @@ class OwnershipRequestsControllerTest < ActionController::TestCase
post :create, params: { rubygem_id: @rubygem.name, note: "small note" }
end
should redirect_to("adoptions index") { rubygem_adoptions_path(@rubygem.slug) }
- should "set success notice flash" do
- expected_notice = "Your ownership request was submitted."
+ should set_flash[:notice].to("Your ownership request was submitted.")
- assert_equal expected_notice, flash[:notice]
- end
should "create ownership request" do
assert_not_nil @rubygem.ownership_requests.find_by(user: @user)
end
@@ -90,11 +91,8 @@ class OwnershipRequestsControllerTest < ActionController::TestCase
post :create, params: { rubygem_id: @rubygem.name }
end
should redirect_to("adoptions index") { rubygem_adoptions_path(@rubygem.slug) }
- should "set error alert flash" do
- expected_notice = "Note can't be blank"
+ should set_flash[:alert].to("Note can't be blank")
- assert_equal expected_notice, flash[:alert]
- end
should "not create ownership call" do
assert_nil @rubygem.ownership_requests.find_by(user: @user)
end
@@ -105,11 +103,7 @@ class OwnershipRequestsControllerTest < ActionController::TestCase
post :create, params: { rubygem_id: @rubygem.name, note: "new note" }
end
should redirect_to("adoptions index") { rubygem_adoptions_path(@rubygem.slug) }
- should "set error alert flash" do
- expected_notice = "User has already been taken"
-
- assert_equal expected_notice, flash[:alert]
- end
+ should set_flash[:alert].to("User has already requested ownership")
end
end
end
@@ -120,10 +114,18 @@ class OwnershipRequestsControllerTest < ActionController::TestCase
@rubygem = create(:rubygem, downloads: 2_000_000)
create(:version, rubygem: @rubygem, created_at: 2.years.ago, number: "1.0.0")
end
- context "when user is owner" do
+ context "when user is owner and verified" do
setup do
create(:ownership, user: @user, rubygem: @rubygem)
+ session[:verification] = 10.minutes.from_now
+ session[:verified_user] = @user.id
+ end
+
+ teardown do
+ session[:verification] = nil
+ session[:verified_user] = nil
end
+
context "on close" do
setup do
@requester = create(:user)
@@ -193,12 +195,29 @@ class OwnershipRequestsControllerTest < ActionController::TestCase
end
end
+ context "when user is owner and not verified" do
+ setup do
+ create(:ownership, user: @user, rubygem: @rubygem)
+ @requester = create(:user)
+ ownership_request = create(:ownership_request, rubygem: @rubygem, user: @requester)
+ patch :update, params: { rubygem_id: @rubygem.name, id: ownership_request.id, status: "close" }
+ end
+ should redirect_to("verify page") { verify_session_path }
+ end
+
context "when user is not an owner" do
setup do
request = create(:ownership_request, rubygem: @rubygem)
+ session[:verification] = 10.minutes.from_now
+ session[:verified_user] = @user.id
patch :update, params: { rubygem_id: @rubygem.name, id: request.id, status: "close" }
end
+ teardown do
+ session[:verification] = nil
+ session[:verified_user] = nil
+ end
+
should respond_with :forbidden
end
end
@@ -208,10 +227,17 @@ class OwnershipRequestsControllerTest < ActionController::TestCase
@rubygem = create(:rubygem, downloads: 2_000_000)
create(:version, rubygem: @rubygem, created_at: 2.years.ago, number: "1.0.0")
end
- context "when user is owner" do
+ context "when user is owner and verified" do
setup do
create(:ownership, rubygem: @rubygem, user: @user)
create_list(:ownership_request, 3, rubygem: @rubygem)
+ session[:verification] = 10.minutes.from_now
+ session[:verified_user] = @user.id
+ end
+
+ teardown do
+ session[:verification] = nil
+ session[:verified_user] = nil
end
context "with successful update" do
@@ -244,11 +270,20 @@ class OwnershipRequestsControllerTest < ActionController::TestCase
end
end
+ context "when user is owner and not verified" do
+ setup do
+ create(:ownership, rubygem: @rubygem, user: @user)
+ patch :close_all, params: { rubygem_id: @rubygem.name }
+ end
+ should redirect_to("verify page") { verify_session_path }
+ end
+
context "user is not owner" do
setup do
create_list(:ownership_request, 3, rubygem: @rubygem)
patch :close_all, params: { rubygem_id: @rubygem.name }
end
+
should respond_with :forbidden
should "not close all open requests" do
@@ -363,7 +398,15 @@ class OwnershipRequestsControllerTest < ActionController::TestCase
context "user has MFA set to strong level, expect normal behaviour" do
setup do
@user.enable_totp!(ROTP::Base32.random_base32, :ui_and_api)
+ session[:verification] = 10.minutes.from_now
+ session[:verified_user] = @user.id
+ end
+
+ teardown do
+ session[:verification] = nil
+ session[:verified_user] = nil
end
+
context "POST to create" do
setup { post :create, params: { rubygem_id: @rubygem.name, note: "small note" } }
diff --git a/test/functional/passwords_controller_test.rb b/test/functional/passwords_controller_test.rb
index be178e1b6b1..9e7ae33ac0f 100644
--- a/test/functional/passwords_controller_test.rb
+++ b/test/functional/passwords_controller_test.rb
@@ -33,39 +33,93 @@ class PasswordsControllerTest < ActionController::TestCase
get :edit, params: { token: "invalidtoken" }
end
- should redirect_to("the home page") { root_path }
+ should redirect_to("the sign in page") { sign_in_path }
+ should set_flash[:alert].to "Please double check the URL or try submitting a new password reset."
should "not sign in the user" do
refute_predicate @controller.request.env[:clearance], :signed_in?
end
-
- should "warn about invalid url" do
- assert_equal "Please double check the URL or try submitting a new password reset.", flash[:alert]
- end
end
context "with valid confirmation_token" do
- setup do
- get :edit, params: { token: @user.confirmation_token }
- end
+ context "when not signed in" do
+ setup do
+ get :edit, params: { token: @user.confirmation_token }
+ end
- should respond_with :success
+ should respond_with :success
- should "sign in the user" do
- assert_predicate @controller.request.env[:clearance], :signed_in?
- end
+ should "not sign in the user" do
+ refute_predicate @controller.request.env[:clearance], :signed_in?
+ end
- should "invalidate the confirmation_token" do
- assert_nil @user.reload.confirmation_token
+ should "invalidate the confirmation_token" do
+ assert_nil @user.reload.confirmation_token
+ end
+
+ should "display edit form" do
+ assert_text "Reset password"
+ assert_selector "input[type=password][autocomplete=new-password]"
+ end
+
+ should "instruct the browser not to send referrer that contains the token" do
+ assert_equal "no-referrer", response.headers["Referrer-Policy"]
+ end
end
- should "display edit form" do
- page.assert_text("Reset password")
- page.assert_selector("input[type=password][autocomplete=new-password]")
+ context "when signed in as the user" do
+ setup do
+ sign_in_as @user
+
+ get :edit, params: { token: @user.confirmation_token }
+ end
+
+ should respond_with :success
+
+ should "leave the user signed in" do
+ assert_predicate @controller.request.env[:clearance], :signed_in?
+ end
+
+ should "invalidate the confirmation_token" do
+ assert_nil @user.reload.confirmation_token
+ end
+
+ should "display edit form" do
+ assert_text "Reset password"
+ assert_selector "input[type=password][autocomplete=new-password]"
+ end
+
+ should "instruct the browser not to send referrer that contains the token" do
+ assert_equal "no-referrer", response.headers["Referrer-Policy"]
+ end
end
- should "instruct the browser not to send referrer that contains the token" do
- assert_equal "no-referrer", response.headers["Referrer-Policy"]
+ context "when signed in as another user" do
+ setup do
+ @other_user = create(:user, api_key: "otheruserkey")
+ sign_in_as @other_user
+
+ get :edit, params: { token: @user.confirmation_token }
+ end
+
+ should respond_with :success
+
+ should "sign the current user out" do
+ refute_predicate @controller.request.env[:clearance], :signed_in?
+ end
+
+ should "invalidate the confirmation_token" do
+ assert_nil @user.reload.confirmation_token
+ end
+
+ should "display edit form" do
+ assert_text "Reset password"
+ assert_selector "input[type=password][autocomplete=new-password]"
+ end
+
+ should "instruct the browser not to send referrer that contains the token" do
+ assert_equal "no-referrer", response.headers["Referrer-Policy"]
+ end
end
end
@@ -75,15 +129,12 @@ class PasswordsControllerTest < ActionController::TestCase
get :edit, params: { token: @user.confirmation_token }
end
- should redirect_to("the home page") { root_path }
+ should redirect_to("the sign in page") { sign_in_path }
+ should set_flash[:alert].to "Please double check the URL or try submitting a new password reset."
should "not sign in the user" do
refute_predicate @controller.request.env[:clearance], :signed_in?
end
-
- should "warn about invalid url" do
- assert_equal "Please double check the URL or try submitting a new password reset.", flash[:alert]
- end
end
context "with totp enabled" do
@@ -190,8 +241,8 @@ class PasswordsControllerTest < ActionController::TestCase
should respond_with :success
- should "sign in the user" do
- assert_predicate @controller.request.env[:clearance], :signed_in?
+ should "not sign in the user" do
+ refute_predicate @controller.request.env[:clearance], :signed_in?
end
should "invalidate the confirmation_token" do
@@ -199,7 +250,7 @@ class PasswordsControllerTest < ActionController::TestCase
end
should "display edit form" do
- page.assert_text("Reset password")
+ assert_text "Reset password"
end
should "clear mfa_expires_at" do
@@ -214,14 +265,11 @@ class PasswordsControllerTest < ActionController::TestCase
end
should respond_with :unauthorized
+ should set_flash.now[:alert].to "Your OTP code is incorrect."
should "not sign in the user" do
refute_predicate @controller.request.env[:clearance], :signed_in?
end
-
- should "alert about otp being incorrect" do
- assert_equal "Your OTP code is incorrect.", flash[:alert]
- end
end
context "when the OTP session is expired" do
@@ -232,17 +280,12 @@ class PasswordsControllerTest < ActionController::TestCase
end
end
- should set_flash.now[:alert]
- should respond_with :unauthorized
+ should set_flash[:alert].to "Your login page session has expired."
+ should redirect_to("the sign in page") { sign_in_path }
should "clear mfa_expires_at" do
assert_nil @controller.session[:mfa_expires_at]
end
-
- should "render sign in page" do
- page.assert_text "Sign in"
- end
-
should "not sign in the user" do
refute_predicate @controller.request.env[:clearance], :signed_in?
end
@@ -282,8 +325,8 @@ class PasswordsControllerTest < ActionController::TestCase
should respond_with :success
- should "sign in the user" do
- assert_predicate @controller.request.env[:clearance], :signed_in?
+ should "not sign in the user" do
+ refute_predicate @controller.request.env[:clearance], :signed_in?
end
should "invalidate the confirmation_token" do
@@ -291,7 +334,7 @@ class PasswordsControllerTest < ActionController::TestCase
end
should "display edit form" do
- page.assert_text("Reset password")
+ assert_text "Reset password"
end
should "clear mfa_expires_at" do
@@ -304,15 +347,12 @@ class PasswordsControllerTest < ActionController::TestCase
post(:webauthn_edit, params: { token: "badtoken" })
end
- should redirect_to("the home page") { root_path }
+ should redirect_to("the sign in page") { sign_in_path }
+ should set_flash[:alert].to "Please double check the URL or try submitting a new password reset."
should "not sign in the user" do
refute_predicate @controller.request.env[:clearance], :signed_in?
end
-
- should "warn about invalid url" do
- assert_equal "Please double check the URL or try submitting a new password reset.", flash[:alert]
- end
end
context "when not providing credentials" do
@@ -388,17 +428,13 @@ class PasswordsControllerTest < ActionController::TestCase
end
end
- should respond_with :unauthorized
- should set_flash.now[:alert]
+ should redirect_to("the sign in page") { sign_in_path }
+ should set_flash[:alert]
should "clear mfa_expires_at" do
assert_nil @controller.session[:mfa_expires_at]
end
- should "render sign in page" do
- page.assert_text "Sign in"
- end
-
should "not sign in the user" do
refute_predicate @controller.request.env[:clearance], :signed_in?
end
@@ -413,7 +449,7 @@ class PasswordsControllerTest < ActionController::TestCase
@old_encrypted_password = @user.encrypted_password
end
- context "when not signed in" do
+ context "when not verified for password reset" do
setup do
put :update, params: {
password_reset: { reset_api_key: "true", reset_api_keys: "true", password: PasswordHelpers::SECURE_TEST_PASSWORD }
@@ -433,57 +469,15 @@ class PasswordsControllerTest < ActionController::TestCase
end
end
- context "when signed in as another user" do
+ context "when not verified for password reset" do
setup do
- @other_user = create(:user, api_key: "otheruserkey")
- @other_api_key = @other_user.api_key
- @other_new_api_key = create(:api_key, owner: @other_user, key: "rubygems_otheruserkey")
- @other_old_encrypted_password = @other_user.encrypted_password
- sign_in_as @other_user
- session[:verification] = 10.minutes.from_now
- session[:verified_user] = @other_user.id
-
- put :update, params: {
- password_reset: { reset_api_key: "true", reset_api_keys: "true", password: PasswordHelpers::SECURE_TEST_PASSWORD }
- }
- end
-
- teardown do
- session[:verification] = nil
- session[:verified_user] = nil
- end
-
- should redirect_to("the dashboard") { dashboard_path }
-
- should "not change the signed in user's api_key" do
- assert_equal(@user.reload.api_key, @api_key)
- end
- should "not change the sign in user's password" do
- assert_equal(@user.reload.encrypted_password, @old_encrypted_password)
- end
-
- # The password controller does not care who is signed in.
- should "change logged in user's api_key" do
- refute_equal(@other_user.reload.api_key, @other_api_key)
- end
- should "change logged in user's password" do
- refute_equal(@other_user.reload.encrypted_password, @other_old_encrypted_password)
- end
- should "expire logged in user's new api key" do
- assert_empty @other_user.reload.api_keys.unexpired
- refute_empty @other_user.reload.api_keys.expired
- end
- end
-
- context "when signed in but not verified" do
- setup do
- sign_in_as @user
put :update, params: {
password_reset: { password: PasswordHelpers::SECURE_TEST_PASSWORD }
}
end
- should redirect_to("the verification page") { verify_session_path }
+ should redirect_to("the sign in page") { sign_in_path }
+ should set_flash[:alert].to "Please double check the URL or try submitting a new password reset."
should "not change api_key" do
assert_equal(@user.reload.api_key, @api_key)
@@ -496,13 +490,7 @@ class PasswordsControllerTest < ActionController::TestCase
context "when signed in" do
setup do
sign_in_as @user
- session[:verification] = 10.minutes.from_now
- session[:verified_user] = @user.id
- end
-
- teardown do
- session[:verification] = nil
- session[:verified_user] = nil
+ get :edit, params: { token: @user.confirmation_token }
end
context "with invalid password" do
@@ -513,6 +501,7 @@ class PasswordsControllerTest < ActionController::TestCase
end
should respond_with :success
+ should set_flash.now[:alert].to "Your password could not be changed. Please try again."
should "not change api_key" do
assert_equal(@user.reload.api_key, @api_key)
@@ -520,9 +509,6 @@ class PasswordsControllerTest < ActionController::TestCase
should "not change password" do
assert_equal(@user.reload.encrypted_password, @old_encrypted_password)
end
- should "alert about invalid password" do
- assert_equal "Your password could not be changed. Please try again.", flash[:alert]
- end
end
context "with a valid password" do
@@ -536,7 +522,7 @@ class PasswordsControllerTest < ActionController::TestCase
end
should set_flash[:alert]
- should redirect_to("the verification page") { verify_session_path }
+ should redirect_to("the sign in page") { sign_in_path }
should "not sign the user out" do
assert_predicate @controller.request.env[:clearance], :signed_in?
diff --git a/test/functional/profiles_controller_test.rb b/test/functional/profiles_controller_test.rb
index 876d60a8381..1cfbb165ec8 100644
--- a/test/functional/profiles_controller_test.rb
+++ b/test/functional/profiles_controller_test.rb
@@ -106,8 +106,8 @@ class ProfilesControllerTest < ActionController::TestCase
should respond_with :success
should "render user delete page" do
- page.assert_text "Delete profile"
- page.assert_selector "input[type=password][autocomplete=current-password]"
+ assert_text "Delete profile"
+ assert_selector "input[type=password][autocomplete=current-password]"
end
end
diff --git a/test/functional/rubygems_controller_test.rb b/test/functional/rubygems_controller_test.rb
index 0bf2ff07e61..68c698ac0e1 100644
--- a/test/functional/rubygems_controller_test.rb
+++ b/test/functional/rubygems_controller_test.rb
@@ -86,9 +86,9 @@ class RubygemsControllerTest < ActionController::TestCase
should respond_with :success
should "include the security events" do
- page.assert_text "Owner Added"
- page.assert_text "Owner Confirmed"
- page.assert_text "Owner Removed"
+ assert_text "Owner Added"
+ assert_text "Owner Confirmed"
+ assert_text "Owner Removed"
end
end
end
diff --git a/test/functional/searches_controller_test.rb b/test/functional/searches_controller_test.rb
index 63a7b2dd576..050f11b5236 100644
--- a/test/functional/searches_controller_test.rb
+++ b/test/functional/searches_controller_test.rb
@@ -70,19 +70,19 @@ class SearchesControllerTest < ActionController::TestCase
should respond_with :success
should "see sinatra on the page in the results" do
- page.assert_text(@sinatra.name)
- page.assert_selector("a[href='#{rubygem_path(@sinatra.slug)}']")
+ assert_text @sinatra.name
+ assert_selector "a[href='#{rubygem_path(@sinatra.slug)}']"
end
should "not see brando on the page in the results" do
- page.assert_no_text(@brando.name)
- page.assert_no_selector("a[href='#{rubygem_path(@brando.slug)}']")
+ refute_text @brando.name
+ refute_selector "a[href='#{rubygem_path(@brando.slug)}']"
end
should "display pagination summary" do
- page.assert_text("all 2 gems")
+ assert page.has_text?("all 2 gems")
end
should "not see suggestions" do
- page.assert_no_text("Did you mean")
- page.assert_no_selector(".search-suggestions")
+ refute_text "Did you mean"
+ refute_selector ".search-suggestions"
end
end
@@ -112,19 +112,19 @@ class SearchesControllerTest < ActionController::TestCase
should respond_with :success
should "see sinatra on the page in the suggestions" do
- page.assert_text("Did you mean")
- assert page.find(".search__suggestions").has_content?(@sinatra.name)
- assert page.has_selector?("a[href='#{search_path(query: @sinatra.name)}']")
+ assert_text "Did you mean"
+ assert_text @sinatra.name, page.find(".search__suggestions")
+ assert_selector "a[href='#{search_path(query: @sinatra.name)}']"
end
should "not see sinatra on the page in the results" do
- page.assert_no_selector("a[href='#{rubygem_path(@sinatra.slug)}']")
+ refute_selector "a[href='#{rubygem_path(@sinatra.slug)}']"
end
should "not see brando on the page in the results" do
- page.assert_no_text(@brando.name)
- page.assert_no_selector("a[href='#{rubygem_path(@brando.slug)}']")
+ refute_text @brando.name
+ refute_selector "a[href='#{rubygem_path(@brando.slug)}']"
end
should "not see filters" do
- page.assert_no_text("Filter")
+ refute_text "Filter"
end
end
@@ -141,10 +141,10 @@ class SearchesControllerTest < ActionController::TestCase
should respond_with :success
should "see sinatra_redux on the page in the results" do
- page.assert_selector("a[href='#{rubygem_path(@sinatra_redux.slug)}']")
+ assert_selector "a[href='#{rubygem_path(@sinatra_redux.slug)}']"
end
should "not see sinatra on the page in the results" do
- page.assert_no_selector("a[href='#{rubygem_path(@sinatra.slug)}']")
+ refute_selector "a[href='#{rubygem_path(@sinatra.slug)}']"
end
end
diff --git a/test/functional/sessions_controller_test.rb b/test/functional/sessions_controller_test.rb
index 7ef1f39db51..b617beb7842 100644
--- a/test/functional/sessions_controller_test.rb
+++ b/test/functional/sessions_controller_test.rb
@@ -474,8 +474,8 @@ class SessionsControllerTest < ActionController::TestCase
end
should "render sign-in form" do
- page.assert_text("Sign in")
- page.assert_selector("input[type=password][autocomplete=current-password]")
+ assert_text "Sign in"
+ assert_selector "input[type=password][autocomplete=current-password]"
end
end
diff --git a/test/functional/stats_controller_test.rb b/test/functional/stats_controller_test.rb
index e3de08a451f..84768c6adf8 100644
--- a/test/functional/stats_controller_test.rb
+++ b/test/functional/stats_controller_test.rb
@@ -58,12 +58,13 @@ class StatsControllerTest < ActionController::TestCase
end
should "not have width greater than 100%" do
- assert_select ".stats__graph__gem__meter" do |element|
- element.pluck(:style).each do |width|
- width =~ /width: (\d+[,.]\d+)%/
+ assert page.has_selector?(".stats__graph__gem__meter")
- assert_operator Regexp.last_match(1).to_f, :<=, 100, "#{Regexp.last_match(1)} is greater than 100"
- end
+ page.find_all(".stats__graph__gem__meter").each do |element|
+ assert element["data-stats-width-value"]
+ width = element["data-stats-width-value"].to_f
+
+ assert_operator width, :<=, 100, "#{width} is greater than 100"
end
end
end
diff --git a/test/functional/subscriptions_controller_test.rb b/test/functional/subscriptions_controller_test.rb
index d0ce9a69bc3..6407699103d 100644
--- a/test/functional/subscriptions_controller_test.rb
+++ b/test/functional/subscriptions_controller_test.rb
@@ -7,6 +7,31 @@ class SubscriptionsControllerTest < ActionController::TestCase
sign_in_as(@user)
end
+ context "on GET to index when the user is not subscribed to any gems" do
+ setup do
+ get :index
+ end
+
+ should respond_with :success
+
+ should "render 'no subscriptions' message" do
+ assert page.has_content?("You're not subscribed to any gems yet.")
+ end
+ end
+
+ context "on GET to index when the user is subscribed" do
+ setup do
+ create(:subscription, rubygem: @rubygem, user: @user)
+ get :index
+ end
+
+ should respond_with :success
+
+ should "show the gem name" do
+ assert page.has_content?(@rubygem.name)
+ end
+ end
+
context "On POST to create for a gem that the user is not subscribed to" do
setup do
post :create, params: { rubygem_id: @rubygem.slug }
diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb
index c038a29023a..0173d72abb3 100644
--- a/test/functional/users_controller_test.rb
+++ b/test/functional/users_controller_test.rb
@@ -11,8 +11,8 @@ class UsersControllerTest < ActionController::TestCase
should render_template(:new)
should "render the new user form" do
- page.assert_text "Sign up"
- page.assert_selector "input[type=password][autocomplete=new-password]"
+ assert_text "Sign up"
+ assert_selector "input[type=password][autocomplete=new-password]"
end
context "when logged in" do
diff --git a/test/functional/versions_controller_test.rb b/test/functional/versions_controller_test.rb
index fcdbca198a3..6b6a4ddec8a 100644
--- a/test/functional/versions_controller_test.rb
+++ b/test/functional/versions_controller_test.rb
@@ -107,7 +107,7 @@ class VersionsControllerTest < ActionController::TestCase
The date displayed was specified by the author in the gemspec.
NOTICE
- assert_select ".gem__version__date", text: "- January 01, 2000*", count: 1 do |elements|
+ assert_select ".gem__version__date", text: "January 01, 2000*", count: 1 do |elements|
version = elements.first
assert_equal(tooltip_text, version["data-tooltip"])
diff --git a/test/gems/sigstore-1.0.0.gem b/test/gems/sigstore-1.0.0.gem
new file mode 100644
index 00000000000..5e669016dc3
Binary files /dev/null and b/test/gems/sigstore-1.0.0.gem differ
diff --git a/test/gems/sigstore-1.0.0.gem.sigstore.json b/test/gems/sigstore-1.0.0.gem.sigstore.json
new file mode 100644
index 00000000000..657949847a3
--- /dev/null
+++ b/test/gems/sigstore-1.0.0.gem.sigstore.json
@@ -0,0 +1 @@
+{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"MIIIMzCCB7mgAwIBAgIUPP5IhTe2WtPWZQRUj2XjDqiiTyEwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwODE5MjIxMDQxWhcNMjQwODE5MjIyMDQxWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ0RRhDNQ5iLcioTLcjjBbw8yiFEatf9mkBEFtvNotLsZ7YukcrRlns+BwlOhSMQcjhP+/JDULnyRAP3LDRPShqOCBtgwggbUMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU8nymsHXqKdr2Gpdc5nX09mR72zMwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wgaUGA1UdEQEB/wSBmjCBl4aBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOQYKKwYBBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAfBgorBgEEAYO/MAECBBF3b3JrZmxvd19kaXNwYXRjaDA2BgorBgEEAYO/MAEDBCgwZjc1OTkyZGM3NTU5NTBiMjA4MTE1ZDZmMjkwYzg3YTcxYTNhZWRlMC0GCisGAQQBg78wAQQEH0V4dHJlbWVseSBkYW5nZXJvdXMgT0lEQyBiZWFjb24wSQYKKwYBBAGDvzABBQQ7c2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24wHQYKKwYBBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTCBpgYKKwYBBAGDvzABCQSBlwyBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABCgQqDCgwZjc1OTkyZGM3NTU5NTBiMjA4MTE1ZDZmMjkwYzg3YTcxYTNhZWRlMB0GCisGAQQBg78wAQsEDwwNZ2l0aHViLWhvc3RlZDBeBgorBgEEAYO/MAEMBFAMTmh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbjA4BgorBgEEAYO/MAENBCoMKDBmNzU5OTJkYzc1NTk1MGIyMDgxMTVkNmYyOTBjODdhNzFhM2FlZGUwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21haW4wGQYKKwYBBAGDvzABDwQLDAk2MzI1OTY4OTcwNwYKKwYBBAGDvzABEAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UwGQYKKwYBBAGDvzABEQQLDAkxMzE4MDQ1NjMwgaYGCisGAQQBg78wARIEgZcMgZRodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24vLmdpdGh1Yi93b3JrZmxvd3MvZXh0cmVtZWx5LWRhbmdlcm91cy1vaWRjLWJlYWNvbi55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoMGY3NTk5MmRjNzU1OTUwYjIwODExNWQ2ZjI5MGM4N2E3MWEzYWVkZTAhBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rpc3BhdGNoMIGCBgorBgEEAYO/MAEVBHQMcmh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJlYWNvbi9hY3Rpb25zL3J1bnMvMTA0NjE2NzgwMzUvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABkWyxQh4AAAQDAEgwRgIhAKo2Keb5q4OZIwh4iKDTde96+2BqMs4mBKjEU+6upNHNAiEA5wQHYBqAf9zEx8BbjbjEfg4hZP/s+NxxQI3dSPs7pngwCgYIKoZIzj0EAwMDaAAwZQIxAJLtd6ybF8WBnJlXW+4bvzKVnshOTh9Neoo/w8pldDrQHskIGqXRpmww6Xn9/Bij7QIwGLgbt6+ZXxnG0PFCTWMc+ZWjnnQkO+l9Y1gA6d6qi0OpJhkrBHEV8aTLqkPEkOb/"},"tlogEntries":[{"logIndex":"122963775","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"hashedrekord","version":"0.0.1"},"integratedTime":"1724105442","inclusionPromise":{"signedEntryTimestamp":"MEQCICgb3uAmDGlq7s+nkAcZ8fD0owzTwVymrzdFMT8d3LCVAiBsFMaj3rfpJzH3rngwalmkASo/bVj8hfWZTEBn9YornQ=="},"inclusionProof":{"logIndex":"1059513","rootHash":"/putP9z3Wss1HCFfDnWmpoa5i8l6WSZeW8XjIgpesm0=","treeSize":"1059514","hashes":["wnH/WECZ5+6nW4nogb4SIPHovLLVBkjJS6L2IUhh+5A=","Q8AmXrXnAEx+KtY/ftlNpoZHIDteiU093akegCKg9hg=","ARspm9MPppfljtu65QhY4zbFe+hgM9sh8BKUa9S42n0=","b8SrajRKbl/sbAkAtv6QLZ2Nq0a5yXAutxBUYCh3UZE=","6WaHP8iph/o/IEco+nWaj+Zb6EgGQbmz8QJ/QpujBCc=","PGV88p37MXO7aoOk8agunetUKPT+id/bPhQB5DR6bKo=","5ttUjdynHsMkxOw13D7urPJiKtAOMg9iZ2c6h7Gp0G4=","UORwhDZHpmRr46aYc0xce9Ibvxq2skbMMMOLkyhLlkQ=","HLt0TJCVl85VuVuUGesK0DTeWljDvsbKPLIsB2GNt+c="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n1059514\n/putP9z3Wss1HCFfDnWmpoa5i8l6WSZeW8XjIgpesm0=\n\n— rekor.sigstore.dev wNI9ajBFAiAVGjyS7gu+/F744y3qmjS32jfsakwaJXcIokIVVOU+wQIhALQHbZh7RDoV2uXsBK18uClNWH/FdlgxS7RNtdAg33s/\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3NzMzOTE1OWMzNzdkYjM2YjkyMTg1MjFiZGQxOTNkMjFlMWI4NmIzZGJjZDdhZjUzYjkwNGZiNWY0ZDRhYjU0In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURvVnZnalNXV2JNVTNJaVEyR0hvMC9DS1R1Vms5UnBnL1laUWZONjl4N0NBSWhBTHg2UmFxTVZ1T2VYYUMyMnF5LzV0NE42OGp3MDZMOERPbUJqSEJDakZseiIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVbE5la05EUWpkdFowRjNTVUpCWjBsVlVGQTFTV2hVWlRKWGRGQlhXbEZTVldveVdHcEVjV2xwVkhsRmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDlFUlRWTmFrbDRUVVJSZUZkb1kwNU5hbEYzVDBSRk5VMXFTWGxOUkZGNFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZLTUZKU2FFUk9VVFZwVEdOcGIxUk1ZMnBxUW1KM09IbHBSa1ZoZEdZNWJXdENSVVlLZEhaT2IzUk1jMW8zV1hWclkzSlNiRzV6SzBKM2JFOW9VMDFSWTJwb1VDc3ZTa1JWVEc1NVVrRlFNMHhFVWxCVGFIRlBRMEowWjNkbloySlZUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlU0Ym5sdENuTklXSEZMWkhJeVIzQmtZelZ1V0RBNWJWSTNNbnBOZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDJkaFZVZEJNVlZrUlZGRlFpOTNVMEp0YWtOQ2JEUmhRbXhIYURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWHBoVjJSNlpFYzVlUXBhVXpGcVlqSTFiV0l6U25SWlZ6VnFXbE01YkdWSVVubGFWekZzWWtocmRGcEhSblZhTWxaNVlqTldla3hZUWpGWmJYaHdXWGt4ZG1GWFVtcE1WMHBzQ2xsWFRuWmlhVGgxV2pKc01HRklWbWxNTTJSMlkyMTBiV0pIT1ROamVUbHNaVWhTZVZwWE1XeGlTR3QwV2tkR2RWb3lWbmxpTTFaNlRGYzVjRnBIVFhRS1dXMVdhRmt5T1hWTWJteDBZa1ZDZVZwWFducE1NbWhzV1ZkU2Vrd3lNV2hoVnpSM1QxRlpTMHQzV1VKQ1FVZEVkbnBCUWtGUlVYSmhTRkl3WTBoTk5ncE1lVGt3WWpKMGJHSnBOV2haTTFKd1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGbVFtZHZja0puUlVWQldVOHZDazFCUlVOQ1FrWXpZak5LY2xwdGVIWmtNVGxyWVZoT2QxbFlVbXBoUkVFeVFtZHZja0puUlVWQldVOHZUVUZGUkVKRFozZGFhbU14VDFScmVWcEhUVE1LVGxSVk5VNVVRbWxOYWtFMFRWUkZNVnBFV20xTmFtdDNXWHBuTTFsVVkzaFpWRTVvV2xkU2JFMURNRWREYVhOSFFWRlJRbWMzT0hkQlVWRkZTREJXTkFwa1NFcHNZbGRXYzJWVFFtdFpWelZ1V2xoS2RtUllUV2RVTUd4RlVYbENhVnBYUm1waU1qUjNVMUZaUzB0M1dVSkNRVWRFZG5wQlFrSlJVVGRqTW14dUNtTXpVblpqYlZWMFdUSTVkVnB0T1hsaVYwWjFXVEpWZGxwWWFEQmpiVlowV2xkNE5VeFhVbWhpYldSc1kyMDVNV041TVhka1YwcHpZVmROZEdJeWJHc0tXWGt4YVZwWFJtcGlNalIzU0ZGWlMwdDNXVUpDUVVkRWRucEJRa0puVVZCamJWWnRZM2s1YjFwWFJtdGplVGwwV1Zkc2RVMUVjMGREYVhOSFFWRlJRZ3BuTnpoM1FWRm5SVXhSZDNKaFNGSXdZMGhOTmt4NU9UQmlNblJzWW1rMWFGa3pVbkJpTWpWNlRHMWtjR1JIYURGWmJsWjZXbGhLYW1JeU5UQmFWelV3Q2t4dFRuWmlWRU5DY0dkWlMwdDNXVUpDUVVkRWRucEJRa05SVTBKc2QzbENiRWRvTUdSSVFucFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVlbUZYWkhvS1pFYzVlVnBUTVdwaU1qVnRZak5LZEZsWE5XcGFVemxzWlVoU2VWcFhNV3hpU0d0MFdrZEdkVm95Vm5saU0xWjZURmhDTVZsdGVIQlplVEYyWVZkU2FncE1WMHBzV1ZkT2RtSnBPSFZhTW13d1lVaFdhVXd6WkhaamJYUnRZa2M1TTJONU9XeGxTRko1V2xjeGJHSklhM1JhUjBaMVdqSldlV0l6Vm5wTVZ6bHdDbHBIVFhSWmJWWm9XVEk1ZFV4dWJIUmlSVUo1V2xkYWVrd3lhR3haVjFKNlRESXhhR0ZYTkhkUFFWbExTM2RaUWtKQlIwUjJla0ZDUTJkUmNVUkRaM2NLV21wak1VOVVhM2xhUjAwelRsUlZOVTVVUW1sTmFrRTBUVlJGTVZwRVdtMU5hbXQzV1hwbk0xbFVZM2haVkU1b1dsZFNiRTFDTUVkRGFYTkhRVkZSUWdwbk56aDNRVkZ6UlVSM2QwNWFNbXd3WVVoV2FVeFhhSFpqTTFKc1drUkNaVUpuYjNKQ1owVkZRVmxQTDAxQlJVMUNSa0ZOVkcxb01HUklRbnBQYVRoMkNsb3liREJoU0ZacFRHMU9kbUpUT1hwaFYyUjZaRWM1ZVZwVE1XcGlNalZ0WWpOS2RGbFhOV3BhVXpsc1pVaFNlVnBYTVd4aVNHdDBXa2RHZFZveVZua0tZak5XZWt4WVFqRlpiWGh3V1hreGRtRlhVbXBNVjBwc1dWZE9kbUpxUVRSQ1oyOXlRbWRGUlVGWlR5OU5RVVZPUWtOdlRVdEVRbTFPZWxVMVQxUkthd3BaZW1NeFRsUnJNVTFIU1hsTlJHZDRUVlJXYTA1dFdYbFBWRUpxVDBSa2FFNTZSbWhOTWtac1drZFZkMGgzV1V0TGQxbENRa0ZIUkhaNlFVSkVaMUZTQ2tSQk9YbGFWMXA2VERKb2JGbFhVbnBNTWpGb1lWYzBkMGRSV1V0TGQxbENRa0ZIUkhaNlFVSkVkMUZNUkVGck1rMTZTVEZQVkZrMFQxUmpkMDUzV1VzS1MzZFpRa0pCUjBSMmVrRkNSVUZSY0VSRFpHOWtTRkozWTNwdmRrd3laSEJrUjJneFdXazFhbUl5TUhaak1teHVZek5TZG1OdFZYUlpNamwxV20wNWVRcGlWMFoxV1RKVmQwZFJXVXRMZDFsQ1FrRkhSSFo2UVVKRlVWRk1SRUZyZUUxNlJUUk5SRkV4VG1wTmQyZGhXVWREYVhOSFFWRlJRbWMzT0hkQlVrbEZDbWRhWTAxbldsSnZaRWhTZDJONmIzWk1NbVJ3WkVkb01WbHBOV3BpTWpCMll6SnNibU16VW5aamJWVjBXVEk1ZFZwdE9YbGlWMFoxV1RKVmRscFlhREFLWTIxV2RGcFhlRFZNVjFKb1ltMWtiR050T1RGamVURjNaRmRLYzJGWFRYUmlNbXhyV1hreGFWcFhSbXBpTWpSMlRHMWtjR1JIYURGWmFUa3pZak5LY2dwYWJYaDJaRE5OZGxwWWFEQmpiVlowV2xkNE5VeFhVbWhpYldSc1kyMDVNV041TVhaaFYxSnFURmRLYkZsWFRuWmlhVFUxWWxkNFFXTnRWbTFqZVRsdkNscFhSbXRqZVRsMFdWZHNkVTFFWjBkRGFYTkhRVkZSUW1jM09IZEJVazFGUzJkM2IwMUhXVE5PVkdzMVRXMVNhazU2VlRGUFZGVjNXV3BKZDA5RVJYZ0tUbGRSTWxwcVNUVk5SMDAwVGpKRk0wMVhSWHBaVjFacldsUkJhRUpuYjNKQ1owVkZRVmxQTDAxQlJWVkNRazFOUlZoa2RtTnRkRzFpUnpreldESlNjQXBqTTBKb1pFZE9iMDFKUjBOQ1oyOXlRbWRGUlVGWlR5OU5RVVZXUWtoUlRXTnRhREJrU0VKNlQyazRkbG95YkRCaFNGWnBURzFPZG1KVE9YcGhWMlI2Q21SSE9YbGFVekZxWWpJMWJXSXpTblJaVnpWcVdsTTViR1ZJVW5sYVZ6RnNZa2hyZEZwSFJuVmFNbFo1WWpOV2VreFlRakZaYlhod1dYa3hkbUZYVW1vS1RGZEtiRmxYVG5aaWFUbG9XVE5TY0dJeU5YcE1NMG94WW01TmRrMVVRVEJPYWtVeVRucG5kMDE2VlhaWldGSXdXbGN4ZDJSSVRYWk5WRUZYUW1kdmNncENaMFZGUVZsUEwwMUJSVmRDUVdkTlFtNUNNVmx0ZUhCWmVrTkNhWGRaUzB0M1dVSkNRVWhYWlZGSlJVRm5VamxDU0hOQlpWRkNNMEZPTURsTlIzSkhDbmg0UlhsWmVHdGxTRXBzYms1M1MybFRiRFkwTTJwNWRDODBaVXRqYjBGMlMyVTJUMEZCUVVKclYzbDRVV2cwUVVGQlVVUkJSV2QzVW1kSmFFRkxieklLUzJWaU5YRTBUMXBKZDJnMGFVdEVWR1JsT1RZck1rSnhUWE0wYlVKTGFrVlZLeloxY0U1SVRrRnBSVUUxZDFGSVdVSnhRV1k1ZWtWNE9FSmlhbUpxUlFwbVp6Um9XbEF2Y3l0T2VIaFJTVE5rVTFCek4zQnVaM2REWjFsSlMyOWFTWHBxTUVWQmQwMUVZVUZCZDFwUlNYaEJTa3gwWkRaNVlrWTRWMEp1U214WUNsY3JOR0oyZWt0V2JuTm9UMVJvT1U1bGIyOHZkemh3YkdSRWNsRkljMnRKUjNGWVVuQnRkM2MyV0c0NUwwSnBhamRSU1hkSFRHZGlkRFlyV2xoNGJrY0tNRkJHUTFSWFRXTXJXbGRxYm01UmEwOHJiRGxaTVdkQk5tUTJjV2t3VDNCS2FHdHlRa2hGVmpoaFZFeHhhMUJGYTA5aUx3b3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENnPT0ifX19fQ=="}]},"messageSignature":{"messageDigest":{"algorithm":"SHA2_256","digest":"dzORWcN32za5IYUhvdGT0h4bhrPbzXr1O5BPtfTUq1Q="},"signature":"MEYCIQDoVvgjSWWbMU3IiQ2GHo0/CKTuVk9Rpg/YZQfN69x7CAIhALx6RaqMVuOeXaC22qy/5t4N68jw06L8DOmBjHBCjFlz"}}
\ No newline at end of file
diff --git a/test/helpers/api_policy_helpers.rb b/test/helpers/api_policy_helpers.rb
new file mode 100644
index 00000000000..0d8ab5d7a7c
--- /dev/null
+++ b/test/helpers/api_policy_helpers.rb
@@ -0,0 +1,109 @@
+require_relative "policy_helpers"
+
+module ApiPolicyHelpers
+ extend ActiveSupport::Concern
+ include PolicyHelpers
+
+ class_methods do
+ def should_require_scope(scope, action)
+ context "requires #{scope} scope" do
+ should "deny ApiKey without scope" do
+ refute_authorized key_without_scope(scope), action
+ end
+
+ should "allow ApiKey with scope" do
+ assert_authorized key_with_scope(scope), action
+ end
+ end
+ end
+
+ def should_require_rubygem_scope(scope, action)
+ context "requires #{scope} and matching rubygem" do
+ should "deny ApiKey with rubygem without scope" do
+ refute_authorized key_without_scope(scope, rubygem: @rubygem), action
+ end
+
+ should "deny ApiKey with scope but wrong rubygem" do
+ refute_authorized key_with_scope(scope, rubygem: create(:rubygem, owners: [@owner])), action
+ end
+
+ should "allow ApiKey with scope and rubygem" do
+ assert_authorized key_with_scope(scope, rubygem: @rubygem), action
+ end
+ end
+ end
+
+ def should_require_user_key(scope, action)
+ context "requires ApiKey owned by a user" do
+ should "deny ApiKey not owned by a user" do
+ refute_authorized trusted_publisher_key(scope), action, I18n.t(:api_key_forbidden)
+ end
+
+ should "allow ApiKey owned by a user" do
+ assert_authorized key_with_scope(scope), action
+ end
+ end
+ end
+
+ def should_require_mfa(scope, action)
+ context "mfa required" do
+ setup do
+ GemDownload.increment(Rubygem::MFA_REQUIRED_THRESHOLD + 1, rubygem_id: @rubygem.id)
+ @rubygem.reload
+ end
+
+ should "deny ApiKey with owner.mfa_required_not_yet_enabled?" do
+ assert_predicate @owner, :mfa_required_not_yet_enabled?
+ refute_authorized key_with_scope(scope), action, I18n.t("multifactor_auths.api.mfa_required_not_yet_enabled")
+ end
+
+ should "deny ApiKey with owner.mfa_required_weak_level_enabled?" do
+ @owner.enable_totp!(ROTP::Base32.random_base32, :ui_only)
+
+ assert_predicate @owner, :mfa_required_weak_level_enabled?
+ refute_authorized key_with_scope(scope), action, I18n.t("multifactor_auths.api.mfa_required_weak_level_enabled")
+ end
+
+ should "allow ApiKey with strong level mfa" do
+ @owner.enable_totp!(ROTP::Base32.random_base32, :ui_and_api)
+
+ assert_predicate @owner, :strong_mfa_level?
+ assert_authorized key_with_scope(scope), action
+ end
+ end
+ end
+
+ def should_delegate_to_policy(scope, action, policy_class)
+ context "delegates to #{policy_class}##{action}" do
+ should "allow if the #{policy_class} allows #{action}" do
+ policy_class.any_instance.stubs(action => true)
+
+ assert_authorized key_with_scope(scope), action
+ end
+
+ should "deny if the #{policy_class} denies #{action}" do
+ policy_class.any_instance.stubs(action => false, :error => "error")
+
+ refute_authorized key_with_scope(scope), action, "error"
+ end
+ end
+ end
+ end
+
+ def trusted_publisher_key(scope)
+ create(:api_key, :trusted_publisher, scopes: [scope])
+ end
+
+ def key_without_scope(scopes, **)
+ scopes = (ApiKey::APPLICABLE_GEM_API_SCOPES - Array.wrap(scopes)).sample(2)
+ key_with_scope(scopes, **)
+ end
+
+ def key_with_scope(scopes, owner: @owner, **)
+ create(:api_key, owner:, scopes: Array.wrap(scopes), **)
+ end
+
+ def refute_authorized(actor, action, message = I18n.t(:api_key_insufficient_scope))
+ super
+ end
+end
diff --git a/test/helpers/avo_helpers.rb b/test/helpers/avo_helpers.rb
new file mode 100644
index 00000000000..1927d7b4bf1
--- /dev/null
+++ b/test/helpers/avo_helpers.rb
@@ -0,0 +1,24 @@
+module AvoHelpers
+ def avo_sign_in_as(user)
+ OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(
+ provider: "github",
+ uid: "1",
+ credentials: {
+ token: user.oauth_token,
+ expires: false
+ },
+ info: {
+ name: user.login
+ }
+ )
+
+ @ip_address = create(:ip_address, ip_address: "127.0.0.1")
+
+ stub_github_info_request(user.info_data)
+
+ visit avo.root_path
+ click_button "Log in with GitHub"
+
+ page.assert_text user.login
+ end
+end
diff --git a/test/helpers/policy_helpers.rb b/test/helpers/policy_helpers.rb
new file mode 100644
index 00000000000..ad4901e5d72
--- /dev/null
+++ b/test/helpers/policy_helpers.rb
@@ -0,0 +1,31 @@
+module PolicyHelpers
+ extend ActiveSupport::Concern
+
+ def assert_authorized(policy_or_actor, action)
+ policy = policy_or_actor if policy_or_actor.is_a?(ApplicationPolicy)
+ policy ||= policy!(policy_or_actor)
+
+ result = policy.send(action)
+ flunk "Expected #{policy.class} to authorize #{action.inspect}\n#{pretty_print_policy(policy)}" unless result
+ flunk "Expected #{policy.class}##{action} not to produce an error: #{policy.error}\n#{pretty_print_policy(policy)}" if policy.error
+
+ assert result
+ end
+
+ def refute_authorized(policy_or_actor, action, message = nil)
+ policy = policy_or_actor if policy_or_actor.is_a?(ApplicationPolicy)
+ policy ||= policy!(policy_or_actor)
+
+ result = policy.send(action)
+ flunk "Expected #{policy.class} to deny #{action.inspect}\n#{pretty_print_policy(policy)}" if result
+ assert_equal message.chomp, policy.error&.chomp if message
+
+ refute result
+ end
+
+ def pretty_print_policy(policy)
+ user = policy.user.respond_to?(:handle) ? "#" : policy.user.inspect
+ error = policy.error ? " Error: #{policy.error}\n" : nil
+ "#{error} User: #{user}\n Record: #{policy.record.inspect}"
+ end
+end
diff --git a/test/integration/about_page_test.rb b/test/integration/about_page_test.rb
deleted file mode 100644
index ed6ea009813..00000000000
--- a/test/integration/about_page_test.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require "test_helper"
-
-class AboutPageTest < ActionDispatch::IntegrationTest
- def assert_about_page_i18n(local)
- get page_url("about"), params: { locale: local }
-
- assert_response :success
- assert page.has_content? I18n.t("pages.about.title")
- end
-
- test "about page i18n for all supported languages" do
- I18n.available_locales.each do |locale|
- assert_about_page_i18n locale
- end
- end
-end
diff --git a/test/integration/api/v1/oidc/rubygem_trusted_publishers_controller_test.rb b/test/integration/api/v1/oidc/rubygem_trusted_publishers_controller_test.rb
index 637d6746a6f..64af8414c8c 100644
--- a/test/integration/api/v1/oidc/rubygem_trusted_publishers_controller_test.rb
+++ b/test/integration/api/v1/oidc/rubygem_trusted_publishers_controller_test.rb
@@ -62,7 +62,7 @@ class Api::V1::OIDC::RubygemTrustedPublishersControllerTest < ActionDispatch::In
should "deny access" do
assert_response :forbidden
- assert_match "The API key doesn't have access", @response.body
+ assert_includes @response.body, "This API key cannot perform the specified action on this gem."
end
end
diff --git a/test/integration/api/v1/oidc/trusted_publisher_controller_test.rb b/test/integration/api/v1/oidc/trusted_publisher_controller_test.rb
index 5ce5bcd204a..98286492fd6 100644
--- a/test/integration/api/v1/oidc/trusted_publisher_controller_test.rb
+++ b/test/integration/api/v1/oidc/trusted_publisher_controller_test.rb
@@ -45,6 +45,20 @@ def jwt(claims = @claims, key: @pkey)
end
context "POST exchange_token" do
+ should "return invalid request with no JWT" do
+ post api_v1_oidc_trusted_publisher_exchange_token_path
+
+ assert_response :bad_request
+ assert_equal({ "error" => "Request is missing param 'jwt'" }, response.parsed_body)
+ end
+
+ should "return invalid request with integer JWT" do
+ post api_v1_oidc_trusted_publisher_exchange_token_path,
+ params: { jwt: 1 }
+
+ assert_response :bad_request
+ end
+
should "return not found with no matching trusted publisher" do
post api_v1_oidc_trusted_publisher_exchange_token_path,
params: { jwt: jwt.to_s }
@@ -108,6 +122,24 @@ def jwt(claims = @claims, key: @pkey)
assert_response :bad_request
end
+ %w[nbf exp iat iss jti].each do |claim|
+ should "return bad request with missing/invalid #{claim}" do
+ payload = jwt # generates jwt hash
+ payload[claim] = ["a"]
+
+ post api_v1_oidc_trusted_publisher_exchange_token_path,
+ params: { jwt: payload.to_s }
+
+ assert_response :bad_request
+
+ payload.delete claim
+ post api_v1_oidc_trusted_publisher_exchange_token_path,
+ params: { jwt: payload.to_s }
+
+ assert_response :bad_request
+ end
+ end
+
should "return not found when time is before nbf" do
@claims["nbf"] += 1_000_000
trusted_publisher = build(:oidc_trusted_publisher_github_action,
diff --git a/test/integration/api/v1/owner_test.rb b/test/integration/api/v1/owner_test.rb
index 1bff0a5706e..936b49c6124 100644
--- a/test/integration/api/v1/owner_test.rb
+++ b/test/integration/api/v1/owner_test.rb
@@ -63,13 +63,13 @@ class Api::V1::OwnerTest < ActionDispatch::IntegrationTest
params: { email: @other_user.email },
headers: { "HTTP_AUTHORIZATION" => @other_user_api_key }
- assert_response :unauthorized
+ assert_response :forbidden
delete api_v1_rubygem_owners_path(@rubygem.slug),
params: { email: @other_user.email },
headers: { "HTTP_AUTHORIZATION" => @other_user_api_key }
- assert_response :unauthorized
+ assert_response :forbidden
post api_v1_rubygem_owners_path(@rubygem.slug),
params: { email: @other_user.email },
diff --git a/test/integration/api/v2/version_information_test.rb b/test/integration/api/v2/version_information_test.rb
index cb5b4aa1da4..e24ca4f672a 100644
--- a/test/integration/api/v2/version_information_test.rb
+++ b/test/integration/api/v2/version_information_test.rb
@@ -29,9 +29,10 @@ def request_endpoint(rubygem, version, format = "json")
test "has required fields" do
request_endpoint(@rubygem, "2.0.0")
json_response = JSON.load(@response.body)
- json_response["sha"]
- json_response["platform"]
- json_response["ruby_version"]
+
+ assert json_response.key?("sha")
+ assert json_response.key?("platform")
+ assert json_response.key?("ruby_version")
end
test "version does not exist" do
diff --git a/test/integration/avo/attestations_controller_test.rb b/test/integration/avo/attestations_controller_test.rb
new file mode 100644
index 00000000000..313642a7f62
--- /dev/null
+++ b/test/integration/avo/attestations_controller_test.rb
@@ -0,0 +1,25 @@
+require "test_helper"
+
+class Avo::AttestationsControllerTest < ActionDispatch::IntegrationTest
+ include AdminHelpers
+
+ test "getting attestations as admin" do
+ admin_sign_in_as create(:admin_github_user, :is_admin)
+
+ get avo.resources_attestations_path
+
+ assert_response :success
+
+ attestation = create(:attestation)
+
+ get avo.resources_attestations_path
+
+ assert_response :success
+ page.assert_text attestation.media_type
+
+ get avo.resources_attestation_path(attestation)
+
+ assert_response :success
+ page.assert_text attestation.media_type
+ end
+end
diff --git a/test/integration/avo/gem_name_reservations_controller_test.rb b/test/integration/avo/gem_name_reservations_controller_test.rb
index 58abc37dacb..57574ab3731 100644
--- a/test/integration/avo/gem_name_reservations_controller_test.rb
+++ b/test/integration/avo/gem_name_reservations_controller_test.rb
@@ -10,8 +10,14 @@ class Avo::GemNameReservationsControllerTest < ActionDispatch::IntegrationTest
get avo.resources_gem_name_reservations_path
assert_response :success
+ end
+
+ test "resource search_query scope" do
+ requires_avo_pro
+
+ admin_sign_in_as create(:admin_github_user, :is_admin)
+ create(:gem_name_reservation, name: "hello")
- # test resource search_query scope
get avo.avo_api_search_path(q: "hello")
assert_response :success
diff --git a/test/integration/null_char_param_test.rb b/test/integration/null_char_param_test.rb
index 0d32fb3ad35..bc0ebd713ad 100644
--- a/test/integration/null_char_param_test.rb
+++ b/test/integration/null_char_param_test.rb
@@ -15,9 +15,13 @@ class NullCharParamTest < ActionDispatch::IntegrationTest
test "cookie with null character responds with bad request for sign in" do
get "/users/new", headers: { "HTTP_COOKIE" => "remember_token=php://input%00.;rubygems_session=php://input%00." }
+
+ assert_response :bad_request
end
test "cookie with null character responds with bad request for releases page" do
get "/releases/popular", headers: { "HTTP_COOKIE" => "rubygems_session=php://input%00." }
+
+ assert_response :bad_request
end
end
diff --git a/test/integration/organizations/onboarding/confirm_controller_test.rb b/test/integration/organizations/onboarding/confirm_controller_test.rb
new file mode 100644
index 00000000000..e7a51033729
--- /dev/null
+++ b/test/integration/organizations/onboarding/confirm_controller_test.rb
@@ -0,0 +1,52 @@
+require "test_helper"
+
+class Organizations::Onboarding::ConfirmControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @user = create(:user, remember_token_expires_at: Gemcutter::REMEMBER_FOR.from_now)
+ post session_path(session: { who: @user.handle, password: PasswordHelpers::SECURE_TEST_PASSWORD })
+
+ @collaborator = create(:user, :mfa_enabled)
+ @rubygem = create(:rubygem, owners: [@user, @collaborator])
+
+ @organization_onboarding = create(
+ :organization_onboarding,
+ :gem,
+ created_by: @user,
+ namesake_rubygem: @rubygem,
+ approved_invites: [{ user: @collaborator, role: "maintainer" }]
+ )
+ end
+
+ context "GET #show" do
+ should "to render the show template" do
+ get "/organizations/onboarding/confirm"
+
+ assert_response :ok
+ end
+ end
+
+ context "PATCH #update" do
+ should "onboard the organization and render a success message" do
+ patch "/organizations/onboarding/confirm"
+
+ assert_redirected_to organization_path(@organization_onboarding.reload.organization)
+
+ follow_redirect!
+
+ assert page.has_content?("Organization onboarded successfully")
+
+ assert_predicate @organization_onboarding.reload, :completed?
+ end
+
+ should "fail to onboard the organization and render an error message" do
+ @conflicting_org = create(:organization, handle: @organization_onboarding.organization_handle)
+
+ patch "/organizations/onboarding/confirm"
+
+ assert page.has_content?("Onboarding error: Validation failed: Handle has already been taken")
+
+ assert_predicate @organization_onboarding.reload, :failed?
+ assert_nil @organization_onboarding.organization
+ end
+ end
+end
diff --git a/test/integration/organizations/onboarding/gems_controller_test.rb b/test/integration/organizations/onboarding/gems_controller_test.rb
new file mode 100644
index 00000000000..a17bf7a5ab8
--- /dev/null
+++ b/test/integration/organizations/onboarding/gems_controller_test.rb
@@ -0,0 +1,50 @@
+require "test_helper"
+
+class Organizations::Onboarding::GemsControllerTest < ActionController::TestCase
+ setup do
+ @user = create(:user, :mfa_enabled)
+ @namesake_rubygem = create(:rubygem, owners: [@user])
+ @gem = create(:rubygem, owners: [@user])
+ @organization_onboarding = create(
+ :organization_onboarding,
+ created_by: @user,
+ namesake_rubygem: @namesake_rubygem,
+ status: :pending,
+ organization_handle: @namesake_rubygem.name,
+ organization_name: "Existing Name"
+ )
+
+ sign_in_as(@user)
+ end
+
+ context "PATCH update" do
+ should "save the selected gems and redirect to the next step" do
+ patch :update, params: { organization_onboarding: { rubygems: [@gem.id] } }
+
+ assert_redirected_to organization_onboarding_users_path
+ assert_equal [@namesake_rubygem.id, @gem.id], @organization_onboarding.reload.rubygems
+ end
+
+ should "allow selecting no additional gems" do
+ patch :update
+
+ assert_redirected_to organization_onboarding_users_path
+ assert_equal [@namesake_rubygem.id], @organization_onboarding.reload.rubygems
+ end
+
+ should "ignore empty params" do
+ patch :update, params: { organization_onboarding: { rubygems: [""] } }
+
+ assert_redirected_to organization_onboarding_users_path
+ assert_equal [@namesake_rubygem.id], @organization_onboarding.reload.rubygems
+ end
+
+ should "invalidate unknown gems" do
+ notmygem = create(:rubygem)
+ patch :update, params: { organization_onboarding: { rubygems: [notmygem.id] } }
+
+ assert_response :unprocessable_entity
+ assert_equal [@namesake_rubygem.id], @organization_onboarding.reload.rubygems
+ end
+ end
+end
diff --git a/test/integration/organizations/onboarding/name_controller_test.rb b/test/integration/organizations/onboarding/name_controller_test.rb
new file mode 100644
index 00000000000..b7c091cd762
--- /dev/null
+++ b/test/integration/organizations/onboarding/name_controller_test.rb
@@ -0,0 +1,60 @@
+require "test_helper"
+
+class Organizations::Onboarding::NameControllerTest < ActionController::TestCase
+ setup do
+ @user = create(:user, :mfa_enabled)
+ @gem = create(:rubygem, owners: [@user])
+
+ sign_in_as(@user)
+ end
+
+ context "GET new" do
+ should "ask the user to start creating a new organization" do
+ get :new
+
+ assert_select "input[name=?]", "organization_onboarding[organization_name]"
+ end
+
+ context "when the user has an existing onboarding" do
+ setup do
+ @organization_onboarding = create(:organization_onboarding, created_by: @user, status: :pending, organization_name: "Existing Name")
+ end
+
+ should "render with the in-progress onboarding" do
+ get :new
+
+ assert_select "input[name=?][value=?]", "organization_onboarding[organization_name]", @organization_onboarding.organization_name
+ end
+ end
+ end
+
+ context "POST create" do
+ should "create a new onboarding and redirect to the next step" do
+ post :create, params: { organization_onboarding: { organization_name: "New Name", organization_handle: @gem.name, name_type: "gem" } }
+
+ assert OrganizationOnboarding.exists?(organization_name: "New Name", organization_handle: @gem.name, name_type: "gem")
+ assert_redirected_to organization_onboarding_gems_path
+ end
+
+ context "when the user has an existing onboarding" do
+ setup do
+ @organization_onboarding = create(:organization_onboarding, created_by: @user, status: :pending, organization_name: "Existing Name")
+ end
+
+ should "update the existing onboarding and redirect to the next step" do
+ post :create, params: { organization_onboarding: { organization_name: "Updated Name" } }
+
+ assert_redirected_to organization_onboarding_gems_path
+ assert_equal "Updated Name", @organization_onboarding.reload.organization_name
+ end
+ end
+
+ context "when the onboarding is invalid" do
+ should "render the form with an error" do
+ post :create, params: { organization_onboarding: { organization_name: "" } }
+
+ assert_response :unprocessable_entity
+ end
+ end
+ end
+end
diff --git a/test/integration/organizations/onboarding/users_controller_test.rb b/test/integration/organizations/onboarding/users_controller_test.rb
new file mode 100644
index 00000000000..97eaeb5f1c2
--- /dev/null
+++ b/test/integration/organizations/onboarding/users_controller_test.rb
@@ -0,0 +1,123 @@
+require "test_helper"
+
+class Organizations::Onboarding::UsersControllerTest < ActionController::TestCase
+ setup do
+ @user = create(:user, :mfa_enabled)
+ @other_users = create_list(:user, 2)
+ @rubygem = create(:rubygem, owners: [@user, *@other_users])
+
+ sign_in_as(@user)
+
+ @organization_onboarding = create(
+ :organization_onboarding,
+ created_by: @user,
+ namesake_rubygem: @rubygem
+ )
+
+ @invites = @organization_onboarding.invites.to_a
+ end
+
+ test "render the list of users to invite" do
+ get :edit
+
+ assert_response :ok
+ # assert a text field has has the handle
+ @invites.each_with_index do |invite, idx|
+ assert_select "input[name='organization_onboarding[invites_attributes][#{idx}][id]'][value='#{invite.id}']"
+ assert_select "select[name='organization_onboarding[invites_attributes][#{idx}][role]']"
+ end
+ end
+
+ test "should update invites ignoring blank rows" do
+ patch :update, params: {
+ organization_onboarding: {
+ invites_attributes: {
+ "0" => { id: @invites[0].id, role: "maintainer" },
+ "1" => { id: @invites[1].id, role: "" }
+ }
+ }
+ }
+
+ assert_redirected_to organization_onboarding_confirm_path
+
+ @organization_onboarding.reload
+
+ assert_equal "maintainer", @organization_onboarding.invites.find_by(user_id: @other_users[0].id).role
+ assert_nil @organization_onboarding.invites.find_by(user_id: @other_users[1].id).role
+ end
+
+ test "should update invites ignoring user_id in invites_attributes" do
+ patch :update, params: {
+ organization_onboarding: {
+ invites_attributes: {
+ "0" => { id: @invites[0].id, role: "maintainer" },
+ "1" => { user_id: @invites[1].user.id, role: "owner" }
+ }
+ }
+ }
+
+ assert_redirected_to organization_onboarding_confirm_path
+
+ @organization_onboarding.reload
+
+ assert_equal "maintainer", @organization_onboarding.invites.find_by(user_id: @other_users[0].id).role
+ assert_nil @organization_onboarding.invites.find_by(user_id: @other_users[1].id).role
+ assert_equal 1, @organization_onboarding.approved_invites.count
+ end
+
+ test "should update multiple users" do
+ patch :update, params: {
+ organization_onboarding: {
+ invites_attributes: {
+ "0" => { id: @invites[0].id, role: "maintainer" },
+ "1" => { id: @invites[1].id, role: "admin" }
+ }
+ }
+ }
+
+ assert_redirected_to organization_onboarding_confirm_path
+
+ @organization_onboarding.reload
+
+ assert_equal "maintainer", @organization_onboarding.invites.find_by(user_id: @other_users[0].id).role
+ assert_equal "admin", @organization_onboarding.invites.find_by(user_id: @other_users[1].id).role
+ end
+
+ test "should update users including existing invites" do
+ patch :update, params: {
+ organization_onboarding: {
+ invites_attributes: {
+ "0" => { id: @invites[0].id, role: "admin" },
+ "1" => { id: @invites[1].id, role: "maintainer" }
+ }
+ }
+ }
+
+ @organization_onboarding.reload
+
+ assert_redirected_to organization_onboarding_confirm_path
+ assert_equal "admin", @organization_onboarding.invites.find_by(user_id: @other_users[0].id).role
+ assert_equal "maintainer", @organization_onboarding.invites.find_by(user_id: @other_users[1].id).role
+
+ get :edit
+
+ assert_select "input[name='organization_onboarding[invites_attributes][0][id]'][value='#{@invites[0].id}']"
+ assert_select "select[name='organization_onboarding[invites_attributes][0][role]'] option[selected][value='admin']"
+ assert_select "input[name='organization_onboarding[invites_attributes][1][id]'][value='#{@invites[1].id}']"
+ assert_select "select[name='organization_onboarding[invites_attributes][1][role]'] option[selected][value='maintainer']"
+
+ patch :update, params: {
+ organization_onboarding: {
+ invites_attributes: {
+ "0" => { id: @invites[0].id, role: "maintainer" },
+ "1" => { id: @invites[1].id, role: "" }
+ }
+ }
+ }
+
+ @organization_onboarding.reload
+
+ assert_equal "maintainer", @organization_onboarding.invites.find_by(user_id: @other_users[0].id).role
+ assert_nil @organization_onboarding.invites.find_by(user_id: @other_users[1].id).role
+ end
+end
diff --git a/test/integration/organizations/onboarding_test.rb b/test/integration/organizations/onboarding_test.rb
new file mode 100644
index 00000000000..8158d94a655
--- /dev/null
+++ b/test/integration/organizations/onboarding_test.rb
@@ -0,0 +1,51 @@
+require "test_helper"
+
+class Organizations::OnboardingControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @user = create(:user, remember_token_expires_at: Gemcutter::REMEMBER_FOR.from_now)
+ post session_path(session: { who: @user.handle, password: PasswordHelpers::SECURE_TEST_PASSWORD })
+ end
+
+ context "GET /organizations/onboarding" do
+ should "redirect to onboarding start page" do
+ get "/organizations/onboarding"
+
+ assert_redirected_to organization_onboarding_name_path
+ end
+ end
+
+ context "DELETE /organizations/onboarding" do
+ should "not destroy an OrganizationOnboarding that is already completed" do
+ organization_onboarding = create(:organization_onboarding, :completed, created_by: @user)
+
+ delete "/organizations/onboarding"
+
+ assert_redirected_to dashboard_path
+ assert OrganizationOnboarding.exists?(id: organization_onboarding.id)
+ end
+
+ should "destroy a pending OrganizationOnboarding created by the current user" do
+ organization_onboarding = create(:organization_onboarding, created_by: @user)
+
+ delete "/organizations/onboarding"
+
+ assert_redirected_to dashboard_path
+ refute OrganizationOnboarding.exists?(id: organization_onboarding.id)
+ end
+
+ should "destroy a failed OrganizationOnboarding created by the current user" do
+ organization_onboarding = create(:organization_onboarding, :failed, created_by: @user)
+
+ delete "/organizations/onboarding"
+
+ assert_redirected_to dashboard_path
+ refute OrganizationOnboarding.exists?(id: organization_onboarding.id)
+ end
+
+ should "redirect to the dashboarding if the current user has not started organization onboarding" do
+ delete "/organizations/onboarding"
+
+ assert_redirected_to dashboard_path
+ end
+ end
+end
diff --git a/test/integration/owner_test.rb b/test/integration/owner_test.rb
index d7aa11e91bb..e13015369cc 100644
--- a/test/integration/owner_test.rb
+++ b/test/integration/owner_test.rb
@@ -256,6 +256,44 @@ class OwnerTest < SystemTest
refute page.has_content? @other_user.handle
end
+ test "updating user to maintainer role" do
+ maintainer = create(:user)
+ create(:ownership, user: maintainer, rubygem: @rubygem)
+
+ visit_ownerships_page
+
+ within_element owner_row(maintainer) do
+ click_button "Edit"
+ end
+
+ select "Maintainer", from: "role"
+
+ click_button "Update"
+
+ assert_cell maintainer, "Role", "Maintainer"
+ end
+
+ test "editing the ownership of the current user" do
+ visit_ownerships_page
+
+ within_element owner_row(@user) do
+ assert_selector "button[disabled]", text: "Edit"
+ end
+ end
+
+ test "creating new owner with maintainer role" do
+ maintainer = create(:user)
+
+ visit_ownerships_page
+
+ fill_in "Handle", with: maintainer.handle
+ select "Maintainer", from: "role"
+
+ click_button "Add Owner"
+
+ assert_cell maintainer, "Role", "Maintainer"
+ end
+
teardown do
@authenticator&.remove!
Capybara.reset_sessions!
diff --git a/test/integration/pages_test.rb b/test/integration/pages_test.rb
index 25cce82ef58..d326d207682 100644
--- a/test/integration/pages_test.rb
+++ b/test/integration/pages_test.rb
@@ -1,20 +1,55 @@
require "test_helper"
class PagesTest < SystemTest
- test "renders existing page" do
- visit "/"
- click_link "About"
-
- assert page.has_content? "Welcome to RubyGems.org"
- end
-
test "gracefully fails on unknown page" do
assert_raises(ActionController::RoutingError) do
visit "/pages/not-existing-one"
end
+ end
+ test "it only allows html format" do
assert_raises(ActionController::RoutingError) do
visit "/pages/data.zip"
end
end
+
+ test "renders /pages/about for all supported languages" do
+ I18n.available_locales.each do |locale|
+ visit "/?locale=#{locale}"
+ click_link I18n.t("layouts.application.footer.about")
+
+ assert page.has_content? I18n.t("pages.about.title")
+ end
+ end
+
+ test "renders /pages/download" do
+ rubygem = create(:rubygem, name: "rubygems-update")
+ create(:version, number: "1.4.8", rubygem: rubygem)
+ create(:version,
+ number: "3.5.22",
+ created_at: Time.zone.local(2024, 10, 16),
+ rubygem: rubygem)
+
+ visit "/pages/download"
+
+ assert page.has_content?("v3.5.22 - October 16, 2024")
+ end
+
+ test "renders /pages/data" do
+ visit "/pages/data"
+
+ assert page.has_content?("PostgreSQL Data")
+ end
+
+ test "renders /pages/security" do
+ visit "/pages/security"
+
+ assert page.has_content?("Security")
+ end
+
+ test "renders /pages/sponsors" do
+ visit "/pages/sponsors"
+
+ assert page.has_content?("Sponsors")
+ end
end
diff --git a/test/integration/password_reset_test.rb b/test/integration/password_reset_test.rb
index 7ddc617c325..327ff07a8a0 100644
--- a/test/integration/password_reset_test.rb
+++ b/test/integration/password_reset_test.rb
@@ -38,11 +38,8 @@ def forgot_password_with(email)
fill_in "Password", with: PasswordHelpers::SECURE_TEST_PASSWORD
click_button "Save this password"
- assert_equal dashboard_path, page.current_path
+ assert_equal sign_in_path, page.current_path
- click_link "Sign out"
-
- visit sign_in_path
fill_in "Email or Username", with: @user.email
fill_in "Password", with: PasswordHelpers::SECURE_TEST_PASSWORD
click_button "Sign in"
@@ -55,8 +52,6 @@ def forgot_password_with(email)
visit password_reset_link
- assert page.has_content?("Sign out")
-
fill_in "Password", with: ""
click_button "Save this password"
@@ -75,6 +70,7 @@ def forgot_password_with(email)
fill_in "Password", with: PasswordHelpers::SECURE_TEST_PASSWORD
click_button "Save this password"
+ assert_equal sign_in_path, page.current_path
assert @user.reload.authenticated? PasswordHelpers::SECURE_TEST_PASSWORD
end
@@ -113,6 +109,7 @@ def forgot_password_with(email)
fill_in "Password", with: PasswordHelpers::SECURE_TEST_PASSWORD
click_button "Save this password"
+ assert_equal sign_in_path, page.current_path
assert @user.reload.authenticated? PasswordHelpers::SECURE_TEST_PASSWORD
assert_event Events::UserEvent::PASSWORD_CHANGED, {},
@@ -130,10 +127,13 @@ def forgot_password_with(email)
fill_in "otp", with: ROTP::TOTP.new(@user.totp_seed).now
click_button "Authenticate"
- assert page.has_content?("Sign out")
+ refute page.has_content?("Sign out")
fill_in "Password", with: PasswordHelpers::SECURE_TEST_PASSWORD
click_button "Save this password"
+
+ assert_equal sign_in_path, page.current_path
+ assert @user.reload.authenticated? PasswordHelpers::SECURE_TEST_PASSWORD
end
test "resetting a password when mfa is enabled but mfa session is expired" do
@@ -166,9 +166,9 @@ def forgot_password_with(email)
fill_in "Password", with: PasswordHelpers::SECURE_TEST_PASSWORD
click_button "Save this password"
- find(:css, ".header__popup-link").click
-
- assert page.has_content?("SIGN OUT")
+ assert page.has_content?("Sign in")
+ assert_equal sign_in_path, page.current_path
+ assert @user.reload.authenticated? PasswordHelpers::SECURE_TEST_PASSWORD
end
test "resetting password when webauthn is enabled using recovery codes" do
@@ -190,9 +190,9 @@ def forgot_password_with(email)
fill_in "Password", with: PasswordHelpers::SECURE_TEST_PASSWORD
click_button "Save this password"
- find(:css, ".header__popup-link").click
-
- assert page.has_content?("SIGN OUT")
+ assert page.has_content?("Sign in")
+ assert_equal sign_in_path, page.current_path
+ assert @user.reload.authenticated? PasswordHelpers::SECURE_TEST_PASSWORD
end
test "resetting password with pending email change" do
diff --git a/test/integration/profile_test.rb b/test/integration/profile_test.rb
index 748e159fb10..8585cfed4ad 100644
--- a/test/integration/profile_test.rb
+++ b/test/integration/profile_test.rb
@@ -88,7 +88,7 @@ def sign_out
assert_changes -> { @user.reload.mail_fails }, from: 1, to: 0 do
visit link
- assert page.has_selector? "#flash_notice", text: "Your email address has been verified"
+ assert page.has_content?("Your email address has been verified")
visit edit_profile_path
assert page.has_selector? "input[value='nick2@example.com']"
@@ -166,8 +166,9 @@ def sign_out
test "seeing ownership calls and requests" do
rubygem = create(:rubygem, owners: [@user], number: "1.0.0")
+ requested_gem = create(:rubygem, number: "2.0.0")
create(:ownership_call, rubygem: rubygem, user: @user, note: "special note")
- create(:ownership_request, rubygem: rubygem, user: @user, note: "request note")
+ create(:ownership_request, rubygem: requested_gem, user: @user, note: "request note")
sign_in
visit profile_path("nick1")
diff --git a/test/integration/push_test.rb b/test/integration/push_test.rb
index b3ec204aa0f..ae4471a3852 100644
--- a/test/integration/push_test.rb
+++ b/test/integration/push_test.rb
@@ -8,6 +8,66 @@ class PushTest < ActionDispatch::IntegrationTest
@key = "12345"
@user = create(:user)
create(:api_key, owner: @user, key: @key, scopes: %i[push_rubygem])
+
+ stub_request(:get, "https://tuf-repo-cdn.sigstore.dev/10.root.json")
+ .to_return(status: 404, body: "", headers: {})
+ end
+
+ test "multipart" do
+ rubygem = create(:rubygem, name: "sigstore", number: "0.0.1")
+ create(:ownership, rubygem: rubygem, user: @user)
+
+ rubygem_trusted_publisher = create(:oidc_rubygem_trusted_publisher, rubygem: rubygem)
+ rubygem_trusted_publisher.trusted_publisher.update!(
+ repository_owner: "sigstore-conformance",
+ repository_name: "extremely-dangerous-public-oidc-beacon",
+ workflow_filename: "extremely-dangerous-oidc-beacon.yml"
+ )
+
+ @key = "543321"
+ create(:api_key, owner: rubygem_trusted_publisher.trusted_publisher, key: @key, scopes: %i[push_rubygem])
+
+ signing_jwt = ["", {
+ aud: "sigstore",
+ iat: Time.zone.now.to_i - 60,
+ exp: Time.zone.now.to_i + 60,
+ nbf: Time.zone.now.to_i - 60,
+ iss: "sigstore-conformance",
+ sub: "sigstore-conformance"
+ }.to_json, ""].map { Base64.strict_encode64(_1) }.join(".")
+
+ Pusher.any_instance.stubs(:sigstore_signing_jwt).returns(signing_jwt)
+ Sigstore::Signer.any_instance.stubs(:sign).returns({})
+ bundle = JSON.parse(File.read(gem_file("sigstore-1.0.0.gem.sigstore.json")))
+
+ post api_v1_rubygems_path,
+ params: { "gem" => Rack::Test::UploadedFile.new(gem_file("sigstore-1.0.0.gem"), "application/octet-stream"),
+ "attestations" => JSON.dump([bundle]) },
+ headers: { "CONTENT_TYPE" => "multipart/mixed", "HTTP_AUTHORIZATION" => @key }
+
+ assert_response :success, response.body
+
+ get info_path("sigstore")
+ info_file = response.body
+
+ assert_response :success
+ assert_equal <<~INFO, info_file
+ ---
+ 0.0.1 |checksum:b5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78,ruby:>= 2.0.0,rubygems:>= 2.6.3
+ 1.0.0 |checksum:#{Digest::SHA256.hexdigest File.binread(gem_file('sigstore-1.0.0.gem'))}
+ INFO
+ assert_equal Digest::MD5.hexdigest(info_file), Rubygem.find_by!(name: "sigstore").versions.find_by(number: "1.0.0").info_checksum
+
+ get api_v2_rubygem_version_path("sigstore", "1.0.0", format: "json")
+
+ assert_response :success
+
+ assert_equal [["application/vnd.dev.sigstore.bundle.v0.3+json", bundle]], Attestation.pluck(:media_type, :body)
+
+ get api_v1_attestation_path("sigstore-1.0.0", format: "json")
+
+ assert_response :success
+ assert_equal [bundle], response.parsed_body
end
test "pushing a gem" do
@@ -77,7 +137,7 @@ class PushTest < ActionDispatch::IntegrationTest
push_gem "sandworm-2.0.0.gem"
- assert_response :success
+ assert_response :success, response.body
perform_enqueued_jobs
perform_enqueued_jobs only: ActionMailer::MailDeliveryJob
diff --git a/test/integration/routing_test.rb b/test/integration/routing_test.rb
index 173e9a28fc8..8bbe312016f 100644
--- a/test/integration/routing_test.rb
+++ b/test/integration/routing_test.rb
@@ -38,6 +38,7 @@ def contoller_in_ui?(controller)
format_path.gsub!(":id", "someid")
format_path.gsub!("*id", "about") # used in high voltage route
format_path.gsub!(":version_id", "someid")
+ format_path.gsub!(":organization_id", "someid")
assert_nothing_raised do
# ex: get(/password/new?format=json)
diff --git a/test/integration/rubygems_test.rb b/test/integration/rubygems_test.rb
index f6d1095e31a..1fc945eed7e 100644
--- a/test/integration/rubygems_test.rb
+++ b/test/integration/rubygems_test.rb
@@ -12,9 +12,20 @@ class RubygemsTest < ActionDispatch::IntegrationTest
assert page.has_content? "arrakis"
end
- test "gems list doesn't fall pray to path_params query param" do
+ test "gems list doesn't fall prey to path_params query param" do
get "/gems?path_params=string"
assert page.has_content? "arrakis"
end
+
+ test "GET to show for a gem published with an attestation" do
+ rubygem = create(:rubygem, name: "attested", number: "1.0.0")
+ trusted_publisher = create(:oidc_rubygem_trusted_publisher, rubygem: rubygem)
+ create(:api_key, scopes: %i[push_rubygem], owner: trusted_publisher.trusted_publisher)
+ create(:attestation, version: rubygem.versions.sole)
+
+ get "/gems/attested"
+
+ assert page.has_content? "Provenance"
+ end
end
diff --git a/test/integration/sign_in_test.rb b/test/integration/sign_in_test.rb
index dc24d2a5fd1..da222320e5d 100644
--- a/test/integration/sign_in_test.rb
+++ b/test/integration/sign_in_test.rb
@@ -297,8 +297,8 @@ class SignInTest < SystemTest
fill_in "Password", with: PasswordHelpers::SECURE_TEST_PASSWORD
click_button "Sign in"
- page.assert_text "Sign in"
- page.assert_text "Bad email or password."
+ assert page.has_content? "Sign in"
+ assert page.has_content? "Bad email or password."
end
teardown do
diff --git a/test/integration/subscriptions_test.rb b/test/integration/subscriptions_test.rb
new file mode 100644
index 00000000000..6e7d65644bb
--- /dev/null
+++ b/test/integration/subscriptions_test.rb
@@ -0,0 +1,36 @@
+require "test_helper"
+
+class SubscriptionsTest < SystemTest
+ setup do
+ @user = create(:user)
+ @rubygem = create(:rubygem, name: "sandworm", number: "1.0.0")
+ @version = create(:version, rubygem: @rubygem, number: "1.1.1")
+ end
+
+ test "subscribe to a gem" do
+ visit subscriptions_path(as: @user.id)
+
+ assert page.has_content? "You're not subscribed to any gems yet."
+
+ visit rubygem_path(@rubygem.slug)
+
+ click_link "Subscribe"
+
+ assert page.has_content? "Unsubscribe"
+
+ visit dashboard_path
+
+ assert page.has_content? @rubygem.name
+ assert page.has_content? @version.number
+
+ visit subscriptions_path
+
+ assert page.has_content? @rubygem.name
+
+ page.find("button[title='Unsubscribe']").click # rubocop:disable Capybara/SpecificActions
+
+ visit subscriptions_path
+
+ assert page.has_content? "You're not subscribed to any gems yet."
+ end
+end
diff --git a/test/jobs/delete_user_job_test.rb b/test/jobs/delete_user_job_test.rb
index 77cbfcfa034..361d13954d3 100644
--- a/test/jobs/delete_user_job_test.rb
+++ b/test/jobs/delete_user_job_test.rb
@@ -134,9 +134,9 @@ class DeleteUserJobTest < ActiveJob::TestCase
open_call = create(:ownership_call, rubygem: rubygem, user: user)
other_call = create(:ownership_call, rubygem: other_rubygem, user: other_user)
- closed_request = create(:ownership_request, ownership_call: other_call, rubygem: rubygem, user: user, status: :closed)
- approved_request = create(:ownership_request, ownership_call: other_call, rubygem: rubygem, user: user, status: :approved)
- open_request = create(:ownership_request, ownership_call: other_call, rubygem: rubygem, user: user)
+ closed_request = create(:ownership_request, ownership_call: other_call, rubygem: other_rubygem, user: user, status: :closed)
+ approved_request = create(:ownership_request, ownership_call: other_call, rubygem: other_rubygem, user: user, status: :approved)
+ open_request = create(:ownership_request, ownership_call: other_call, rubygem: other_rubygem, user: user)
other_request = create(:ownership_request, ownership_call: open_call, rubygem: rubygem, user: other_user)
assert_delete user
diff --git a/test/jobs/notify_web_hook_job_test.rb b/test/jobs/notify_web_hook_job_test.rb
index 7ab3aaa48c6..88425ca8012 100644
--- a/test/jobs/notify_web_hook_job_test.rb
+++ b/test/jobs/notify_web_hook_job_test.rb
@@ -46,30 +46,12 @@ class NotifyWebHookJobTest < ActiveJob::TestCase
end
should "succeed with hook relay" do
- NotifyWebHookJob.any_instance.expects(:use_hook_relay?).once.returns(true)
stub_request(:post, "https://api.hookrelay.dev/hooks///webhook_id-#{@hook.id}")
.with(headers: {
"Content-Type" => "application/json",
"HR_TARGET_URL" => @hook.url,
"HR_MAX_ATTEMPTS" => "3"
- }).to_return(status: 200, body: { id: 12_345 }.to_json)
-
- perform_enqueued_jobs do
- @job.enqueue
- end
-
- assert_performed_jobs 1, only: NotifyWebHookJob
- assert_enqueued_jobs 0, only: NotifyWebHookJob
- end
-
- should "succeed without hook relay" do
- NotifyWebHookJob.any_instance.expects(:use_hook_relay?).once.returns(false)
- stub_request(:post, @hook.url)
- .with(headers: {
- "Content-Type" => "application/json",
- "HR_TARGET_URL" => @hook.url,
- "HR_MAX_ATTEMPTS" => "3"
- }).to_return(status: 200, body: "")
+ }).to_return_json(status: 200, body: { id: 12_345 })
perform_enqueued_jobs do
@job.enqueue
@@ -87,30 +69,12 @@ class NotifyWebHookJobTest < ActiveJob::TestCase
end
should "discard the job on a 422 with hook relay" do
- NotifyWebHookJob.any_instance.expects(:use_hook_relay?).twice.returns(true)
stub_request(:post, "https://api.hookrelay.dev/hooks///webhook_id-#{@hook.id}")
.with(headers: {
"Content-Type" => "application/json",
"HR_TARGET_URL" => @hook.url,
"HR_MAX_ATTEMPTS" => "3"
- }).to_return(status: 422, body: { error: "Invalid url" }.to_json)
-
- perform_enqueued_jobs do
- @job.enqueue
- end
-
- assert_performed_jobs 1, only: NotifyWebHookJob
- assert_enqueued_jobs 0, only: NotifyWebHookJob
- end
-
- should "finish the job on a 422 without hook relay" do
- NotifyWebHookJob.any_instance.expects(:use_hook_relay?).once.returns(false)
- stub_request(:post, @hook.url)
- .with(headers: {
- "Content-Type" => "application/json",
- "HR_TARGET_URL" => @hook.url,
- "HR_MAX_ATTEMPTS" => "3"
- }).to_return(status: 422, body: "")
+ }).to_return_json(status: 422, body: { error: "Invalid url" })
perform_enqueued_jobs do
@job.enqueue
diff --git a/test/jobs/refresh_oidc_provider_job_test.rb b/test/jobs/refresh_oidc_provider_job_test.rb
index d11f87bf398..d90a7d2be12 100644
--- a/test/jobs/refresh_oidc_provider_job_test.rb
+++ b/test/jobs/refresh_oidc_provider_job_test.rb
@@ -142,4 +142,15 @@ def stub_requests(config_status: 200, jwks_status: 200, config_body: {}, jwks_bo
assert_nil @provider.reload.configuration
end
end
+
+ context "when the config endpoint returns a different URI for jwks" do
+ setup do
+ stub_requests(config_body: { jwks_uri: "https://example.com/jwks" })
+ end
+
+ should "raise an error" do
+ assert_kind_of RefreshOIDCProviderJob::JWKSURIMismatchError, RefreshOIDCProviderJob.perform_now(provider: @provider)
+ assert_nil @provider.reload.configuration
+ end
+ end
end
diff --git a/test/jobs/rstuf/check_job_test.rb b/test/jobs/rstuf/check_job_test.rb
index d45a535de96..9298770393d 100644
--- a/test/jobs/rstuf/check_job_test.rb
+++ b/test/jobs/rstuf/check_job_test.rb
@@ -47,6 +47,16 @@ class Rstuf::CheckJobTest < ActiveJob::TestCase
end
end
+ test "perform raises a retry exception on pre_run state and retries" do
+ retry_response = { "data" => { "state" => "PRE_RUN" } }
+ stub_request(:get, "#{Rstuf.base_url}/api/v1/task/?task_id=#{@task_id}")
+ .to_return(status: 200, body: retry_response.to_json, headers: { "Content-Type" => "application/json" })
+
+ assert_enqueued_with(job: Rstuf::CheckJob, args: [@task_id]) do
+ Rstuf::CheckJob.perform_now(@task_id)
+ end
+ end
+
test "perform raises a retry exception on retry state and retries" do
retry_response = { "data" => { "state" => "UNKNOWN" } }
stub_request(:get, "#{Rstuf.base_url}/api/v1/task/?task_id=#{@task_id}")
diff --git a/test/jobs/verify_link_job_test.rb b/test/jobs/verify_link_job_test.rb
index 5874f7e0584..121aeed0224 100644
--- a/test/jobs/verify_link_job_test.rb
+++ b/test/jobs/verify_link_job_test.rb
@@ -199,4 +199,16 @@ class VerifyLinkJobTest < ActiveJob::TestCase
assert_equal 1, @docs.failures_since_last_verification
assert_enqueued_jobs 0, only: VerifyLinkJob
end
+
+ test "does not retry link verification for localhost link" do
+ @home.update!(uri: "https://localhost")
+ freeze_time
+
+ VerifyLinkJob.perform_now(link_verification: @home)
+
+ refute_predicate @home.reload, :verified?
+ assert_nil @home.last_verified_at
+ assert_equal 1, @home.failures_since_last_verification
+ assert_enqueued_jobs 0, only: VerifyLinkJob
+ end
end
diff --git a/test/lib/access_test.rb b/test/lib/access_test.rb
new file mode 100644
index 00000000000..190cffcfd73
--- /dev/null
+++ b/test/lib/access_test.rb
@@ -0,0 +1,46 @@
+require "test_helper"
+
+class AccessTest < ActiveSupport::TestCase
+ context "roles are correctly sequenced" do
+ should "be in the correct order" do
+ assert_operator Access::OWNER, :>, Access::ADMIN
+ assert_operator Access::ADMIN, :>, Access::MAINTAINER
+ end
+ end
+
+ context ".role_for_flag" do
+ should "return the role for a given permission flag" do
+ assert_equal "owner", Access.role_for_flag(Access::OWNER)
+ end
+
+ context "when the permission flag does not exist" do
+ should "raise an error" do
+ assert_raises(ArgumentError) { Access.role_for_flag(999) }
+ end
+ end
+ end
+
+ context ".flag_for_role" do
+ should "return the role for a given permission flag" do
+ assert_equal Access::OWNER, Access.flag_for_role("owner")
+ end
+
+ should "cast the given input into the correct type" do
+ assert_equal Access::OWNER, Access.flag_for_role(:owner)
+ end
+
+ context "when the role does not exist" do
+ should "raise an error" do
+ assert_raises(KeyError) { Access.flag_for_role("unknown") }
+ end
+ end
+ end
+
+ context ".with_minimum_role" do
+ should "return the range of roles for a given permission flag" do
+ assert_equal (Access::OWNER..), Access.with_minimum_role("owner")
+ assert_equal (Access::ADMIN..), Access.with_minimum_role("admin")
+ assert_equal (Access::MAINTAINER..), Access.with_minimum_role("maintainer")
+ end
+ end
+end
diff --git a/test/mailers/owners_test.rb b/test/mailers/owners_test.rb
new file mode 100644
index 00000000000..a4bd094fd7d
--- /dev/null
+++ b/test/mailers/owners_test.rb
@@ -0,0 +1,20 @@
+require "test_helper"
+
+class OwnersMailerTest < ActionMailer::TestCase
+ setup do
+ @owner = create(:user)
+ @maintainer = create(:user)
+ @rubygem = create(:rubygem, name: "test-gem")
+ @owner_ownership = create(:ownership, rubygem: @rubygem, user: @owner)
+ @maintainer_ownership = create(:ownership, rubygem: @rubygem, user: @maintainer)
+ end
+
+ context "#owner_updated" do
+ should "include host in subject" do
+ email = OwnersMailer.with(ownership: @maintainer_ownership).owner_updated
+
+ assert_emails(1) { email.deliver_now }
+ assert_equal email.subject, "Your role was updated for #{@rubygem.name} gem"
+ end
+ end
+end
diff --git a/test/mailers/previews/mailer_preview.rb b/test/mailers/previews/mailer_preview.rb
index 7487ff01316..e2241ae7712 100644
--- a/test/mailers/previews/mailer_preview.rb
+++ b/test/mailers/previews/mailer_preview.rb
@@ -96,6 +96,13 @@ def owner_added
OwnersMailer.owner_added(user.id, owner.id, authorizer.id, gem.id)
end
+ def owner_updated
+ ownership = Ownership.last
+ owner = User.last
+
+ OwnersMailer.with(ownership: ownership, authorizer: owner).owner_updated
+ end
+
def api_key_created
api_key = ApiKey.where(owner_type: "User").last
Mailer.api_key_created(api_key.id)
@@ -215,4 +222,14 @@ def totp_disabled
Mailer.totp_disabled(user_id, Time.now.utc)
end
+
+ def admin_manual
+ Mailer.admin_manual(User.last, "A subject", <<~TEXT)
+ A body
+ with multiple lines
+ and a link to https://example.com
+ and an emoji 🎉
+ and a p tag
with html
and a link
+ TEXT
+ end
end
diff --git a/test/models/attestation_test.rb b/test/models/attestation_test.rb
new file mode 100644
index 00000000000..3d8eb1a4ecd
--- /dev/null
+++ b/test/models/attestation_test.rb
@@ -0,0 +1,7 @@
+require "test_helper"
+
+class AttestationTest < ActiveSupport::TestCase
+ should belong_to(:version)
+ should validate_presence_of(:media_type)
+ should validate_presence_of(:body)
+end
diff --git a/test/models/concerns/rubygem_searchable_test.rb b/test/models/concerns/rubygem_searchable_test.rb
index c72eeaf79de..d7089196e78 100644
--- a/test/models/concerns/rubygem_searchable_test.rb
+++ b/test/models/concerns/rubygem_searchable_test.rb
@@ -79,6 +79,48 @@ class RubygemSearchableTest < ActiveSupport::TestCase
assert_equal v, json[k], "value doesn't match for key: #{k}"
end
end
+
+ should "set the suggest json" do
+ json = @rubygem.search_data
+
+ assert_equal "example_gem", json[:suggest][:input]
+ end
+
+ should "calculate the suggestion weight based on the number of downloads" do
+ weights = [
+ [0, 0],
+ [10, 1],
+ [100, 2],
+ [1_000, 3],
+ [10_000, 4],
+ [100_000, 5],
+ [1_000_000, 6],
+ [10_000_000, 7],
+ [100_000_000, 8],
+ [1_000_000_000, 9]
+ ]
+
+ weights.each do |downloads, weight|
+ @rubygem.gem_download.update(count: downloads)
+ json = @rubygem.search_data
+
+ assert_equal weight, json[:suggest][:weight]
+ end
+ end
+
+ context "when the number of downloads exceeds a 32 bit integer" do
+ setup do
+ @rubygem = create(:rubygem, name: "large_downloads_example_gem", downloads: 10_000_000_000) # 10 Billion downloads
+ @version = create(:version, number: "1.0.0", rubygem: @rubygem)
+ import_and_refresh
+ end
+
+ should "allow the number of downloads to be stored as a 64 bit integer" do
+ json = @rubygem.search_data
+
+ assert_equal 10_000_000_000, json[:downloads]
+ end
+ end
end
context "rubygems analyzer" do
diff --git a/test/models/gem_name_reservation_test.rb b/test/models/gem_name_reservation_test.rb
index 2a46647f449..809355804e0 100644
--- a/test/models/gem_name_reservation_test.rb
+++ b/test/models/gem_name_reservation_test.rb
@@ -13,21 +13,24 @@ class GemNameReservationTest < ActiveSupport::TestCase
should_not allow_value("Abc").for(:name)
should allow_value("abc").for(:name)
should validate_uniqueness_of(:name).case_insensitive
+ should validate_length_of(:name).is_at_most(Gemcutter::MAX_FIELD_LENGTH)
end
context "#reserved?" do
should "recognize reserved gem name" do
create(:gem_name_reservation, name: "reserved-gem-name")
- GemNameReservation.reserved?("reserved-gem-name")
+
+ assert GemNameReservation.reserved?("reserved-gem-name")
end
should "recognize reserved case insensitive gem name" do
create(:gem_name_reservation, name: "reserved-gem-name")
- GemNameReservation.reserved?("RESERVED-gem-name")
+
+ assert GemNameReservation.reserved?("RESERVED-gem-name")
end
should "recognize not reserved gem name" do
- GemNameReservation.reserved?("totally-random-gem-name")
+ refute GemNameReservation.reserved?("totally-random-gem-name")
end
end
end
diff --git a/test/models/gem_typo_exception_test.rb b/test/models/gem_typo_exception_test.rb
index f9d4acef3ac..1dc77b7fe9a 100644
--- a/test/models/gem_typo_exception_test.rb
+++ b/test/models/gem_typo_exception_test.rb
@@ -3,6 +3,7 @@
class GemTypoExceptionTest < ActiveSupport::TestCase
context "name validations" do
should validate_uniqueness_of(:name).case_insensitive
+ should validate_length_of(:name).is_at_most(Gemcutter::MAX_FIELD_LENGTH)
should "be a valid factory" do
assert_predicate build(:gem_typo_exception), :valid?
diff --git a/test/models/linkset_test.rb b/test/models/linkset_test.rb
index 261890a1bcc..a57d2c58db9 100644
--- a/test/models/linkset_test.rb
+++ b/test/models/linkset_test.rb
@@ -36,4 +36,11 @@ class LinksetTest < ActiveSupport::TestCase
assert_equal @spec.homepage, @linkset.home
end
end
+
+ context "validations" do
+ %w[home code docs wiki mail bugs].each do |link|
+ should allow_value("http://example.com").for(link.to_sym)
+ should validate_length_of(link).is_at_most(Gemcutter::MAX_FIELD_LENGTH)
+ end
+ end
end
diff --git a/test/models/log_ticket_test.rb b/test/models/log_ticket_test.rb
index bd8cbd34fb6..876c42587fb 100644
--- a/test/models/log_ticket_test.rb
+++ b/test/models/log_ticket_test.rb
@@ -12,8 +12,10 @@ class LogTicketTest < ActiveSupport::TestCase
end
should "allow different keys for the same directory" do
- LogTicket.create!(directory: "test", key: "bar")
- LogTicket.create!(directory: "test", key: "baz")
+ assert_nothing_raised do
+ LogTicket.create!(directory: "test", key: "bar")
+ LogTicket.create!(directory: "test", key: "baz")
+ end
end
context "#pop" do
diff --git a/test/models/membership_test.rb b/test/models/membership_test.rb
new file mode 100644
index 00000000000..08d0bedce0b
--- /dev/null
+++ b/test/models/membership_test.rb
@@ -0,0 +1,31 @@
+require "test_helper"
+
+class MembershipTest < ActiveSupport::TestCase
+ should belong_to(:organization)
+ should belong_to(:user)
+
+ setup do
+ @organization = FactoryBot.create(:organization)
+ @user = FactoryBot.create(:user)
+ end
+
+ should "be unconfirmed by default" do
+ membership = Membership.create!(organization: @organization, user: @user)
+
+ assert_not(membership.confirmed?)
+ assert_empty(Membership.confirmed)
+ end
+
+ should "be confirmed with confirmed_at" do
+ membership = Membership.create!(organization: @organization, user: @user, confirmed_at: Time.zone.now)
+
+ assert_predicate(membership, :confirmed?)
+ assert_equal(Membership.confirmed, [membership])
+ end
+
+ should "have a default role" do
+ membership = Membership.create!(organization: @organization, user: @user)
+
+ assert_predicate membership, :maintainer?
+ end
+end
diff --git a/test/models/oidc/api_key_role_test.rb b/test/models/oidc/api_key_role_test.rb
index 790f6a1ee16..bce620fd32b 100644
--- a/test/models/oidc/api_key_role_test.rb
+++ b/test/models/oidc/api_key_role_test.rb
@@ -8,6 +8,8 @@ class OIDC::ApiKeyRoleTest < ActiveSupport::TestCase
should have_many :id_tokens
should validate_presence_of :api_key_permissions
should validate_presence_of :access_policy
+ should validate_presence_of :name
+ should validate_length_of(:name).is_at_most(Gemcutter::MAX_FIELD_LENGTH)
setup do
@role = build(:oidc_api_key_role)
diff --git a/test/models/oidc/trusted_publisher/github_action_test.rb b/test/models/oidc/trusted_publisher/github_action_test.rb
index b87e8e63e8f..bae69432ff9 100644
--- a/test/models/oidc/trusted_publisher/github_action_test.rb
+++ b/test/models/oidc/trusted_publisher/github_action_test.rb
@@ -7,10 +7,22 @@ class OIDC::TrustedPublisher::GitHubActionTest < ActiveSupport::TestCase
should have_many(:rubygem_trusted_publishers)
should have_many(:api_keys).inverse_of(:owner)
- should validate_presence_of(:repository_owner)
- should validate_presence_of(:repository_name)
- should validate_presence_of(:workflow_filename)
- should validate_presence_of(:repository_owner_id)
+ context "validations" do
+ setup do
+ stub_request(:get, Addressable::Template.new("https://api.github.com/users/{user}"))
+ .to_return(status: 404, body: "", headers: {})
+ end
+ should validate_presence_of(:repository_owner)
+ should validate_length_of(:repository_owner).is_at_most(Gemcutter::MAX_FIELD_LENGTH)
+ should validate_presence_of(:repository_name)
+ should validate_length_of(:repository_name).is_at_most(Gemcutter::MAX_FIELD_LENGTH)
+ should validate_presence_of(:workflow_filename)
+ should validate_length_of(:workflow_filename).is_at_most(Gemcutter::MAX_FIELD_LENGTH)
+ should validate_presence_of(:repository_owner_id)
+ should validate_length_of(:repository_owner_id).is_at_most(Gemcutter::MAX_FIELD_LENGTH)
+
+ should validate_length_of(:environment).is_at_most(Gemcutter::MAX_FIELD_LENGTH)
+ end
test "validates publisher uniqueness" do
publisher = create(:oidc_trusted_publisher_github_action)
diff --git a/test/models/organization_onboarding_test.rb b/test/models/organization_onboarding_test.rb
new file mode 100644
index 00000000000..df92aeef198
--- /dev/null
+++ b/test/models/organization_onboarding_test.rb
@@ -0,0 +1,267 @@
+require "test_helper"
+
+class OrganizationOnboardingTest < ActiveSupport::TestCase
+ setup do
+ @owner = create(:user)
+ @maintainer = create(:user)
+ @rubygem = create(:rubygem, owners: [@owner], maintainers: [@maintainer])
+
+ @onboarding = create(
+ :organization_onboarding,
+ name_type: "gem",
+ organization_name: "Test Organization",
+ organization_handle: @rubygem.name,
+ created_by: @owner,
+ namesake_rubygem: @rubygem
+ )
+ maintainer_invite = @onboarding.invites.first
+ maintainer_invite.update(role: :maintainer)
+ end
+
+ context "validations" do
+ context "when the created_by field is blank" do
+ setup do
+ @onboarding.created_by = nil
+ end
+
+ should "require a onboarded by user" do
+ assert_predicate @onboarding, :invalid?
+ assert_equal ["must exist"], @onboarding.errors[:created_by]
+ end
+ end
+
+ context "when the user does not have the required gem roles" do
+ setup do
+ @onboarding.created_by = @maintainer
+ end
+
+ should "be invalid" do
+ assert_predicate @onboarding, :invalid?
+ end
+
+ should "add an error to the rubygems attribute" do
+ @onboarding.valid?
+
+ assert_equal ["must be an owner of the #{@rubygem.name} gem"], @onboarding.errors[:created_by]
+ end
+ end
+
+ context "when the user specifies a gem they do not own" do
+ setup do
+ @other_user = create(:user)
+ @other_rubygem = create(:rubygem, owners: [@other_user])
+ @onboarding.rubygems = [@other_rubygem.id]
+ end
+
+ should "be invalid" do
+ assert_predicate @onboarding, :invalid?
+ end
+
+ should "add an error to the rubygems attribute" do
+ @onboarding.valid?
+
+ assert_equal ["must be an owner of the #{@other_rubygem.name} gem"], @onboarding.errors[:created_by]
+ end
+
+ should "not add an invite for the users on the gem" do
+ @onboarding.save
+ @onboarding.reload
+
+ assert_nil @onboarding.invites.find_by(user_id: @other_user.id)
+ end
+ end
+
+ context "when the user specifices a user as the name of the organization" do
+ setup do
+ @onboarding.name_type = :user
+ end
+
+ should "set the Organization Onboarding handle to the handle of the User" do
+ assert_equal @rubygem.name, @onboarding.organization_handle
+ end
+ end
+
+ context "when the user specifies a gem as the name of the organization" do
+ setup do
+ @onboarding.name_type = :gem
+ end
+
+ context "when the name is a valid gem that the user owns" do
+ should "be valid" do
+ @onboarding.organization_handle = @rubygem.name
+
+ assert_predicate @onboarding, :valid?
+ end
+ end
+
+ context "when the name is not valid" do
+ should "it is ignored and set to the gem name" do
+ @onboarding.organization_handle = "invalid"
+
+ assert_predicate @onboarding, :invalid?
+ end
+ end
+ end
+ end
+
+ context "#set_user_handle" do
+ context "when the gem name is set to user" do
+ setup do
+ @onboarding.name_type = :user
+ end
+
+ should "automatically set the organization handle to the handle of the user" do
+ @onboarding.valid?
+
+ assert_equal @owner.handle, @onboarding.organization_handle
+ end
+ end
+ end
+
+ context "#available_rubygems" do
+ should "exclude gems that already have an organization" do
+ create(:rubygem, owners: [@owner], organization: create(:organization))
+
+ assert_equal [@rubygem], @onboarding.available_rubygems
+ end
+ end
+
+ context "#rubygems=" do
+ should "exclude gems that already have an organization" do
+ other_rubygem = create(:rubygem, owners: [@owner], organization: create(:organization))
+ @onboarding.rubygems = [@rubygem.id, other_rubygem.id]
+
+ assert_equal [@rubygem.id], @onboarding.rubygems
+ end
+
+ should "add invites for the owners and maintainers of the specified rubygems" do
+ other_user = create(:user)
+ rubygem = create(:rubygem, owners: [@owner, other_user])
+ @onboarding.rubygems = [rubygem.id]
+ @onboarding.save
+ @onboarding.reload
+
+ assert_equal [@maintainer, other_user].map(&:handle).sort, @onboarding.users.map(&:handle).sort
+ end
+ end
+
+ context "#invites_attributes=" do
+ should "allow upding the role of an existing invite" do
+ invite = @onboarding.invites.find_by(user_id: @maintainer.id)
+
+ assert_equal "maintainer", invite.role
+
+ @onboarding.invites_attributes = {
+ "0" => { id: invite.id, role: "admin" }
+ }
+
+ @onboarding.save
+
+ invite.reload
+
+ assert_equal "admin", invite.role
+ end
+
+ should "prevent adding users that are not already invited" do
+ other_user = create(:user)
+ @onboarding.invites_attributes = {
+ "0" => { user_id: other_user.id, role: "maintainer" }
+ }
+
+ assert_equal 1, @onboarding.invites.count
+ assert_nil @onboarding.invites.find_by(user_id: other_user.id)
+ end
+ end
+
+ context "#onboard!" do
+ setup do
+ @onboarding.onboard!
+ end
+
+ should "mark the onboarding as completed" do
+ assert_predicate @onboarding, :completed?
+ end
+
+ should "create an organization with the specified name and handle" do
+ assert_not_nil @onboarding.organization
+ assert_equal @onboarding.organization_name, @onboarding.organization.name
+ assert_equal @onboarding.organization_handle, @onboarding.organization.handle
+ end
+
+ should "create a confirmed owner membership for the person that onboarded the organization" do
+ membership = @onboarding.organization.memberships.find_by(user_id: @onboarding.created_by)
+
+ assert_predicate membership, :owner?
+ assert_predicate membership, :confirmed?
+ end
+
+ should "create unconfirmed memberships for each invitee" do
+ membership = @onboarding.organization.unconfirmed_memberships.find_by(user_id: @maintainer.id)
+
+ assert_predicate membership, :maintainer?
+ assert_not_predicate membership, :confirmed?
+ end
+
+ should "set the organization_id for each specified rubygem" do
+ assert_equal @onboarding.organization.id, @rubygem.reload.organization_id
+ end
+
+ should "remove Ownership records that have been migrated to Memberships" do
+ assert_nil Ownership.find_by(user: @owner, rubygem: @rubygem)
+ assert_nil Ownership.find_by(user: @maintainer, rubygem: @rubygem)
+ end
+
+ context "when a user is marked as an Outside Contributor" do
+ setup do
+ @contributor = create(:user)
+ @ownership = create(:ownership, user: @contributor, rubygem: @rubygem, role: "owner")
+ @onboarding.invites << create(:organization_onboarding_invite, user: @contributor, role: :outside_contributor)
+ end
+
+ should "not remove the Ownership record" do
+ assert_not_nil Ownership.find_by(user: @contributor, rubygem: @rubygem)
+ end
+ end
+
+ context "when onboarding encounters an error" do
+ setup do
+ @onboarding = create(:organization_onboarding, created_by: @owner)
+ @onboarding.stubs(:create_organization!).raises(ActiveRecord::ActiveRecordError, "stubbed error")
+ end
+
+ should "mark the onboarding as failed" do
+ assert_raises ActiveRecord::ActiveRecordError, "stubbed error" do
+ @onboarding.onboard!
+ end
+
+ assert_predicate @onboarding, :failed?
+ end
+
+ should "record the error message" do
+ assert_raises ActiveRecord::ActiveRecordError, "stubbed error" do
+ @onboarding.onboard!
+ end
+
+ assert_equal "stubbed error", @onboarding.error
+ end
+ end
+
+ context "when the onboarding is already completed" do
+ setup do
+ @onboarding = create(:organization_onboarding, :completed, created_by: @owner)
+ end
+
+ should "raise an error" do
+ assert_raises StandardError, "onboard has already been completed" do
+ @onboarding.onboard!
+ end
+ end
+ end
+ end
+
+ context "#available_rubygems" do
+ should "return the rubygems that the user owns" do
+ assert_equal [@rubygem], @onboarding.available_rubygems
+ end
+ end
+end
diff --git a/test/models/organization_test.rb b/test/models/organization_test.rb
new file mode 100644
index 00000000000..2f956c0f33c
--- /dev/null
+++ b/test/models/organization_test.rb
@@ -0,0 +1,67 @@
+require "test_helper"
+
+class OrganizationTest < ActiveSupport::TestCase
+ should have_many(:memberships).dependent(:destroy)
+ should have_many(:unconfirmed_memberships).dependent(:destroy)
+ should have_many(:users).through(:memberships)
+ should have_many(:rubygems).dependent(:nullify)
+
+ # Waiting for Ownerships to be made polymorphic
+ #
+ # should have_many(:ownerships).dependent(:destroy)
+ # should have_many(:unconfirmed_ownerships).dependent(:destroy)
+ # should have_many(:rubygems).through(:ownerships)
+
+ context "validations" do
+ context "handle" do
+ should allow_value("CapsLOCK").for(:handle)
+ should_not allow_value(nil).for(:handle)
+ should_not allow_value("1abcde").for(:handle)
+ should_not allow_value("abc^%def").for(:handle)
+ should_not allow_value("abc\n