155 lines
4.3 KiB
Ruby
155 lines
4.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rack/utils'
|
|
|
|
module RuboCop
|
|
module Cop
|
|
module RSpec
|
|
# This cops checks for `have_http_status` usages in specs.
|
|
# It also discourages the usage of numeric HTTP status codes in
|
|
# `have_gitlab_http_status`.
|
|
#
|
|
# @example
|
|
#
|
|
# # bad
|
|
# expect(response).to have_http_status(200)
|
|
# expect(response).to have_http_status(:ok)
|
|
# expect(response).to have_gitlab_http_status(200)
|
|
# expect(response.status).to eq(200)
|
|
# expect(response.status).not_to eq(200)
|
|
#
|
|
# # good
|
|
# expect(response).to have_gitlab_http_status(:ok)
|
|
# expect(response).not_to have_gitlab_http_status(:ok)
|
|
#
|
|
class HaveGitlabHttpStatus < RuboCop::Cop::Cop
|
|
CODE_TO_SYMBOL = Rack::Utils::SYMBOL_TO_STATUS_CODE.invert
|
|
|
|
MSG_MATCHER_NAME =
|
|
'Prefer `have_gitlab_http_status` over `have_http_status`.'
|
|
|
|
MSG_NUMERIC_STATUS =
|
|
'Prefer named HTTP status `%{name}` over ' \
|
|
'its numeric representation `%{code}`.'
|
|
|
|
MSG_RESPONSE_STATUS =
|
|
'Prefer `have_gitlab_http_status` matcher over ' \
|
|
'`response.status`.'
|
|
|
|
MSG_UNKNOWN_STATUS = 'HTTP status `%{code}` is unknown. ' \
|
|
'Please provide a valid one or disable this cop.'
|
|
|
|
MSG_DOCS_LINK = 'https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#have_gitlab_http_status'
|
|
|
|
REPLACEMENT = 'have_gitlab_http_status(%{arg})'
|
|
|
|
REPLACEMENT_RESPONSE_STATUS =
|
|
'expect(response).%{expectation} have_gitlab_http_status(%{arg})'
|
|
|
|
def_node_matcher :have_http_status?, <<~PATTERN
|
|
(send nil?
|
|
{ :have_http_status :have_gitlab_http_status }
|
|
_
|
|
)
|
|
PATTERN
|
|
|
|
def_node_matcher :response_status_eq?, <<~PATTERN
|
|
(send
|
|
(send nil? :expect
|
|
(send
|
|
(send nil? :response) :status)) ${ :to :not_to }
|
|
(send nil? :eq
|
|
(int $_)))
|
|
PATTERN
|
|
|
|
def on_send(node)
|
|
offense_for_matcher(node) || offense_for_response_status(node)
|
|
end
|
|
|
|
def autocorrect(node)
|
|
lambda do |corrector|
|
|
replacement = replace_matcher(node) || replace_response_status(node)
|
|
corrector.replace(node.source_range, replacement)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def offense_for_matcher(node)
|
|
return unless have_http_status?(node)
|
|
|
|
offenses = [
|
|
offense_for_name(node),
|
|
offense_for_status(node)
|
|
].compact
|
|
|
|
return if offenses.empty?
|
|
|
|
add_offense(node, message: message_for(*offenses))
|
|
end
|
|
|
|
def offense_for_response_status(node)
|
|
return unless response_status_eq?(node)
|
|
|
|
add_offense(node, message: message_for(MSG_RESPONSE_STATUS))
|
|
end
|
|
|
|
def replace_matcher(node)
|
|
return unless have_http_status?(node)
|
|
|
|
code = extract_numeric_code(node)
|
|
arg = code_to_symbol(code) || argument(node).source
|
|
|
|
format(REPLACEMENT, arg: arg)
|
|
end
|
|
|
|
def replace_response_status(node)
|
|
expectation, code = response_status_eq?(node)
|
|
return unless code
|
|
|
|
arg = code_to_symbol(code)
|
|
format(REPLACEMENT_RESPONSE_STATUS, expectation: expectation, arg: arg)
|
|
end
|
|
|
|
def offense_for_name(node)
|
|
return if method_name(node) == :have_gitlab_http_status
|
|
|
|
MSG_MATCHER_NAME
|
|
end
|
|
|
|
def offense_for_status(node)
|
|
code = extract_numeric_code(node)
|
|
return unless code
|
|
|
|
symbol = code_to_symbol(code)
|
|
return format(MSG_UNKNOWN_STATUS, code: code) unless symbol
|
|
|
|
format(MSG_NUMERIC_STATUS, name: symbol, code: code)
|
|
end
|
|
|
|
def message_for(*offenses)
|
|
(offenses + [MSG_DOCS_LINK]).join(' ')
|
|
end
|
|
|
|
def code_to_symbol(code)
|
|
CODE_TO_SYMBOL[code]&.inspect
|
|
end
|
|
|
|
def extract_numeric_code(node)
|
|
arg_node = argument(node)
|
|
return unless arg_node&.type == :int
|
|
|
|
arg_node.children[0]
|
|
end
|
|
|
|
def method_name(node)
|
|
node.children[1]
|
|
end
|
|
|
|
def argument(node)
|
|
node.children[2]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|