summaryrefslogtreecommitdiff
path: root/qa/qa/support/api.rb
blob: ea19d9ef3327c3cb18a072658d146d506a648177 (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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# frozen_string_literal: true

module QA
  module Support
    module API
      extend self

      HTTP_STATUS_OK = 200
      HTTP_STATUS_CREATED = 201
      HTTP_STATUS_NO_CONTENT = 204
      HTTP_STATUS_ACCEPTED = 202
      HTTP_STATUS_PERMANENT_REDIRECT = 308
      HTTP_STATUS_NOT_FOUND = 404
      HTTP_STATUS_TOO_MANY_REQUESTS = 429
      HTTP_STATUS_SERVER_ERROR = 500

      def post(url, payload, args = {})
        with_retry_on_too_many_requests do
          default_args = {
            method: :post,
            url: url,
            payload: payload,
            verify_ssl: false
          }

          RestClient::Request.execute(default_args.merge(with_canary(args)))
        rescue StandardError => e
          return_response_or_raise(e)
        end
      end

      def get(url, args = {})
        with_retry_on_too_many_requests do
          default_args = {
            method: :get,
            url: url,
            verify_ssl: false
          }

          RestClient::Request.execute(default_args.merge(with_canary(args)))
        rescue StandardError => e
          return_response_or_raise(e)
        end
      end

      def patch(url, payload = nil, args = {})
        with_retry_on_too_many_requests do
          default_args = {
            method: :patch,
            url: url,
            payload: payload,
            verify_ssl: false
          }

          RestClient::Request.execute(default_args.merge(with_canary(args)))
        rescue StandardError => e
          return_response_or_raise(e)
        end
      end

      def put(url, payload = nil, args = {})
        with_retry_on_too_many_requests do
          default_args = {
            method: :put,
            url: url,
            payload: payload,
            verify_ssl: false
          }

          RestClient::Request.execute(default_args.merge(with_canary(args)))
        rescue StandardError => e
          return_response_or_raise(e)
        end
      end

      def delete(url)
        with_retry_on_too_many_requests do
          RestClient::Request.execute(
            method: :delete,
            url: url,
            verify_ssl: false)
        rescue StandardError => e
          return_response_or_raise(e)
        end
      end

      def head(url)
        with_retry_on_too_many_requests do
          RestClient::Request.execute(
            method: :head,
            url: url,
            verify_ssl: false)
        rescue StandardError => e
          return_response_or_raise(e)
        end
      end

      def masked_url(url)
        url.sub(/private_token=[^&]*/, "private_token=[****]")
      end

      # Merges the gitlab_canary cookie into existing cookies for mixed environment testing.
      #
      # @param [Hash] args the existing args passed to method
      # @return [Hash] args or args with merged canary cookie if it exists
      def with_canary(args)
        args.deep_merge(cookies: QA::Runtime::Env.canary_cookie)
      end

      def with_retry_on_too_many_requests
        response = nil

        Support::Retrier.retry_until(log: false) do
          response = yield

          if response.code == HTTP_STATUS_TOO_MANY_REQUESTS
            wait_seconds = response.headers[:retry_after].to_i
            QA::Runtime::Logger.debug("Received 429 - Too many requests. Waiting for #{wait_seconds} seconds.")

            sleep wait_seconds
          end

          response.code != HTTP_STATUS_TOO_MANY_REQUESTS
        end

        response
      end

      def parse_body(response)
        JSON.parse(response.body, symbolize_names: true)
      end

      def return_response_or_raise(error)
        raise error, masked_url(error.to_s) unless error.respond_to?(:response) && error.response

        error.response
      end

      def auto_paginated_response(url, attempts: 0)
        pages = []
        with_paginated_response_body(url, attempts: attempts) { |response| pages << response }

        pages.flatten
      end

      def with_paginated_response_body(url, attempts: 0)
        not_ok_error = lambda do |resp|
          raise "Failed to GET #{masked_url(url)} - (#{resp.code}): `#{resp}`."
        end

        loop do
          response = if attempts > 0
                       Retrier.retry_on_exception(max_attempts: attempts, log: false) do
                         get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK }
                       end
                     else
                       get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK }
                     end

          page, pages, next_page = response.headers.values_at(:x_page, :x_total_pages, :x_next_page)
          api_endpoint = url.match(%r{v4/(\S+)\?})[1]

          QA::Runtime::Logger.debug("Fetching page (#{page}/#{pages}) for '#{api_endpoint}' ...") unless pages.to_i <= 1

          yield parse_body(response)

          break if next_page.empty?

          url = url.match?(/&page=\d+/) ? url.gsub(/&page=\d+/, "&page=#{next_page}") : "#{url}&page=#{next_page}"
        end
      end
    end
  end
end