summaryrefslogtreecommitdiff
path: root/qa/qa/resource/api_fabricator.rb
blob: d82109c1d54c71e4b907347b92d9d5c21837bd9d (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
# 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_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

      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

      def api_put(body = api_put_body)
        response = put(
          Runtime::API::Request.new(api_client, api_put_path).url,
          body)

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

        process_api_response(parse_body(response))
      end

      def api_fabrication_http_method
        @api_fabrication_http_method ||= :post
      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

      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_get
        process_api_response(parse_body(api_get_from(api_get_path)))
      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, "Failed to GET #{request.mask_url} - (#{response.code}): `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
        elsif response.code != HTTP_STATUS_OK
          raise ResourceNotFoundError, "Resource at #{request.mask_url} could not be found (#{response.code}): `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
        end

        @api_fabrication_http_method = :get # rubocop:disable Gitlab/ModuleWithInstanceVariables

        response
      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 api_post
        process_api_response(api_post_to(api_post_path, api_post_body))
      end

      def api_post_to(post_path, post_body)
        if post_path == "/graphql"
          graphql_response = post(Runtime::API::Request.new(api_client, post_path).url, query: post_body)

          body = flatten_hash(parse_body(graphql_response))

          unless graphql_response.code == HTTP_STATUS_OK && (body[:errors].nil? || body[:errors].empty?)
            raise(ResourceFabricationFailedError, <<~MSG)
              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.transform_keys { |key| key.to_s.underscore.to_sym }
        else
          response = post(Runtime::API::Request.new(api_client, post_path).url, post_body)

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

          parse_body(response)
        end
      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

      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, "Resource at #{request.mask_url} could not be deleted (#{response.code}): `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
        end

        response
      end

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

      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
    end
  end
end