summaryrefslogtreecommitdiff
path: root/qa/qa/resource/api_fabricator.rb
blob: cbb68cccdf108c7c4a05aef37b4c7e9ad98f834d (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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# frozen_string_literal: true

require 'active_support/core_ext/object/deep_dup'
require 'capybara/dsl'

module QA
  module Resource
    module ApiFabricator
      include Capybara::DSL
      include Support::API
      include Errors

      attr_reader :api_fabrication_http_method
      attr_writer :api_client
      attr_accessor :api_user, :api_resource, :api_response

      def api_support?
        respond_to?(:api_get_path) &&
          (respond_to?(:api_post_path) && respond_to?(:api_post_body)) ||
          (respond_to?(:api_put_path) && respond_to?(:api_put_body))
      end

      # @return [String] the resource web url
      def fabricate_via_api!
        unless api_support?
          raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!"
        end

        resource_web_url(api_post)
      end

      def reload!
        api_get

        self
      end

      def remove_via_api!
        api_delete
      end

      def eager_load_api_client!
        return unless api_client.nil?

        api_client.tap do |client|
          # Eager-load the API client so that the personal token creation isn't
          # taken in account in the actual resource creation timing.
          client.user = user
          client.personal_access_token
        end
      end

      # Checks if a resource already exists
      #
      # @return [Boolean] true if the resource returns HTTP status code 200
      def exists?
        request = Runtime::API::Request.new(api_client, api_get_path)
        response = get(request.url)

        response.code == HTTP_STATUS_OK
      end

      # Parameters included in the query URL
      #
      # @return [Hash]
      def query_parameters
        @query_parameters ||= {}
      end

      private

      # rubocop:disable Gitlab/ModuleWithInstanceVariables
      def api_get
        process_api_response(parse_body(api_get_from(api_get_path))).tap do
          # Record method that was used to create certain resource
          #  :get - resource already existed in GitLab instance and was fetched via get request
          #  :post - resource was created from scratch using post request
          #  :put - resource was created from scratch using put request
          @api_fabrication_http_method ||= :get
        end
      end

      def api_get_from(get_path)
        path = "#{get_path}#{query_parameters_to_string}"
        request = Runtime::API::Request.new(api_client, path)
        response = get(request.url)

        if response.code == HTTP_STATUS_SERVER_ERROR
          raise(InternalServerError, <<~MSG.strip)
            Failed to GET #{request.mask_url} - (#{response.code}): `#{response}`.
            #{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}
          MSG
        elsif response.code != HTTP_STATUS_OK
          raise(ResourceNotFoundError, <<~MSG.strip)
            Resource at #{request.mask_url} could not be found (#{response.code}): `#{response}`.
            #{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}
          MSG
        end

        response
      end

      def api_post
        process_api_response(api_post_to(api_post_path, api_post_body)).tap do
          @api_fabrication_http_method ||= :post
        end
      end

      def api_post_to(post_path, post_body, args = {})
        if post_path == "/graphql"
          payload = post_body.is_a?(String) ? { query: post_body } : post_body
          graphql_response = post(Runtime::API::Request.new(api_client, post_path).url, payload, args)

          body = flatten_hash(parse_body(graphql_response))

          unless graphql_response.code == HTTP_STATUS_OK && (body[:errors].nil? || body[:errors].empty?)
            raise(ResourceFabricationFailedError, <<~MSG.strip)
              Fabrication of #{self.class.name} using the API failed (#{graphql_response.code}) with `#{graphql_response}`.
              #{QA::Support::Loglinking.failure_metadata(graphql_response.headers[:x_request_id])}
            MSG
          end

          body[:id] = body.fetch(:id).split('/').last if body.key?(:id)

          body.deep_transform_keys { |key| key.to_s.underscore.to_sym }
        else
          response = post(Runtime::API::Request.new(api_client, post_path).url, post_body, args)

          unless response.code == HTTP_STATUS_CREATED
            raise(ResourceFabricationFailedError, <<~MSG.strip)
              Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`.
              #{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}
            MSG
          end

          parse_body(response)
        end
      end

      def api_put
        process_api_response(api_put_to(api_put_path, api_put_body)).tap do
          @api_fabrication_http_method ||= :put
        end
      end

      def api_put_to(put_path, body)
        response = put(Runtime::API::Request.new(api_client, put_path).url, body)

        unless response.code == HTTP_STATUS_OK
          raise(ResourceFabricationFailedError, <<~MSG.strip)
            Updating #{self.class.name} using the API failed (#{response.code}) with `#{response}`.
            #{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}
          MSG
        end

        parse_body(response)
      end

      def api_delete
        request = Runtime::API::Request.new(api_client, api_delete_path)
        response = delete(request.url)

        unless [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_ACCEPTED].include? response.code
          raise(ResourceNotDeletedError, <<~MSG.strip)
            Resource at #{request.mask_url} could not be deleted (#{response.code}): `#{response}`.
            #{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}
          MSG
        end

        response
      end

      def resource_web_url(resource)
        resource.fetch(:web_url) do
          raise ResourceURLMissingError,
            "API resource for #{self.class.name} does not expose a `web_url` property: `#{resource}`."
        end
      end

      def api_client
        @api_client ||= Runtime::API::Client.new(
          :gitlab,
          is_new_session: !current_url.start_with?('http'),
          user: api_user
        )
      end
      # rubocop:enable Gitlab/ModuleWithInstanceVariables

      def process_api_response(parsed_response)
        self.api_response = parsed_response
        self.api_resource = transform_api_resource(parsed_response.deep_dup)
      end

      def transform_api_resource(api_resource)
        api_resource
      end

      # Get api request url
      #
      # @param [String] path
      # @return [String]
      def request_url(path, **opts)
        Runtime::API::Request.new(api_client, path, **opts).url
      end

      # Query parameters formatted as `?key1=value1&key2=value2...`
      #
      # @return [String]
      def query_parameters_to_string
        query_parameters.each_with_object([]) do |(k, v), arr|
          arr << "#{k}=#{v}"
        end.join('&').prepend('?').chomp('?') # prepend `?` unless the string is blank
      end

      def flatten_hash(param)
        param.each_pair.reduce({}) do |a, (k, v)|
          v.is_a?(Hash) ? a.merge(flatten_hash(v)) : a.merge(k.to_sym => v)
        end
      end
    end
  end
end