Skip to content

Commit 350ba7b

Browse files
authored
feat: Add x idempotency header (#259)
* feat: add x idempotency header for each request * fix rubocop * move the uuid in init once * tweak * tweak format * add tests * update changelog
1 parent 94f16d8 commit 350ba7b

File tree

6 files changed

+72
-6
lines changed

6 files changed

+72
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Read `release_notes.md` for commit level details.
55
## [Unreleased]
66

77
### Enhancements
8+
- Add `x-idempotency-key` header support (https://github.com/appium/appium-base-driver/pull/400)
9+
- Can disable the header with `enable_idempotency_header: false` in `appium_lib` capability. Defaults to `true`.
810

911
### Bug fixes
1012

lib/appium_lib_core/common/base/http_default.rb

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
require 'securerandom'
16+
1517
require_relative '../../version'
1618

1719
module Appium
1820
module Core
1921
class Base
2022
module Http
23+
module RequestHeaders
24+
KEYS = {
25+
idempotency: 'X-Idempotency-Key'
26+
}.freeze
27+
end
28+
2129
class Default < Selenium::WebDriver::Remote::Http::Default
2230
DEFAULT_HEADERS = {
2331
'Accept' => CONTENT_TYPE,
@@ -26,6 +34,19 @@ class Default < Selenium::WebDriver::Remote::Http::Default
2634
"appium/ruby_lib_core/#{VERSION} (#{::Selenium::WebDriver::Remote::Http::Common::DEFAULT_HEADERS['User-Agent']})"
2735
}.freeze
2836

37+
attr_accessor :additional_headers
38+
39+
def initialize(open_timeout: nil, read_timeout: nil, enable_idempotency_header: true)
40+
@open_timeout = open_timeout
41+
@read_timeout = read_timeout
42+
43+
@additional_headers = if enable_idempotency_header
44+
{ RequestHeaders::KEYS[:idempotency] => SecureRandom.uuid }
45+
else
46+
{}
47+
end
48+
end
49+
2950
# Update <code>server_url</code> provided when ruby_lib _core created a default http client.
3051
# Set <code>@http</code> as nil to re-create http client for the <code>server_url</code>
3152
#
@@ -65,19 +86,20 @@ def validate_url_param(scheme, host, port, path)
6586
def call(verb, url, command_hash)
6687
url = server_url.merge(url) unless url.is_a?(URI)
6788
headers = DEFAULT_HEADERS.dup
89+
headers = headers.merge @additional_headers unless @additional_headers.empty?
6890
headers['Cache-Control'] = 'no-cache' if verb == :get
6991

7092
if command_hash
7193
payload = JSON.generate(command_hash)
7294
headers['Content-Length'] = payload.bytesize.to_s if [:post, :put].include?(verb)
73-
74-
::Appium::Logger.info(" >>> #{url} | #{payload}")
75-
::Appium::Logger.debug(" > #{headers.inspect}")
7695
elsif verb == :post
7796
payload = '{}'
7897
headers['Content-Length'] = '2'
7998
end
8099

100+
::Appium::Logger.info(" >>> #{url} | #{payload}")
101+
::Appium::Logger.info(" > #{headers.inspect}")
102+
81103
request verb, url, headers, payload
82104
end
83105
end

lib/appium_lib_core/driver.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ module Ios
3434
class Options
3535
attr_reader :custom_url, :default_wait, :export_session, :export_session_path,
3636
:port, :wait_timeout, :wait_interval, :listener,
37-
:direct_connect
37+
:direct_connect, :enable_idempotency_header
3838

3939
def initialize(appium_lib_opts)
4040
@custom_url = appium_lib_opts.fetch :server_url, nil
4141
@default_wait = appium_lib_opts.fetch :wait, Driver::DEFAULT_IMPLICIT_WAIT
42+
@enable_idempotency_header = appium_lib_opts.fetch :enable_idempotency_header, true
4243

4344
# bump current session id into a particular file
4445
@export_session = appium_lib_opts.fetch :export_session, false
@@ -104,6 +105,12 @@ class Driver
104105
# @return [Appium::Core::Base::Http::Default] the http client
105106
attr_reader :http_client
106107

108+
# Return if adding 'x-idempotency-key' header is each request.
109+
# The key is unique for each http client instance. <code>Defaults to true</code>
110+
# https://github.com/appium/appium-base-driver/pull/400
111+
# @return [Bool]
112+
attr_reader :enable_idempotency_header
113+
107114
# Device type to request from the appium server
108115
# @return [Symbol] :android and :ios, for example
109116
attr_reader :device
@@ -114,7 +121,7 @@ class Driver
114121
attr_reader :automation_name
115122

116123
# Custom URL for the selenium server. If set this attribute, ruby_lib_core try to handshake to the custom url.<br>
117-
# Defaults to false. Then try to connect to <code>http://127.0.0.1:#{port}/wd/hub<code>.
124+
# Defaults to false. Then try to connect to <code>http://127.0.0.1:#{port}/wd/hub</code>.
118125
# @return [String]
119126
attr_reader :custom_url
120127

@@ -376,7 +383,9 @@ def start_driver(server_url: nil,
376383
private
377384

378385
def create_http_client(http_client: nil, open_timeout: nil, read_timeout: nil)
379-
@http_client = http_client || Appium::Core::Base::Http::Default.new
386+
@http_client = http_client || Appium::Core::Base::Http::Default.new(
387+
enable_idempotency_header: @enable_idempotency_header
388+
)
380389

381390
# open_timeout and read_timeout are explicit wait.
382391
@http_client.open_timeout = open_timeout if open_timeout
@@ -570,6 +579,7 @@ def set_appium_lib_specific_values(appium_lib_opts)
570579
opts = Options.new appium_lib_opts
571580

572581
@custom_url ||= opts.custom_url # Keep existence capability if it's already provided
582+
@enable_idempotency_header = opts.enable_idempotency_header
573583

574584
@default_wait = opts.default_wait
575585

test/unit/android/device/mjsonwp/commands_test.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,24 @@ def test_shake
4949

5050
def test_device_time
5151
stub_request(:get, "#{SESSION}/appium/device/system_time")
52+
.with(body: {}.to_json)
5253
.to_return(headers: HEADER, status: 200, body: { value: 'device time' }.to_json)
5354

5455
@driver.device_time
5556

5657
assert_requested(:get, "#{SESSION}/appium/device/system_time", times: 1)
5758
end
5859

60+
def test_device_time_with_format
61+
stub_request(:get, "#{SESSION}/appium/device/system_time")
62+
.with(body: { format: 'YYYY-MM-DD' }.to_json)
63+
.to_return(headers: HEADER, status: 200, body: { value: 'device time' }.to_json)
64+
65+
@driver.device_time('YYYY-MM-DD')
66+
67+
assert_requested(:get, "#{SESSION}/appium/device/system_time", times: 1)
68+
end
69+
5970
def test_open_notifications
6071
stub_request(:post, "#{SESSION}/appium/device/open_notifications")
6172
.to_return(headers: HEADER, status: 200, body: { value: nil }.to_json)

test/unit/android/device/w3c/commands_test.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,24 @@ def test_shake
4848

4949
def test_device_time
5050
stub_request(:get, "#{SESSION}/appium/device/system_time")
51+
.with(body: {}.to_json)
5152
.to_return(headers: HEADER, status: 200, body: { value: 'device time' }.to_json)
5253

5354
@driver.device_time
5455

5556
assert_requested(:get, "#{SESSION}/appium/device/system_time", times: 1)
5657
end
5758

59+
def test_device_time_with_format
60+
stub_request(:get, "#{SESSION}/appium/device/system_time")
61+
.with(body: { format: 'YYYY-MM-DD' }.to_json)
62+
.to_return(headers: HEADER, status: 200, body: { value: 'device time' }.to_json)
63+
64+
@driver.device_time('YYYY-MM-DD')
65+
66+
assert_requested(:get, "#{SESSION}/appium/device/system_time", times: 1)
67+
end
68+
5869
def test_open_notifications
5970
stub_request(:post, "#{SESSION}/appium/device/open_notifications")
6071
.to_return(headers: HEADER, status: 200, body: { value: nil }.to_json)

test/unit/driver_test.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ def test_default_timeout_for_http_client
101101
assert_equal '/wd/hub/', uri.path
102102
end
103103

104+
def test_http_client
105+
client = ::Appium::Core::Base::Http::Default.new enable_idempotency_header: true
106+
assert client.additional_headers.key?('X-Idempotency-Key')
107+
end
108+
109+
def test_http_client_no_idempotency_header
110+
client = ::Appium::Core::Base::Http::Default.new enable_idempotency_header: false
111+
assert !client.additional_headers.key?('X-Idempotency-Key')
112+
end
113+
104114
def test_default_timeout_for_http_client_with_direct
105115
def android_mock_create_session_w3c_direct(core)
106116
response = {

0 commit comments

Comments
 (0)