From e0beafd2881b67303964e35069f4c54175aec050 Mon Sep 17 00:00:00 2001 From: Robert Blakey Date: Fri, 7 Jun 2024 19:07:16 -0400 Subject: [PATCH 1/3] feat: add rswag cop --- config/default.yml | 7 ++ lib/rubocop/cop/betterment.rb | 1 + lib/rubocop/cop/betterment/not_using_rswag.rb | 34 +++++++++ .../cop/betterment/not_using_rswag_spec.rb | 74 +++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 lib/rubocop/cop/betterment/not_using_rswag.rb create mode 100644 spec/rubocop/cop/betterment/not_using_rswag_spec.rb diff --git a/config/default.yml b/config/default.yml index f901284..bcd9db7 100644 --- a/config/default.yml +++ b/config/default.yml @@ -100,6 +100,13 @@ Betterment/UnscopedFind: FactoryBot/AssociationStyle: Enabled: false +Betterment/NotUsingRswag: + Enabled: false + SafeAutoCorrect: false + Description: Detect API specs missing OpenAPI documentation using rswag + Include: + - spec/requests/**/*_spec.rb + FactoryBot/ConsistentParenthesesStyle: Enabled: false diff --git a/lib/rubocop/cop/betterment.rb b/lib/rubocop/cop/betterment.rb index c465785..4e4f574 100644 --- a/lib/rubocop/cop/betterment.rb +++ b/lib/rubocop/cop/betterment.rb @@ -24,3 +24,4 @@ require 'rubocop/cop/betterment/fetch_boolean' require 'rubocop/cop/betterment/render_status' require 'rubocop/cop/betterment/redirect_status' +require 'rubocop/cop/betterment/not_using_rswag' diff --git a/lib/rubocop/cop/betterment/not_using_rswag.rb b/lib/rubocop/cop/betterment/not_using_rswag.rb new file mode 100644 index 0000000..4fb930e --- /dev/null +++ b/lib/rubocop/cop/betterment/not_using_rswag.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Betterment + class NotUsingRswag < Base + MSG = 'API tests should use documented using rswag and not the built in `get`, `post`, `put`, `patch`, `delete` methods' + + # @!method test?(node) + def_node_matcher :test?, <<-PATTERN + (block (send nil? :it _) ...) + PATTERN + + # @!method shared_method?(node) + def_node_matcher :shared_method?, <<-PATTERN + (def ...) + PATTERN + + # @!method before_block?(node) + def_node_matcher :before_block?, <<-PATTERN + (block (send nil? :before) ...) + PATTERN + + RESTRICT_ON_SEND = %i(get put patch post delete).freeze + + def on_send(node) + return unless node.ancestors.any? { |a| test?(a) || shared_method?(a) || before_block?(a) } + + add_offense(node) + end + end + end + end +end diff --git a/spec/rubocop/cop/betterment/not_using_rswag_spec.rb b/spec/rubocop/cop/betterment/not_using_rswag_spec.rb new file mode 100644 index 0000000..55bbc04 --- /dev/null +++ b/spec/rubocop/cop/betterment/not_using_rswag_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe RuboCop::Cop::Betterment::NotUsingRswag, :config do + let(:error_message) do + 'API tests should use documented using rswag and not the built in `get`, `post`, `put`, `patch`, `delete` methods' + end + + %w(get put patch post delete).each do |method_name| + it "rejects using the built in #{method_name} method in a test" do + expect_offense(<<~RUBY) + RSpec.describe MyApiController do + it 'returns ok status with expected response' do + get "/api/widgets/1" + ^^^^^^^^^^^^^^^^^^^^ #{error_message} + expect(response).to have_http_status :ok + expect(response_json).to eq accepted_response.as_json + end + end + RUBY + end + + it "rejects using the built in #{method_name} method in a shared method" do + expect_offense(<<~RUBY) + RSpec.describe MyApiController do + def make_get_request + get "/api/widgets/1" + ^^^^^^^^^^^^^^^^^^^^ #{error_message} + end + + it 'returns ok status with expected response' do + make_get_request + + expect(response).to have_http_status :ok + expect(response_json).to eq accepted_response.as_json + end + end + RUBY + end + + it "rejects using the built in #{method_name} method in a before block" do + expect_offense(<<~RUBY) + RSpec.describe MyApiController do + before do + get "/api/widgets/1" + ^^^^^^^^^^^^^^^^^^^^ #{error_message} + end + + it 'returns ok status with expected response' do + expect(response).to have_http_status :ok + expect(response_json).to eq accepted_response.as_json + end + end + RUBY + end + + it "accepts using the rswag #{method_name} method in the example setup" do + expect_no_offenses(<<~RUBY) + RSpec.describe MyApiController do + path "/api/widgets/1" do + get 'shows a foo widget' do + + it 'returns ok status with expected response' do + expect(response).to have_http_status :ok + expect(response_json).to eq accepted_response.as_json + end + end + end + end + RUBY + end + end +end From 472aaac02d9640d3b658ef082bd6bdc570d6740b Mon Sep 17 00:00:00 2001 From: Robert Blakey Date: Wed, 20 Nov 2024 00:45:52 -0500 Subject: [PATCH 2/3] fix: update to confirm correct structure --- lib/rubocop/cop/betterment/not_using_rswag.rb | 74 +++++++--- .../cop/betterment/not_using_rswag_spec.rb | 129 +++++++++++------- 2 files changed, 135 insertions(+), 68 deletions(-) diff --git a/lib/rubocop/cop/betterment/not_using_rswag.rb b/lib/rubocop/cop/betterment/not_using_rswag.rb index 4fb930e..4890309 100644 --- a/lib/rubocop/cop/betterment/not_using_rswag.rb +++ b/lib/rubocop/cop/betterment/not_using_rswag.rb @@ -4,30 +4,70 @@ module RuboCop module Cop module Betterment class NotUsingRswag < Base - MSG = 'API tests should use documented using rswag and not the built in `get`, `post`, `put`, `patch`, `delete` methods' + MSG = 'Ensure request spec conforms to the required structure.' - # @!method test?(node) - def_node_matcher :test?, <<-PATTERN - (block (send nil? :it _) ...) - PATTERN + def on_new_investigation + return if processed_source.ast.nil? - # @!method shared_method?(node) - def_node_matcher :shared_method?, <<-PATTERN - (def ...) - PATTERN + unless valid_path_structure?(processed_source.ast) + add_offense(processed_source.ast) + end + end - # @!method before_block?(node) - def_node_matcher :before_block?, <<-PATTERN - (block (send nil? :before) ...) - PATTERN + private - RESTRICT_ON_SEND = %i(get put patch post delete).freeze + def valid_path_structure?(node) + node.each_descendant.any? do |descendant| + next unless path_node?(descendant) - def on_send(node) - return unless node.ancestors.any? { |a| test?(a) || shared_method?(a) || before_block?(a) } + find_method_and_response_nodes(descendant) + end + end + + def find_method_and_response_nodes(node) + method_found = false + response_found = false - add_offense(node) + node.each_descendant do |descendant| + method_found ||= method_node?(descendant) + response_found ||= response_node?(descendant) + break if method_found && response_found + end + + method_found && response_found end + + # @!method path_with_code?(node) + def_node_matcher :path_with_code?, <<~PATTERN + (block + (send nil? :path (str _)) + args + !nil?) + PATTERN + + # @!method path_node?(node) + def_node_matcher :path_node?, <<~PATTERN + (block + (send nil? :path (str _)) + args + _) + PATTERN + + # @!method method_node?(node) + def_node_matcher :method_node?, <<~PATTERN + (block + (send nil? {:get :post :put :patch :delete} str) + args + _) + PATTERN + + # @!method response_node?(node) + def_node_matcher :response_node?, <<~PATTERN + (block + (send nil? :response str str) + args + _) + PATTERN end end end diff --git a/spec/rubocop/cop/betterment/not_using_rswag_spec.rb b/spec/rubocop/cop/betterment/not_using_rswag_spec.rb index 55bbc04..fd1360e 100644 --- a/spec/rubocop/cop/betterment/not_using_rswag_spec.rb +++ b/spec/rubocop/cop/betterment/not_using_rswag_spec.rb @@ -2,73 +2,100 @@ require 'spec_helper' -describe RuboCop::Cop::Betterment::NotUsingRswag, :config do - let(:error_message) do - 'API tests should use documented using rswag and not the built in `get`, `post`, `put`, `patch`, `delete` methods' +RSpec.describe RuboCop::Cop::Betterment::NotUsingRswag, :config do + it 'does not register and offense on an empty syntax tree' do + expect_no_offenses("") end - %w(get put patch post delete).each do |method_name| - it "rejects using the built in #{method_name} method in a test" do - expect_offense(<<~RUBY) - RSpec.describe MyApiController do - it 'returns ok status with expected response' do - get "/api/widgets/1" - ^^^^^^^^^^^^^^^^^^^^ #{error_message} - expect(response).to have_http_status :ok - expect(response_json).to eq accepted_response.as_json + it 'registers an offense when no path is found' do + expect_offense(<<~RUBY) + RSpec.describe MyApiController do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ensure request spec conforms to the required structure. + describe 'A request spec' do + context 'some situation' do + it 'does something' do + expect(true).to be_truthy + end end end - RUBY - end - - it "rejects using the built in #{method_name} method in a shared method" do - expect_offense(<<~RUBY) - RSpec.describe MyApiController do - def make_get_request - get "/api/widgets/1" - ^^^^^^^^^^^^^^^^^^^^ #{error_message} - end - - it 'returns ok status with expected response' do - make_get_request + end + RUBY + end - expect(response).to have_http_status :ok - expect(response_json).to eq accepted_response.as_json + it 'does not register an offense for valid structure' do + expect_no_offenses(<<~RUBY) + RSpec.describe MyApiController do + path '/blogs' do + get 'Creates a blog' do + response '201', 'blog created' do + end end end - RUBY - end + end + RUBY + end - it "rejects using the built in #{method_name} method in a before block" do - expect_offense(<<~RUBY) - RSpec.describe MyApiController do - before do - get "/api/widgets/1" - ^^^^^^^^^^^^^^^^^^^^ #{error_message} + it 'registers an offense if the method and response nodes are missing' do + expect_offense(<<~RUBY) + RSpec.describe MyApiController do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ensure request spec conforms to the required structure. + path '/blogs' do + context 'some situation' do + it 'does something else' do + expect(true).to be_truthy + end end + end + end + RUBY + end - it 'returns ok status with expected response' do - expect(response).to have_http_status :ok - expect(response_json).to eq accepted_response.as_json + it 'does not register an offense for nested contexts with valid structure' do + expect_no_offenses(<<~RUBY) + RSpec.describe MyApiController do + path '/blogs' do + context 'some situation' do + get 'Creates a blog' do + context 'another situation' do + response '201', 'blog created' do + end + end + end end end - RUBY - end - - it "accepts using the rswag #{method_name} method in the example setup" do - expect_no_offenses(<<~RUBY) - RSpec.describe MyApiController do - path "/api/widgets/1" do - get 'shows a foo widget' do + end + RUBY + end - it 'returns ok status with expected response' do - expect(response).to have_http_status :ok - expect(response_json).to eq accepted_response.as_json + it 'registers an offense if the response node is missing even with nested contexts' do + expect_offense(<<~RUBY) + RSpec.describe MyApiController do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ensure request spec conforms to the required structure. + path '/blogs' do + context 'some situation' do + get 'Creates a blog' do + context 'another situation' do + it 'does something else' do + expect(true).to be_truthy + end end end end end - RUBY - end + end + RUBY + end + + it 'registers if http method calls are happening within "it" examples' do + expect_offense(<<~RUBY) + RSpec.describe MyApiController do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ensure request spec conforms to the required structure. + it 'returns ok status with expected response' do + get "/api/widgets/1" + expect(response).to have_http_status :ok + expect(response_json).to eq accepted_response.as_json + end + end + RUBY end end From b53849f68c987b95b5af3e21e7aa95cab07f58b5 Mon Sep 17 00:00:00 2001 From: Robert Blakey Date: Thu, 5 Dec 2024 15:45:22 -0500 Subject: [PATCH 3/3] test: add expectation for rswag and non-rswag docs --- .../cop/betterment/not_using_rswag_spec.rb | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/spec/rubocop/cop/betterment/not_using_rswag_spec.rb b/spec/rubocop/cop/betterment/not_using_rswag_spec.rb index fd1360e..d2f0431 100644 --- a/spec/rubocop/cop/betterment/not_using_rswag_spec.rb +++ b/spec/rubocop/cop/betterment/not_using_rswag_spec.rb @@ -50,6 +50,29 @@ RUBY end + it 'registers an no offense if method and response nodes are present in parallel with non-rswag context' do + expect_no_offenses(<<~RUBY) + RSpec.describe MyApiController do + path '/blogs' do + context 'some situation' do + get 'Creates a blog' do + context 'another situation' do + response '201', 'blog created' do + end + end + end + end + end + + it 'returns ok status with expected response' do + get "/api/widgets/1" + expect(response).to have_http_status :ok + expect(response_json).to eq accepted_response.as_json + end + end + RUBY + end + it 'does not register an offense for nested contexts with valid structure' do expect_no_offenses(<<~RUBY) RSpec.describe MyApiController do