diff --git a/CHANGELOG.md b/CHANGELOG.md index b55bcec..f1d0d69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Master (Unreleased) +- Add `RSpecRails/HttpStatusNameConsistency` cop. ([@taketo1113]) + ## 2.31.0 (2025-03-10) - Handle unknown HTTP status codes for `RSpecRails/HttpStatus` cop. ([@viralpraxis]) @@ -86,6 +88,7 @@ [@pirj]: https://github.com/pirj [@r7kamura]: https://github.com/r7kamura [@splattael]: https://github.com/splattael +[@taketo1113]: https://github.com/taketo1113 [@tmaier]: https://github.com/tmaier [@viralpraxis]: https://github.com/viralpraxis [@ydah]: https://github.com/ydah diff --git a/config/default.yml b/config/default.yml index 51ec888..7224693 100644 --- a/config/default.yml +++ b/config/default.yml @@ -35,6 +35,12 @@ RSpecRails/HttpStatus: VersionChanged: '2.20' Reference: https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/HttpStatus +RSpecRails/HttpStatusNameConsistency: + Description: Enforces consistency by using the current HTTP status names. + Enabled: pending + VersionAdded: "<>" + Reference: https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/HttpStatusNameConsistency + RSpecRails/InferredSpecType: Description: Identifies redundant spec type. Enabled: pending diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index b4fc11c..489ea9e 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -5,6 +5,7 @@ * xref:cops_rspecrails.adoc#rspecrailsavoidsetuphook[RSpecRails/AvoidSetupHook] * xref:cops_rspecrails.adoc#rspecrailshavehttpstatus[RSpecRails/HaveHttpStatus] * xref:cops_rspecrails.adoc#rspecrailshttpstatus[RSpecRails/HttpStatus] +* xref:cops_rspecrails.adoc#rspecrailshttpstatusnameconsistency[RSpecRails/HttpStatusNameConsistency] * xref:cops_rspecrails.adoc#rspecrailsinferredspectype[RSpecRails/InferredSpecType] * xref:cops_rspecrails.adoc#rspecrailsminitestassertions[RSpecRails/MinitestAssertions] * xref:cops_rspecrails.adoc#rspecrailsnegationbevalid[RSpecRails/NegationBeValid] diff --git a/docs/modules/ROOT/pages/cops_rspecrails.adoc b/docs/modules/ROOT/pages/cops_rspecrails.adoc index 07650f9..6ec8b30 100644 --- a/docs/modules/ROOT/pages/cops_rspecrails.adoc +++ b/docs/modules/ROOT/pages/cops_rspecrails.adoc @@ -211,6 +211,38 @@ it { is_expected.to have_http_status :ok } * https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/HttpStatus +[#rspecrailshttpstatusnameconsistency] +== RSpecRails/HttpStatusNameConsistency + +|=== +| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed + +| Pending +| Yes +| Always +| <> +| - +|=== + +Enforces consistency by using the current HTTP status names. + +[#examples-rspecrailshttpstatusnameconsistency] +=== Examples + +[source,ruby] +---- +# bad +it { is_expected.to have_http_status :unprocessable_entity } + +# good +it { is_expected.to have_http_status :unprocessable_content } +---- + +[#references-rspecrailshttpstatusnameconsistency] +=== References + +* https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/HttpStatusNameConsistency + [#rspecrailsinferredspectype] == RSpecRails/InferredSpecType diff --git a/lib/rubocop/cop/rspec_rails/http_status_name_consistency.rb b/lib/rubocop/cop/rspec_rails/http_status_name_consistency.rb new file mode 100644 index 0000000..b5cad2e --- /dev/null +++ b/lib/rubocop/cop/rspec_rails/http_status_name_consistency.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpecRails + # Enforces consistency by using the current HTTP status names. + # + # @example + # + # # bad + # it { is_expected.to have_http_status :unprocessable_entity } + # + # # good + # it { is_expected.to have_http_status :unprocessable_content } + # + class HttpStatusNameConsistency < ::RuboCop::Cop::Base + extend AutoCorrector + + requires_gem 'rack', '>= 3.1.0' + + MSG = 'Use `Prefer `:%s` over `:%s`.' + + RESTRICT_ON_SEND = %i[have_http_status].freeze + + PREFERRED_STATUSES = { + unprocessable_entity: :unprocessable_content, + payload_too_large: :content_too_large + }.freeze + + # @!method http_status(node) + def_node_matcher :http_status, <<~PATTERN + (send nil? :have_http_status ${sym}) + PATTERN + + def on_send(node) + http_status(node) do |arg| + check_status_name_consistency(arg) + end + end + alias on_csend on_send + + private + + def check_status_name_consistency(node) + return unless node.sym_type? && PREFERRED_STATUSES.key?(node.value) + + current_status = node.value + preferred_status = PREFERRED_STATUSES[current_status] + + message = format(MSG, current: current_status, + preferred: preferred_status) + + add_offense(node, message: message) do |corrector| + corrector.replace(node, ":#{preferred_status}") + end + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec_rails_cops.rb b/lib/rubocop/cop/rspec_rails_cops.rb index 1a101b3..59d200b 100644 --- a/lib/rubocop/cop/rspec_rails_cops.rb +++ b/lib/rubocop/cop/rspec_rails_cops.rb @@ -3,6 +3,7 @@ require_relative 'rspec_rails/avoid_setup_hook' require_relative 'rspec_rails/have_http_status' require_relative 'rspec_rails/http_status' +require_relative 'rspec_rails/http_status_name_consistency' require_relative 'rspec_rails/inferred_spec_type' require_relative 'rspec_rails/minitest_assertions' require_relative 'rspec_rails/negation_be_valid' diff --git a/spec/rubocop/cop/rspec_rails/http_status_name_consistency_spec.rb b/spec/rubocop/cop/rspec_rails/http_status_name_consistency_spec.rb new file mode 100644 index 0000000..0a6e261 --- /dev/null +++ b/spec/rubocop/cop/rspec_rails/http_status_name_consistency_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::RSpecRails::HttpStatusNameConsistency, :config do + context 'when Rack is older than 3.1' do + let(:gem_versions) { { 'rack' => '3.0.0' } } + + it 'does nothing when using :unprocessable_entity' do + expect_no_offenses(<<~RUBY) + it { is_expected.to have_http_status :unprocessable_entity } + RUBY + end + + it 'does nothing when using :payload_too_large' do + expect_no_offenses(<<~RUBY) + it { is_expected.to have_http_status :payload_too_large } + RUBY + end + end + + context 'when Rack is 3.1 or later' do + let(:gem_versions) { { 'rack' => '3.1.0' } } + + it 'registers an offense when using :unprocessable_entity' do + expect_offense(<<~RUBY) + it { is_expected.to have_http_status :unprocessable_entity } + ^^^^^^^^^^^^^^^^^^^^^ Use `Prefer `:unprocessable_content` over `:unprocessable_entity`. + RUBY + + expect_correction(<<~RUBY) + it { is_expected.to have_http_status :unprocessable_content } + RUBY + end + + it 'does not register an offense when using :unprocessable_content' do + expect_no_offenses(<<~RUBY) + it { is_expected.to have_http_status :unprocessable_content } + RUBY + end + + it 'registers an offense when using :payload_too_large' do + expect_offense(<<~RUBY) + it { is_expected.to have_http_status :payload_too_large } + ^^^^^^^^^^^^^^^^^^ Use `Prefer `:content_too_large` over `:payload_too_large`. + RUBY + + expect_correction(<<~RUBY) + it { is_expected.to have_http_status :content_too_large } + RUBY + end + + it 'does not register an offense when using :content_too_large' do + expect_no_offenses(<<~RUBY) + it { is_expected.to have_http_status :content_too_large } + RUBY + end + + it 'does nothing when using numeric value' do + expect_no_offenses(<<~RUBY) + it { is_expected.to have_http_status 200 } + RUBY + end + + it 'does nothing when using string value' do + expect_no_offenses(<<~RUBY) + it { is_expected.to have_http_status "200" } + RUBY + end + end +end