From c6107c22a8940d80d368008e82ffb0aa4286b99c Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 14 Jan 2026 14:36:11 -0500 Subject: [PATCH 1/4] Enable mailer previews in staging environment for enhanced testing - Configured the action mailer to show previews and set the preview path for mailer views. - This addition allows for easier testing of email layouts and content before sending, improving the development workflow. --- config/environments/staging.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/environments/staging.rb b/config/environments/staging.rb index b86d6913..75ab1276 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -58,6 +58,10 @@ config.action_mailer.delivery_method = :letter_opener_web config.action_mailer.perform_deliveries = true +# Enable mailer previews (protected by authorization in config/initializers/mailer_previews.rb) + config.action_mailer.show_previews = true + config.action_mailer.preview_path = "#{Rails.root}/test/mailers/previews" + # I18n config.i18n.fallbacks = true From 00abb0866c29dd5c4791b15d19134681bb526ec4 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 14 Jan 2026 14:55:25 -0500 Subject: [PATCH 2/4] Remove custom preview path for action mailer in staging environment - Eliminated the specific preview path configuration for action mailer, allowing the default behavior to be used. - This change simplifies the configuration and aligns with standard Rails practices for mailer previews. --- config/environments/staging.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 75ab1276..51ad6747 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -60,7 +60,6 @@ # Enable mailer previews (protected by authorization in config/initializers/mailer_previews.rb) config.action_mailer.show_previews = true - config.action_mailer.preview_path = "#{Rails.root}/test/mailers/previews" # I18n config.i18n.fallbacks = true From 186a750cc473dba0b888d376c6dec7dd1a45e941 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:03:10 +0000 Subject: [PATCH 3/4] Bump undici in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [undici](https://github.com/nodejs/undici). Updates `undici` from 6.21.3 to 6.23.0 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v6.21.3...v6.23.0) --- updated-dependencies: - dependency-name: undici dependency-version: 6.23.0 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6333b99b..35d5af1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3891,9 +3891,9 @@ undici-types@~6.20.0: integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== undici@^6.16.1: - version "6.21.3" - resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.3.tgz#185752ad92c3d0efe7a7d1f6854a50f83b552d7a" - integrity sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw== + version "6.23.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.23.0.tgz#7953087744d9095a96f115de3140ca3828aff3a4" + integrity sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" From 22e9a01e622536b43ba42917862e993e2b214ad3 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Mon, 19 Jan 2026 14:09:53 -0500 Subject: [PATCH 4/4] Refactor toggle_disqualified action in EntriesController for improved authorization - Introduced a new before_action to handle entry retrieval specifically for the toggle_disqualified action, ensuring that authorization checks are correctly applied. - Updated the authorization call in the toggle_disqualified method to use the new policy method, enhancing security and clarity. - Added comprehensive tests for the toggle_disqualified action, covering various user roles and their permissions, ensuring robust access control. - Enhanced entry_policy_spec to include tests for the toggle_disqualified action, verifying permissions for different user roles. These changes improve the maintainability and security of the EntriesController while ensuring proper authorization logic is enforced. --- app/controllers/entries_controller.rb | 11 +- spec/controllers/entries_controller_spec.rb | 169 ++++++++++++++++++++ spec/policies/entry_policy_spec.rb | 80 +++++++++ 3 files changed, 258 insertions(+), 2 deletions(-) diff --git a/app/controllers/entries_controller.rb b/app/controllers/entries_controller.rb index edab4b6c..f3fcbd35 100644 --- a/app/controllers/entries_controller.rb +++ b/app/controllers/entries_controller.rb @@ -1,6 +1,7 @@ class EntriesController < ApplicationController include AvailableContestsConcern - before_action :set_entry, only: %i[ show edit update destroy soft_delete toggle_disqualified modal_details ] + before_action :set_entry, only: %i[ show edit update destroy soft_delete modal_details ] + before_action :set_entry_for_toggle_disqualified, only: %i[ toggle_disqualified ] before_action :set_entry_for_profile, only: %i[ applicant_profile ] before_action :authorize_entry, only: %i[show edit update destroy] before_action :authorize_index, only: [ :index ] @@ -111,7 +112,7 @@ def soft_delete end def toggle_disqualified - authorize @entry + authorize @entry, :toggle_disqualified? @entry.toggle!(:disqualified) redirect_to request.referer || root_path, notice: 'Entry disqualification status has been updated.' end @@ -143,6 +144,12 @@ def set_entry @entry = policy_scope(Entry).find(params[:id]) end + # For toggle_disqualified, find the entry directly and let authorization handle access control + # This ensures container admins can toggle entries even if the scope doesn't include them + def set_entry_for_toggle_disqualified + @entry = Entry.find(params[:id]) + end + # For applicant_profile, we want to find the entry first, then authorize it def set_entry_for_profile @entry = policy_scope(Entry).find(params[:id]) diff --git a/spec/controllers/entries_controller_spec.rb b/spec/controllers/entries_controller_spec.rb index f40e9342..c61b0201 100644 --- a/spec/controllers/entries_controller_spec.rb +++ b/spec/controllers/entries_controller_spec.rb @@ -35,4 +35,173 @@ end end end + + describe "PATCH #toggle_disqualified" do + let(:container) { create(:container) } + let(:contest_description) { create(:contest_description, :active, container: container) } + let(:contest_instance) { create(:contest_instance, contest_description: contest_description) } + let(:profile) { create(:profile) } + let(:entry) { create(:entry, profile: profile, contest_instance: contest_instance, disqualified: false) } + + context "when user is a Container Administrator for the container" do + let(:admin_user) { create(:user) } + let(:admin_role) { create(:role, kind: 'Collection Administrator') } + + before do + create(:assignment, user: admin_user, container: container, role: admin_role) + sign_in admin_user + end + + it "toggles the disqualification status from false to true" do + expect { + patch :toggle_disqualified, params: { id: entry.id } + }.to change { entry.reload.disqualified }.from(false).to(true) + end + + it "toggles the disqualification status from true to false" do + entry.update(disqualified: true) + expect { + patch :toggle_disqualified, params: { id: entry.id } + }.to change { entry.reload.disqualified }.from(true).to(false) + end + + it "redirects to the referer" do + request.env['HTTP_REFERER'] = '/some/path' + patch :toggle_disqualified, params: { id: entry.id } + expect(response).to redirect_to('/some/path') + end + + it "redirects to root path when no referer" do + patch :toggle_disqualified, params: { id: entry.id } + expect(response).to redirect_to(root_path) + end + + it "sets a success notice message" do + patch :toggle_disqualified, params: { id: entry.id } + expect(flash[:notice]).to eq('Entry disqualification status has been updated.') + end + end + + context "when user is a Collection Manager for the container" do + let(:manager_user) { create(:user) } + let(:manager_role) { create(:role, kind: 'Collection Manager') } + + before do + create(:assignment, user: manager_user, container: container, role: manager_role) + sign_in manager_user + end + + it "toggles the disqualification status" do + expect { + patch :toggle_disqualified, params: { id: entry.id } + }.to change { entry.reload.disqualified }.from(false).to(true) + end + + it "sets a success notice message" do + patch :toggle_disqualified, params: { id: entry.id } + expect(flash[:notice]).to eq('Entry disqualification status has been updated.') + end + end + + context "when user is Axis Mundi" do + let(:axis_mundi_user) { create(:user, :axis_mundi) } + + before do + sign_in axis_mundi_user + end + + it "toggles the disqualification status" do + expect { + patch :toggle_disqualified, params: { id: entry.id } + }.to change { entry.reload.disqualified }.from(false).to(true) + end + + it "sets a success notice message" do + patch :toggle_disqualified, params: { id: entry.id } + expect(flash[:notice]).to eq('Entry disqualification status has been updated.') + end + end + + context "when user is a Container Administrator for a different container" do + let(:other_container) { create(:container) } + let(:other_admin_user) { create(:user) } + let(:admin_role) { create(:role, kind: 'Collection Administrator') } + + before do + create(:assignment, user: other_admin_user, container: other_container, role: admin_role) + sign_in other_admin_user + end + + it "does not toggle the disqualification status" do + expect { + patch :toggle_disqualified, params: { id: entry.id } + }.not_to change { entry.reload.disqualified } + end + + it "redirects with unauthorized message" do + patch :toggle_disqualified, params: { id: entry.id } + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq("!!! Not authorized !!!") + end + end + + context "when user is the entry owner" do + before do + sign_in profile.user + end + + it "does not toggle the disqualification status" do + expect { + patch :toggle_disqualified, params: { id: entry.id } + }.not_to change { entry.reload.disqualified } + end + + it "redirects with unauthorized message" do + patch :toggle_disqualified, params: { id: entry.id } + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq("!!! Not authorized !!!") + end + end + + context "when user is a judge assigned to the contest instance" do + let(:judge_user) { create(:user, :with_judge_role) } + + before do + create(:judging_assignment, user: judge_user, contest_instance: contest_instance) + sign_in judge_user + end + + it "does not toggle the disqualification status" do + expect { + patch :toggle_disqualified, params: { id: entry.id } + }.not_to change { entry.reload.disqualified } + end + + it "redirects with unauthorized message" do + patch :toggle_disqualified, params: { id: entry.id } + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq("!!! Not authorized !!!") + end + end + + context "when user has no special role" do + let(:regular_user) { create(:user) } + + before do + sign_in regular_user + end + + it "does not toggle the disqualification status" do + expect { + patch :toggle_disqualified, params: { id: entry.id } + }.not_to change { entry.reload.disqualified } + end + + it "redirects with unauthorized message" do + patch :toggle_disqualified, params: { id: entry.id } + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to eq("!!! Not authorized !!!") + end + end + end end diff --git a/spec/policies/entry_policy_spec.rb b/spec/policies/entry_policy_spec.rb index 292096c7..a4438937 100644 --- a/spec/policies/entry_policy_spec.rb +++ b/spec/policies/entry_policy_spec.rb @@ -60,4 +60,84 @@ let(:user) { create(:user, :axis_mundi) } it { is_expected.to permit_action(:show) } end + + describe "#toggle_disqualified?" do + let(:container) { contest_instance.contest_description.container } + + context "for a visitor" do + let(:user) { nil } + it { is_expected.to forbid_action(:toggle_disqualified) } + end + + context "for a Container Administrator in the same container" do + let(:user) { create(:user) } + let(:admin_role) { create(:role, kind: 'Collection Administrator') } + + before do + create(:assignment, user: user, container: container, role: admin_role) + end + + it { is_expected.to permit_action(:toggle_disqualified) } + end + + context "for a Collection Manager in the same container" do + let(:user) { create(:user) } + let(:manager_role) { create(:role, kind: 'Collection Manager') } + + before do + create(:assignment, user: user, container: container, role: manager_role) + end + + it { is_expected.to permit_action(:toggle_disqualified) } + end + + context "for Axis Mundi" do + let(:user) { create(:user, :axis_mundi) } + it { is_expected.to permit_action(:toggle_disqualified) } + end + + context "for a Container Administrator in a different container" do + let(:other_container) { create(:container) } + let(:user) { create(:user) } + let(:admin_role) { create(:role, kind: 'Collection Administrator') } + + before do + create(:assignment, user: user, container: other_container, role: admin_role) + end + + it { is_expected.to forbid_action(:toggle_disqualified) } + end + + context "for a Collection Manager in a different container" do + let(:other_container) { create(:container) } + let(:user) { create(:user) } + let(:manager_role) { create(:role, kind: 'Collection Manager') } + + before do + create(:assignment, user: user, container: other_container, role: manager_role) + end + + it { is_expected.to forbid_action(:toggle_disqualified) } + end + + context "for the entry owner" do + let(:user) { profile.user } + it { is_expected.to forbid_action(:toggle_disqualified) } + end + + context "for a judge assigned to the contest instance" do + let(:user) { create(:user, :with_judge_role) } + + before do + create(:judging_assignment, user: user, contest_instance: contest_instance) + end + + it { is_expected.to forbid_action(:toggle_disqualified) } + end + + context "for a regular user with no special role" do + let(:user) { create(:user) } + it { is_expected.to forbid_action(:toggle_disqualified) } + end + end end