1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
|
# frozen_string_literal: true
require 'rack/utils'
require 'rubocop-rspec'
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::Base
extend RuboCop::Cop::AutoCorrector
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
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), &corrector(node))
end
def offense_for_response_status(node)
return unless response_status_eq?(node)
add_offense(node, message: message_for(MSG_RESPONSE_STATUS), &corrector(node))
end
def corrector(node)
->(corrector) do
replacement = replace_matcher(node) || replace_response_status(node)
corrector.replace(node.source_range, replacement) if node.source_range.source != replacement
end
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
|