File: excon_adapter.rb

package info (click to toggle)
ruby-webmock 3.25.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,172 kB
  • sloc: ruby: 12,829; makefile: 6
file content (167 lines) | stat: -rw-r--r-- 5,282 bytes parent folder | download
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
# frozen_string_literal: true

begin
  require 'excon'
rescue LoadError
  # excon not found
end

if defined?(Excon)
  WebMock::VersionChecker.new('Excon', Excon::VERSION, '0.27.5').check_version!

  module WebMock
    module HttpLibAdapters

      class ExconAdapter < HttpLibAdapter
        PARAMS_TO_DELETE = [:expects, :idempotent,
                            :instrumentor_name, :instrumentor,
                            :response_block,
                            :__construction_args, :stack,
                            :connection, :response]

        adapter_for :excon

        instance_exec do
          @original_excon_mock_default = nil
          @stub = nil
        end

        def self.enable!
          self.add_excon_stub
        end

        def self.disable!
          self.remove_excon_stub
        end

        def self.add_excon_stub
          if not @stub
            @original_excon_mock_default = ::Excon.defaults[:mock]
            ::Excon.defaults[:mock] = true
            @stub = ::Excon.stub({}) do |params|
              self.handle_request(params)
            end
          end
        end

        def self.remove_excon_stub
          ::Excon.defaults[:mock] = @original_excon_mock_default
          @original_excon_mock_default = nil
          Excon.stubs.delete(@stub)
          @stub = nil
        end

        def self.handle_request(params)
          mock_request  = self.build_request params.dup
          WebMock::RequestRegistry.instance.requested_signatures.put(mock_request)

          if mock_response = WebMock::StubRegistry.instance.response_for_request(mock_request)
            self.perform_callbacks(mock_request, mock_response, real_request: false)
            response = self.real_response(mock_response)
            response
          elsif WebMock.net_connect_allowed?(mock_request.uri)
            conn = new_excon_connection(params)
            real_response = conn.request(request_params_from(params.merge(mock: false)))

            ExconAdapter.perform_callbacks(mock_request, ExconAdapter.mock_response(real_response), real_request: true)

            real_response.data
          else
            raise WebMock::NetConnectNotAllowedError.new(mock_request)
          end
        end

        def self.new_excon_connection(params)
          # Ensure the connection is constructed with the exact same args
          # that the orginal connection was constructed with.
          args = params.fetch(:__construction_args)
          ::Excon::Connection.new(connection_params_from args.merge(mock: false))
        end

        def self.connection_params_from(hash)
          hash = hash.dup
          PARAMS_TO_DELETE.each { |key| hash.delete(key) }
          hash
        end

        def self.request_params_from(hash)
          hash = hash.dup
          if defined?(Excon::VALID_REQUEST_KEYS)
            hash.reject! {|key,_| !Excon::VALID_REQUEST_KEYS.include?(key) }
          end
          PARAMS_TO_DELETE.each { |key| hash.delete(key) }
          hash
        end

        def self.to_query(hash)
          string = "".dup
          for key, values in hash
            if values.nil?
              string << key.to_s << '&'
            else
              for value in [*values]
                string << key.to_s << '=' << CGI.escape(value.to_s) << '&'
              end
            end
          end
          string.chop! # remove trailing '&'
        end

        def self.build_request(params)
          params = params.dup
          params.delete(:user)
          params.delete(:password)
          method  = (params.delete(:method) || :get).to_s.downcase.to_sym
          params[:query] = to_query(params[:query]) if params[:query].is_a?(Hash)
          uri = Addressable::URI.new(params).to_s
          WebMock::RequestSignature.new method, uri, body: body_from(params), headers: params[:headers]
        end

        def self.body_from(params)
          body = params[:body]
          return body unless body.respond_to?(:read)

          contents = body.read
          body.rewind if body.respond_to?(:rewind)
          contents
        end

        def self.real_response(mock)
          raise Excon::Errors::Timeout if mock.should_timeout
          mock.raise_error_if_any
          {
            body: mock.body,
            status: mock.status[0].to_i,
            reason_phrase: mock.status[1],
            headers: mock.headers || {}
          }
        end

        def self.mock_response(real)
          mock = WebMock::Response.new
          mock.status  = [real.status, real.reason_phrase]
          mock.headers = real.headers
          mock.body    = real.body.dup
          mock
        end

        def self.perform_callbacks(request, response, options = {})
          return unless WebMock::CallbackRegistry.any_callbacks?
          WebMock::CallbackRegistry.invoke_callbacks(options.merge(lib: :excon), request, response)
        end
      end
    end
  end

  Excon::Connection.class_eval do
    def self.new(args = {})
      args.delete(:__construction_args)
      super(args).tap do |instance|
        instance.data[:__construction_args] = args
      end
    end
  end

  # Suppresses Excon connection argument validation warning
  Excon::VALID_CONNECTION_KEYS << :__construction_args
end