Welcome to mirror list, hosted at ThFree Co, Russian Federation.

have_gitlab_http_status.rb « rspec « cop « rubocop - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 86ece72b4f5c83d52b08a06a8dbdf902f7150a60 (plain)
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
# 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::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)
          lambda do |corrector|
            replacement = replace_matcher(node) || replace_response_status(node)
            corrector.replace(node.source_range, 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