diff --git a/app/avo/resources/rubygem_resource.rb b/app/avo/resources/rubygem_resource.rb index 09636f782de..520a406f14f 100644 --- a/app/avo/resources/rubygem_resource.rb +++ b/app/avo/resources/rubygem_resource.rb @@ -41,6 +41,8 @@ class IndexedFilter < ScopeBooleanFilter; end field :link_verifications, as: :has_many field :oidc_rubygem_trusted_publishers, as: :has_many + field :archived, as: :boolean + field :events, as: :has_many field :audits, as: :has_many end diff --git a/app/controllers/api/v1/archive_controller.rb b/app/controllers/api/v1/archive_controller.rb new file mode 100644 index 00000000000..8440665c84f --- /dev/null +++ b/app/controllers/api/v1/archive_controller.rb @@ -0,0 +1,19 @@ +class Api::V1::ArchiveController < Api::BaseController + before_action :authenticate_with_api_key + before_action :verify_with_otp + before_action :find_rubygem + + def create + authorize @rubygem, :archive? + @rubygem.archive!(@api_key.user) + + render plain: response_with_mfa_warning("#{@rubygem.name} was succesfully archived.") + end + + def destroy + authorize @rubygem, :unarchive? + @rubygem.unarchive! + + render plain: response_with_mfa_warning("#{@rubygem.name} was succesfully unarchived.") + end +end diff --git a/app/controllers/archive_controller.rb b/app/controllers/archive_controller.rb new file mode 100644 index 00000000000..69714ace9c0 --- /dev/null +++ b/app/controllers/archive_controller.rb @@ -0,0 +1,31 @@ +class ArchiveController < ApplicationController + include SessionVerifiable + + verify_session_before + before_action :find_rubygem + before_action :verify_mfa_requirement + + def show + end + + def create + authorize @rubygem, :archive? + @rubygem.archive!(current_user) + + redirect_to rubygem_path(@rubygem), notice: t(".success") + end + + def destroy + authorize @rubygem, :unarchive? + @rubygem.unarchive! + + redirect_to rubygem_path(@rubygem), notice: t(".success") + end + + private + + def verify_mfa_requirement + return if @rubygem.mfa_requirement_satisfied_for?(current_user) + index_with_error t("owners.mfa_required"), :forbidden + end +end diff --git a/app/controllers/rubygems_controller.rb b/app/controllers/rubygems_controller.rb index e5a9ef07d30..2f9722660e0 100644 --- a/app/controllers/rubygems_controller.rb +++ b/app/controllers/rubygems_controller.rb @@ -23,6 +23,9 @@ def index def show @versions = @rubygem.public_versions.limit(5) @adoption = @rubygem.ownership_call + + flash[:notice] = t(".archived_notice") if @rubygem.archived? + if @versions.to_a.any? render "show" else diff --git a/app/helpers/rubygems_helper.rb b/app/helpers/rubygems_helper.rb index 7898a9e9475..f3720ab5342 100644 --- a/app/helpers/rubygems_helper.rb +++ b/app/helpers/rubygems_helper.rb @@ -97,6 +97,14 @@ def rubygem_trusted_publishers_link(rubygem) link_to t("rubygems.aside.links.trusted_publishers"), rubygem_trusted_publishers_path(rubygem.slug), class: "gem__link t-list__item" end + def rubygems_archive_link(rubygem) + link_to t("rubges.aside.links.archive"), rubygems_archive_path(rubygem.slug), class: "gem__link t-list__item", method: "post" + end + + def rubygems_unarchive_link(rubygem) + link_to t("rubges.aside.links.unarchive"), rubygems_archive_path(rubygem.slug), class: "gem__link t-list__item", method: "delete" + end + def oidc_api_key_role_links(rubygem) roles = current_user.oidc_api_key_roles.for_rubygem(rubygem) diff --git a/app/models/api_key.rb b/app/models/api_key.rb index d0a4c113ad0..c51d9cf595e 100644 --- a/app/models/api_key.rb +++ b/app/models/api_key.rb @@ -1,9 +1,10 @@ class ApiKey < ApplicationRecord class ScopeError < RuntimeError; end - API_SCOPES = %i[show_dashboard index_rubygems push_rubygem yank_rubygem add_owner remove_owner access_webhooks + API_SCOPES = %i[show_dashboard index_rubygems push_rubygem yank_rubygem archive_rubygem unarchive_rubygem add_owner remove_owner access_webhooks configure_trusted_publishers].freeze - APPLICABLE_GEM_API_SCOPES = %i[push_rubygem yank_rubygem add_owner remove_owner configure_trusted_publishers].freeze + APPLICABLE_GEM_API_SCOPES = %i[push_rubygem yank_rubygem archive_rubygem unarchive_rubygem add_owner remove_owner + configure_trusted_publishers].freeze EXCLUSIVE_SCOPES = %i[show_dashboard].freeze self.ignored_columns += API_SCOPES diff --git a/app/models/pusher.rb b/app/models/pusher.rb index 6b256ce230a..0ad35ecd069 100644 --- a/app/models/pusher.rb +++ b/app/models/pusher.rb @@ -18,7 +18,7 @@ def initialize(api_key, body, request: nil) def process trace("gemcutter.pusher.process", tags: { "gemcutter.api_key.owner" => owner.to_gid }) do - pull_spec && find && authorize && verify_gem_scope && verify_mfa_requirement && validate && save + pull_spec && find && authorize && verify_gem_scope && verify_not_archived && verify_mfa_requirement && validate && save end end @@ -32,6 +32,12 @@ def verify_gem_scope notify("This API key cannot perform the specified action on this gem.", 403) end + def verify_not_archived + return true unless rubygem.archived? + + notify("This gem has been archived, and is in a read-only state.", 401) + end + def verify_mfa_requirement (!api_key.user? || owner.mfa_enabled?) || !(version_mfa_required? || rubygem.metadata_mfa_required?) || notify("Rubygem requires owners to enable MFA. You must enable MFA before pushing new version.", 403) diff --git a/app/models/rubygem.rb b/app/models/rubygem.rb index a306bcbfd31..b3e949f4990 100644 --- a/app/models/rubygem.rb +++ b/app/models/rubygem.rb @@ -123,6 +123,8 @@ def create_gem_download joins(:gem_download).order("MAX(gem_downloads.count) DESC").news(days) } + scope :unmaintained, -> { where(unmaintained: true) } + def self.letterize(letter) /\A[A-Za-z]\z/.match?(letter) ? letter.upcase : "A" end @@ -372,6 +374,22 @@ def linkable_verification_uri URI.join("https://rubygems.org/gems/", name) end + def archive!(actor) + update!( + archived: true, + archived_at: Time.current, + archived_by: actor.id + ) + end + + def unarchive! + update!( + archived: false, + archived_at: nil, + archived_by: nil + ) + end + private # a gem namespace is not protected if it is diff --git a/app/policies/api/rubygem_policy.rb b/app/policies/api/rubygem_policy.rb index e181f66850a..5fb1ce676da 100644 --- a/app/policies/api/rubygem_policy.rb +++ b/app/policies/api/rubygem_policy.rb @@ -39,4 +39,18 @@ def configure_trusted_publishers? api_key_scope?(:configure_trusted_publishers, rubygem) && user_authorized?(rubygem, :configure_trusted_publishers?) end + + def archive? + user_api_key? && + mfa_requirement_satisfied?(rubygem) && + api_key_scope?(:archive_rubygem, rubygem) && + user_authorized?(rubygem, :archive?) + end + + def unarchive? + user_api_key? && + mfa_requirement_satisfied?(rubygem) && + api_key_scope?(:unarchive_rubygem, rubygem) && + user_authorized?(rubygem, :unarchive?) + end end diff --git a/app/policies/rubygem_policy.rb b/app/policies/rubygem_policy.rb index 221c6d8d048..30eb15d4f6f 100644 --- a/app/policies/rubygem_policy.rb +++ b/app/policies/rubygem_policy.rb @@ -57,4 +57,12 @@ def add_owner? def remove_owner? rubygem_owned_by?(user) end + + def archive? + rubygem_owned_by?(user) + end + + def unarchive? + rubygem_owned_by?(user) + end end diff --git a/config/locales/de.yml b/config/locales/de.yml index fdbace32003..1d125e6a23b 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -786,6 +786,7 @@ de: show_all_versions: Zeige alle Versionen (%{count} total) versions_header: Versionen yanked_notice: + archived_notice: show_yanked: not_hosted_notice: Dieses Gem wird aktuell nicht auf RubyGems.org gehostet. reserved_namespace_html: @@ -1055,3 +1056,8 @@ de: api_key_gem_html: api_key_mfa: not_required: + archive: + create: + success: + destroy: + success: diff --git a/config/locales/en.yml b/config/locales/en.yml index b3c9b6c60fd..a8d4d143278 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -699,6 +699,7 @@ en: show_all_versions: Show all versions (%{count} total) versions_header: Versions yanked_notice: This version has been yanked, and it is not available for download directly or for other gems that may have depended on it. + archived_notice: This gem has been archived. It is now read-only and will not accept new versions. show_yanked: not_hosted_notice: This gem is not currently hosted on RubyGems.org. Yanked versions of this gem may already exist. reserved_namespace_html: @@ -976,3 +977,8 @@ en: api_key_gem_html: "Gem: %{gem}" api_key_mfa: "MFA: %{mfa}" not_required: "Not required" + archive: + create: + success: "%{gem} was successfully archvied" + destroy: + success: "%{gem} was Successfully unarchived" diff --git a/config/locales/es.yml b/config/locales/es.yml index 4c4f5d65958..789f79c3629 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -801,6 +801,7 @@ es: versions_header: Versiones yanked_notice: Esta versión fue borrada, y no está disponible para su descarga directa ni por otras gemas que puedan haber dependido de la misma. + archived_notice: show_yanked: not_hosted_notice: Esta gema no está alojada actualmente en RubyGems.org. Es posible que ya exista alguna versión borrada de esta gema. @@ -1110,3 +1111,8 @@ es: api_key_gem_html: api_key_mfa: not_required: + archive: + create: + success: + destroy: + success: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 84a029f80e7..57f8f36d046 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -724,6 +724,7 @@ fr: versions_header: Versions yanked_notice: 'Retrait de Gem : disponible ni directement, ni pour les gems qui en dépendraient.' + archived_notice: show_yanked: not_hosted_notice: Gem non hébergé sur Rubygems pour le moment. reserved_namespace_html: @@ -1005,3 +1006,8 @@ fr: api_key_gem_html: api_key_mfa: not_required: + archive: + create: + success: + destroy: + success: diff --git a/config/locales/ja.yml b/config/locales/ja.yml index a2c008f9079..00a735b4e02 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -693,6 +693,7 @@ ja: show_all_versions: 全てのバージョンを表示(全%{count}件) versions_header: バージョン履歴 yanked_notice: このバージョンはヤンクされ、直接のダウンロードや依存関係になっている可能性がある他のgemは利用できません。 + archived_notice: show_yanked: not_hosted_notice: このgemは現在RubyGems.org上ではホストされていません。このgemのヤンクされたバージョンはまだ存在する可能性があります。 reserved_namespace_html: @@ -981,3 +982,8 @@ ja: api_key_gem_html: 'gem: %{gem}' api_key_mfa: 'MFA: %{mfa}' not_required: 必要ではありません + archive: + create: + success: + destroy: + success: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 2c41e12c4f2..e38166b8ef5 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -691,6 +691,7 @@ nl: show_all_versions: Toon alle versies (%{count} totaal) versions_header: Versies yanked_notice: + archived_notice: show_yanked: not_hosted_notice: Deze gem wordt momenteel niet gehost op rubygems.org. reserved_namespace_html: @@ -960,3 +961,8 @@ nl: api_key_gem_html: api_key_mfa: not_required: + archive: + create: + success: + destroy: + success: diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 34a5748e8d5..ec67093ab98 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -704,6 +704,7 @@ pt-BR: show_all_versions: Mostrar todas as versões (%{count}) versions_header: Versões yanked_notice: Esta gem foi removida, e não está mais disponível para download. + archived_notice: show_yanked: not_hosted_notice: Esta gem não está hospedada no Gemcutter. reserved_namespace_html: @@ -983,3 +984,8 @@ pt-BR: api_key_gem_html: api_key_mfa: not_required: + archive: + create: + success: + destroy: + success: diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 7bac93b6af7..7b895bd4917 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -699,6 +699,7 @@ zh-CN: show_all_versions: 显示所有版本 (共 %{count} 个) versions_header: 版本列表 yanked_notice: 这个 Gem 版本已经撤回了,无法直接下载,也无法被其他 Gem 依赖。 + archived_notice: show_yanked: not_hosted_notice: 这个 Gem 目前没有被托管在 RubyGems.org 中。这个 Gem 撤回的版本可能已经存在了。 reserved_namespace_html: @@ -974,3 +975,8 @@ zh-CN: api_key_gem_html: api_key_mfa: not_required: + archive: + create: + success: + destroy: + success: diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 3dca89fcae8..78801a4cf32 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -693,6 +693,7 @@ zh-TW: show_all_versions: 顯示所有版本(共 %{count}) versions_header: 版本列表 yanked_notice: 這個 Gem 版本已被移除,因此無法提供下載,也無法被其他的 Gem 相依。 + archived_notice: show_yanked: not_hosted_notice: 這個 Gem 目前沒有在 RubyGems.org 上 reserved_namespace_html: @@ -965,3 +966,8 @@ zh-TW: api_key_gem_html: api_key_mfa: not_required: + archive: + create: + success: + destroy: + success: diff --git a/config/routes.rb b/config/routes.rb index fd5871e8c0c..8c5ae359dd2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -89,6 +89,9 @@ constraints rubygem_id: Patterns::ROUTE_PATTERN do resource :owners, only: %i[show create destroy] resources :trusted_publishers, controller: 'oidc/rubygem_trusted_publishers', only: %i[index create destroy show] + + post :archive, to: "archive#create" + delete :archive, to: "archive#destroy" end end @@ -223,6 +226,10 @@ end resources :adoptions, only: %i[index] resources :trusted_publishers, controller: 'oidc/rubygem_trusted_publishers', only: %i[index create destroy new] + + get 'archive', to: 'archive#show' + post 'archive', to: 'archive#create' + delete 'archive', to: 'archive#destroy' end resources :ownership_calls, only: :index diff --git a/db/migrate/20240815014607_add_archived_to_rubygem.rb b/db/migrate/20240815014607_add_archived_to_rubygem.rb new file mode 100644 index 00000000000..afebe1cc82a --- /dev/null +++ b/db/migrate/20240815014607_add_archived_to_rubygem.rb @@ -0,0 +1,10 @@ +class AddArchivedToRubygem < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + add_column :rubygems, :archived, :boolean, default: false, null: false + add_column :rubygems, :archived_at, :datetime + add_column :rubygems, :archived_by, :integer + add_index :rubygems, :archived, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 8fbfefc8343..79f5de7d081 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_07_22_182907) do +ActiveRecord::Schema[7.1].define(version: 2024_08_15_014607) do # These are extensions that must be enabled in order to support this database enable_extension "hstore" enable_extension "pgcrypto" @@ -464,8 +464,12 @@ t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil t.boolean "indexed", default: false, null: false + t.boolean "archived", default: false, null: false + t.datetime "archived_at" + t.integer "archived_by" 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 ["archived"], name: "index_rubygems_on_archived" t.index ["indexed"], name: "index_rubygems_on_indexed" t.index ["name"], name: "index_rubygems_on_name", unique: true end diff --git a/test/factories/rubygem.rb b/test/factories/rubygem.rb index 78224af58d9..d53ff7dff5a 100644 --- a/test/factories/rubygem.rb +++ b/test/factories/rubygem.rb @@ -8,6 +8,12 @@ name + trait :archived do + archived_at { Time.current } + archived { true } + archived_by { 1 } + end + after(:build) do |rubygem, evaluator| if evaluator.linkset rubygem.linkset = evaluator.linkset diff --git a/test/functional/archive_controller_test.rb b/test/functional/archive_controller_test.rb new file mode 100644 index 00000000000..9761fdf4b35 --- /dev/null +++ b/test/functional/archive_controller_test.rb @@ -0,0 +1,71 @@ +require "test_helper" + +class ArchiveControllerTest < ActionController::TestCase + setup do + @user = create(:user) + @rubygem = create(:rubygem) + @ownership = create(:ownership, rubygem: @rubygem, user: @user) + end + + context "on POST to create" do + context "when not signed in" do + setup do + post :create, params: { rubygem_id: "a_gem" } + end + + should "redirect to sign in page" do + assert_redirected_to sign_in_path + end + end + + context "when the user is signed in and authorized" do + setup do + verified_sign_in_as(@user) + post :create, params: { rubygem_id: @rubygem.name } + end + + should "archive the gem" do + assert_predicate @rubygem.reload, :archived? + end + + should "redirect the user to the gem page" do + assert_redirected_to rubygem_path(@rubygem) + end + + should "set a succesfull notice message" do + assert_equal I18n.t("archive.create.success"), flash[:notice] + end + end + end + + context "on DELETE to destroy" do + context "when the user is not signed in" do + setup do + delete :destroy, params: { rubygem_id: @rubygem.name } + end + + should "redirect to sign in page" do + assert_redirected_to sign_in_path + end + end + + context "when the user is signed in and authorized" do + setup do + verified_sign_in_as(@user) + delete :destroy, params: { rubygem_id: @rubygem.name } + end + + should "unarchive the gem" do + assert_not @rubygem.reload.archived? + end + + should "redirect the user to the gem page" do + assert_redirected_to rubygem_path(@rubygem) + end + + should "set a successfull notice message" do + assert_equal I18n.t("archive.destroy.success"), flash[:notice] + end + end + end +end diff --git a/test/functional/rubygems_controller_test.rb b/test/functional/rubygems_controller_test.rb index 0bf2ff07e61..f07faff17bb 100644 --- a/test/functional/rubygems_controller_test.rb +++ b/test/functional/rubygems_controller_test.rb @@ -414,6 +414,17 @@ class RubygemsControllerTest < ActionController::TestCase end end + context "on GET to show for an archived gem" do + setup do + @rubygem = create(:rubygem, :archived) + get :show, params: { id: @rubygem.slug } + end + + should "render a notice indicating the gem has been archived" do + assert page.has_content? I18n.t("rubygems.show.archived_notice") + end + end + context "When not logged in" do context "On GET to show for a gem" do setup do diff --git a/test/integration/api/v1/archive_test.rb b/test/integration/api/v1/archive_test.rb new file mode 100644 index 00000000000..d4ea9f4d14c --- /dev/null +++ b/test/integration/api/v1/archive_test.rb @@ -0,0 +1,77 @@ +require "test_helper" + +class Api::V1::ArchiveTest < ActionDispatch::IntegrationTest + setup do + @key = "12345" + @user = create(:user) + @rubygem = create(:rubygem) + @ownership = create(:ownership, user: @user, rubygem: @rubygem) + create(:api_key, owner: @user, key: @key, scopes: %i[archive_rubygem unarchive_rubygem]) + end + + context "on POST to create" do + context "when the user is not authorized" do + setup do + post api_v1_rubygem_archive_path(@rubygem.name) + end + + should "respond with HTTP 401" do + assert_response :unauthorized + end + + should "not archive the gem" do + refute_predicate @rubygem, :archived? + end + end + + context "when the user is authenicated and authorized" do + setup do + post api_v1_rubygem_archive_path(@rubygem.name), + headers: { HTTP_AUTHORIZATION: @key } + end + + should "respond with HTTP 200" do + assert_response :success + end + + should "render a success message" do + assert_equal "#{@rubygem.name} was succesfully archived.", response.body + end + end + end + + context "on DELETE to destroy" do + context "when the user is unauthenticated" do + setup do + delete api_v1_rubygem_archive_path(@rubygem.name) + end + + should "respond with HTTP 401" do + assert_response :unauthorized + end + + should "not archive the gem" do + assert_not @rubygem.archived? + end + end + + context "when the user is authenticated and authorized" do + setup do + delete api_v1_rubygem_archive_path(@rubygem.name), + headers: { HTTP_AUTHORIZATION: @key } + end + + should "respond with HTTP 200" do + assert_response :success + end + + should "unarchive the gem" do + refute_predicate @rubygem, :archived? + end + + should "render a success message" do + assert_equal "#{@rubygem.name} was succesfully unarchived.", response.body + end + end + end +end diff --git a/test/models/pusher_test.rb b/test/models/pusher_test.rb index ae382a07e38..9bf64fe8776 100644 --- a/test/models/pusher_test.rb +++ b/test/models/pusher_test.rb @@ -47,6 +47,7 @@ class PusherTest < ActiveSupport::TestCase @cutter.stubs(:authorize).returns true @cutter.stubs(:verify_mfa_requirement).returns true @cutter.stubs(:verify_gem_scope).returns true + @cutter.stubs(:verify_not_archived).returns true @cutter.stubs(:validate).returns true @cutter.stubs(:save) @@ -104,6 +105,7 @@ class PusherTest < ActiveSupport::TestCase @cutter.stubs(:authorize).returns true @cutter.stubs(:verify_gem_scope).returns true @cutter.stubs(:verify_mfa_requirement).returns false + @cutter.stubs(:verify_not_archived).returns true @cutter.stubs(:validate).never @cutter.stubs(:save).never @@ -116,6 +118,7 @@ class PusherTest < ActiveSupport::TestCase @cutter.stubs(:authorize).returns true @cutter.stubs(:verify_gem_scope).returns true @cutter.stubs(:verify_mfa_requirement).returns true + @cutter.stubs(:verify_not_archived).returns true @cutter.stubs(:validate).returns false @cutter.stubs(:save).never @@ -844,4 +847,21 @@ def two_cert_chain(signing_key:, root_not_before: Time.current, cert_not_before: RubygemFs.mock! end end + + context "the gem has been archived" do + setup do + @rubygem = create(:rubygem, :archived, name: "test") + + Gem::Specification.any_instance.stubs(:authors).returns(["user@example.com"]) + + @gem = gem_file("test-1.0.0.gem") + @cutter = Pusher.new(@api_key, @gem) + end + + should "not not process the gem" do + refute @cutter.process + assert_equal "This gem has been archived, and is in a read-only state.", @cutter.message + assert_equal 401, @cutter.code + end + end end diff --git a/test/models/rubygem_test.rb b/test/models/rubygem_test.rb index 68626ff4a44..3dca33a9ff5 100644 --- a/test/models/rubygem_test.rb +++ b/test/models/rubygem_test.rb @@ -1180,4 +1180,29 @@ class RubygemTest < ActiveSupport::TestCase refute_predicate @version_three, :yanked? end end + + context "#archive!" do + setup do + @user = create(:user) + @rubygem = create(:rubygem) + end + + should "set the archived flag" do + @rubygem.archive!(@user) + + assert_predicate @rubygem, :archived? + end + end + + context "#unarchive!" do + setup do + @rubygem = create(:rubygem) + end + + should "unset the archived flag" do + @rubygem.unarchive! + + refute_predicate @rubygem, :archived? + end + end end diff --git a/test/policies/rubygem_policy_test.rb b/test/policies/rubygem_policy_test.rb index 25b4d473d55..602452c2df1 100644 --- a/test/policies/rubygem_policy_test.rb +++ b/test/policies/rubygem_policy_test.rb @@ -79,4 +79,20 @@ def policy!(user) refute_authorized nil, :show_unconfirmed_ownerships? end end + + context "#archive?" do + should "only allow the owner" do + assert_authorized @owner, :archive? + refute_authorized @user, :archive? + refute_authorized nil, :archive? + end + end + + context "#unarchive?" do + should "only allow the owner" do + assert_authorized @owner, :unarchive? + refute_authorized @user, :unarchive? + refute_authorized nil, :unarchive? + end + end end