diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d8e6aab..5be2e6da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Read `release_notes.md` for commit level details. ## [Unreleased] ### Enhancements +- Add `x-idempotency-key` header support (https://github.com/appium/appium-base-driver/pull/400) + - Can disable the header with `enable_idempotency_header: false` in `appium_lib` capability. Defaults to `true`. ### Bug fixes diff --git a/lib/appium_lib_core/common/base/http_default.rb b/lib/appium_lib_core/common/base/http_default.rb index ad321263..04d8a918 100644 --- a/lib/appium_lib_core/common/base/http_default.rb +++ b/lib/appium_lib_core/common/base/http_default.rb @@ -12,12 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'securerandom' + require_relative '../../version' module Appium module Core class Base module Http + module RequestHeaders + KEYS = { + idempotency: 'X-Idempotency-Key' + }.freeze + end + class Default < Selenium::WebDriver::Remote::Http::Default DEFAULT_HEADERS = { 'Accept' => CONTENT_TYPE, @@ -26,6 +34,19 @@ class Default < Selenium::WebDriver::Remote::Http::Default "appium/ruby_lib_core/#{VERSION} (#{::Selenium::WebDriver::Remote::Http::Common::DEFAULT_HEADERS['User-Agent']})" }.freeze + attr_accessor :additional_headers + + def initialize(open_timeout: nil, read_timeout: nil, enable_idempotency_header: true) + @open_timeout = open_timeout + @read_timeout = read_timeout + + @additional_headers = if enable_idempotency_header + { RequestHeaders::KEYS[:idempotency] => SecureRandom.uuid } + else + {} + end + end + # Update server_url provided when ruby_lib _core created a default http client. # Set @http as nil to re-create http client for the server_url # @@ -65,19 +86,20 @@ def validate_url_param(scheme, host, port, path) def call(verb, url, command_hash) url = server_url.merge(url) unless url.is_a?(URI) headers = DEFAULT_HEADERS.dup + headers = headers.merge @additional_headers unless @additional_headers.empty? headers['Cache-Control'] = 'no-cache' if verb == :get if command_hash payload = JSON.generate(command_hash) headers['Content-Length'] = payload.bytesize.to_s if [:post, :put].include?(verb) - - ::Appium::Logger.info(" >>> #{url} | #{payload}") - ::Appium::Logger.debug(" > #{headers.inspect}") elsif verb == :post payload = '{}' headers['Content-Length'] = '2' end + ::Appium::Logger.info(" >>> #{url} | #{payload}") + ::Appium::Logger.info(" > #{headers.inspect}") + request verb, url, headers, payload end end diff --git a/lib/appium_lib_core/driver.rb b/lib/appium_lib_core/driver.rb index b656ccf6..8fb3d93e 100644 --- a/lib/appium_lib_core/driver.rb +++ b/lib/appium_lib_core/driver.rb @@ -34,11 +34,12 @@ module Ios class Options attr_reader :custom_url, :default_wait, :export_session, :export_session_path, :port, :wait_timeout, :wait_interval, :listener, - :direct_connect + :direct_connect, :enable_idempotency_header def initialize(appium_lib_opts) @custom_url = appium_lib_opts.fetch :server_url, nil @default_wait = appium_lib_opts.fetch :wait, Driver::DEFAULT_IMPLICIT_WAIT + @enable_idempotency_header = appium_lib_opts.fetch :enable_idempotency_header, true # bump current session id into a particular file @export_session = appium_lib_opts.fetch :export_session, false @@ -104,6 +105,12 @@ class Driver # @return [Appium::Core::Base::Http::Default] the http client attr_reader :http_client + # Return if adding 'x-idempotency-key' header is each request. + # The key is unique for each http client instance. Defaults to true + # https://github.com/appium/appium-base-driver/pull/400 + # @return [Bool] + attr_reader :enable_idempotency_header + # Device type to request from the appium server # @return [Symbol] :android and :ios, for example attr_reader :device @@ -114,7 +121,7 @@ class Driver attr_reader :automation_name # Custom URL for the selenium server. If set this attribute, ruby_lib_core try to handshake to the custom url.
- # Defaults to false. Then try to connect to http://127.0.0.1:#{port}/wd/hub. + # Defaults to false. Then try to connect to http://127.0.0.1:#{port}/wd/hub. # @return [String] attr_reader :custom_url @@ -376,7 +383,9 @@ def start_driver(server_url: nil, private def create_http_client(http_client: nil, open_timeout: nil, read_timeout: nil) - @http_client = http_client || Appium::Core::Base::Http::Default.new + @http_client = http_client || Appium::Core::Base::Http::Default.new( + enable_idempotency_header: @enable_idempotency_header + ) # open_timeout and read_timeout are explicit wait. @http_client.open_timeout = open_timeout if open_timeout @@ -570,6 +579,7 @@ def set_appium_lib_specific_values(appium_lib_opts) opts = Options.new appium_lib_opts @custom_url ||= opts.custom_url # Keep existence capability if it's already provided + @enable_idempotency_header = opts.enable_idempotency_header @default_wait = opts.default_wait diff --git a/test/unit/android/device/mjsonwp/commands_test.rb b/test/unit/android/device/mjsonwp/commands_test.rb index 59738971..1f9a9ac4 100644 --- a/test/unit/android/device/mjsonwp/commands_test.rb +++ b/test/unit/android/device/mjsonwp/commands_test.rb @@ -49,6 +49,7 @@ def test_shake def test_device_time stub_request(:get, "#{SESSION}/appium/device/system_time") + .with(body: {}.to_json) .to_return(headers: HEADER, status: 200, body: { value: 'device time' }.to_json) @driver.device_time @@ -56,6 +57,16 @@ def test_device_time assert_requested(:get, "#{SESSION}/appium/device/system_time", times: 1) end + def test_device_time_with_format + stub_request(:get, "#{SESSION}/appium/device/system_time") + .with(body: { format: 'YYYY-MM-DD' }.to_json) + .to_return(headers: HEADER, status: 200, body: { value: 'device time' }.to_json) + + @driver.device_time('YYYY-MM-DD') + + assert_requested(:get, "#{SESSION}/appium/device/system_time", times: 1) + end + def test_open_notifications stub_request(:post, "#{SESSION}/appium/device/open_notifications") .to_return(headers: HEADER, status: 200, body: { value: nil }.to_json) diff --git a/test/unit/android/device/w3c/commands_test.rb b/test/unit/android/device/w3c/commands_test.rb index 82c26c61..c0c2dcf3 100644 --- a/test/unit/android/device/w3c/commands_test.rb +++ b/test/unit/android/device/w3c/commands_test.rb @@ -48,6 +48,7 @@ def test_shake def test_device_time stub_request(:get, "#{SESSION}/appium/device/system_time") + .with(body: {}.to_json) .to_return(headers: HEADER, status: 200, body: { value: 'device time' }.to_json) @driver.device_time @@ -55,6 +56,16 @@ def test_device_time assert_requested(:get, "#{SESSION}/appium/device/system_time", times: 1) end + def test_device_time_with_format + stub_request(:get, "#{SESSION}/appium/device/system_time") + .with(body: { format: 'YYYY-MM-DD' }.to_json) + .to_return(headers: HEADER, status: 200, body: { value: 'device time' }.to_json) + + @driver.device_time('YYYY-MM-DD') + + assert_requested(:get, "#{SESSION}/appium/device/system_time", times: 1) + end + def test_open_notifications stub_request(:post, "#{SESSION}/appium/device/open_notifications") .to_return(headers: HEADER, status: 200, body: { value: nil }.to_json) diff --git a/test/unit/driver_test.rb b/test/unit/driver_test.rb index eee3a1ff..58ee212e 100644 --- a/test/unit/driver_test.rb +++ b/test/unit/driver_test.rb @@ -101,6 +101,16 @@ def test_default_timeout_for_http_client assert_equal '/wd/hub/', uri.path end + def test_http_client + client = ::Appium::Core::Base::Http::Default.new enable_idempotency_header: true + assert client.additional_headers.key?('X-Idempotency-Key') + end + + def test_http_client_no_idempotency_header + client = ::Appium::Core::Base::Http::Default.new enable_idempotency_header: false + assert !client.additional_headers.key?('X-Idempotency-Key') + end + def test_default_timeout_for_http_client_with_direct def android_mock_create_session_w3c_direct(core) response = {