ably-rest 1.0.6 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/submodules/ably-ruby/.editorconfig +14 -0
  4. data/lib/submodules/ably-ruby/.travis.yml +4 -4
  5. data/lib/submodules/ably-ruby/CHANGELOG.md +43 -2
  6. data/lib/submodules/ably-ruby/README.md +3 -2
  7. data/lib/submodules/ably-ruby/Rakefile +32 -0
  8. data/lib/submodules/ably-ruby/SPEC.md +1277 -835
  9. data/lib/submodules/ably-ruby/ably.gemspec +9 -4
  10. data/lib/submodules/ably-ruby/lib/ably/auth.rb +30 -4
  11. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +8 -2
  12. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +1 -1
  13. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +1 -1
  14. data/lib/submodules/ably-ruby/lib/ably/models/device_details.rb +87 -0
  15. data/lib/submodules/ably-ruby/lib/ably/models/device_push_details.rb +86 -0
  16. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +23 -2
  17. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
  18. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +32 -2
  19. data/lib/submodules/ably-ruby/lib/ably/models/push_channel_subscription.rb +89 -0
  20. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +1 -1
  21. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +1 -1
  22. data/lib/submodules/ably-ruby/lib/ably/modules/exception_codes.rb +128 -0
  23. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +15 -2
  24. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +1 -1
  25. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +1 -0
  26. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +1 -1
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +24 -102
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +2 -6
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/publisher.rb +74 -0
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/push_channel.rb +62 -0
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +87 -0
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  35. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +8 -5
  36. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +7 -7
  37. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +1 -1
  38. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +4 -4
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +3 -3
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/push.rb +40 -0
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/push/admin.rb +61 -0
  42. data/lib/submodules/ably-ruby/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  43. data/lib/submodules/ably-ruby/lib/ably/realtime/push/device_registrations.rb +105 -0
  44. data/lib/submodules/ably-ruby/lib/ably/rest.rb +1 -0
  45. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +33 -5
  46. data/lib/submodules/ably-ruby/lib/ably/rest/channel/push_channel.rb +62 -0
  47. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +138 -28
  48. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  49. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -0
  50. data/lib/submodules/ably-ruby/lib/ably/rest/push.rb +42 -0
  51. data/lib/submodules/ably-ruby/lib/ably/rest/push/admin.rb +54 -0
  52. data/lib/submodules/ably-ruby/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  53. data/lib/submodules/ably-ruby/lib/ably/rest/push/device_registrations.rb +103 -0
  54. data/lib/submodules/ably-ruby/lib/ably/version.rb +7 -2
  55. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +233 -8
  56. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +166 -51
  57. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +149 -0
  58. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +1 -1
  59. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +4 -4
  60. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +19 -17
  61. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +5 -5
  62. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_admin_spec.rb +696 -0
  63. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_spec.rb +27 -0
  64. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +41 -3
  65. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +2 -2
  66. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +129 -10
  67. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +175 -4
  68. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_admin_spec.rb +896 -0
  69. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_spec.rb +25 -0
  70. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +1 -1
  71. data/lib/submodules/ably-ruby/spec/run_parallel_tests +33 -0
  72. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +10 -3
  73. data/lib/submodules/ably-ruby/spec/unit/models/device_details_spec.rb +102 -0
  74. data/lib/submodules/ably-ruby/spec/unit/models/device_push_details_spec.rb +101 -0
  75. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +51 -3
  76. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +17 -2
  77. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +1 -1
  78. data/lib/submodules/ably-ruby/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  79. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +12 -0
  80. data/lib/submodules/ably-ruby/spec/unit/realtime/push_channel_spec.rb +36 -0
  81. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +8 -1
  82. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +30 -0
  83. data/lib/submodules/ably-ruby/spec/unit/rest/push_channel_spec.rb +36 -0
  84. metadata +29 -4
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Ably::Realtime::Push, :event_machine do
5
+ vary_by_protocol do
6
+ let(:default_options) { { key: api_key, environment: environment, protocol: protocol} }
7
+ let(:client_options) { default_options }
8
+ let(:client) do
9
+ Ably::Realtime::Client.new(client_options)
10
+ end
11
+ subject { client.push }
12
+
13
+ describe '#activate' do
14
+ it 'raises an unsupported exception' do
15
+ expect { subject.activate('foo') }.to raise_error(Ably::Exceptions::PushNotificationsNotSupported)
16
+ stop_reactor
17
+ end
18
+ end
19
+
20
+ describe '#deactivate' do
21
+ it 'raises an unsupported exception' do
22
+ expect { subject.deactivate('foo') }.to raise_error(Ably::Exceptions::PushNotificationsNotSupported)
23
+ stop_reactor
24
+ end
25
+ end
26
+ end
27
+ end
@@ -758,9 +758,10 @@ describe Ably::Auth do
758
758
  end
759
759
 
760
760
  it 'updates the persisted auth options that are then used for subsequent authorize requests' do
761
- expect(auth.options[:authUrl]).to be_nil
762
- auth.authorize({}, authUrl: 'http://foo.com')
763
- expect(auth.options[:authUrl]).to eql('http://foo.com')
761
+ auth_url = "https://echo.ably.io/?type=text&body=#{auth.request_token.token}"
762
+ expect(auth.options[:auth_url]).to be_nil
763
+ auth.authorize({}, auth_url: auth_url)
764
+ expect(auth.options[:auth_url]).to eql(auth_url)
764
765
  end
765
766
 
766
767
  context 'with a lambda for the :auth_callback option' do
@@ -1338,5 +1339,42 @@ describe Ably::Auth do
1338
1339
  expect(response).to be_a(Ably::Models::TokenDetails)
1339
1340
  end
1340
1341
  end
1342
+
1343
+ # RSC1, RSC1a, RSA3c, RSA3d
1344
+ context 'when using JWT' do
1345
+ let(:auth_url) { 'https://echo.ably.io/createJWT' }
1346
+ let(:token) { Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}").body }
1347
+ let(:client) { Ably::Rest::Client.new(token: token, environment: environment, protocol: protocol) }
1348
+
1349
+ it 'authenticates correctly using the JWT token generated by the echo server' do
1350
+ expect(client.stats).to_not be_nil()
1351
+ end
1352
+
1353
+ context 'when the JWT embeds an Ably token' do
1354
+ let(:token) { Faraday.post(auth_url, { keyName: key_name, keySecret: key_secret, jwtType: :embedded }).body }
1355
+
1356
+ it 'authenticates correctly using the embedded token' do
1357
+ expect(client.stats).to_not be_nil()
1358
+ end
1359
+
1360
+ context 'and the requested token is encrypted' do
1361
+ let(:token) { Faraday.post(auth_url, { keyName: key_name, keySecret: key_secret, jwtType: :embedded, encrypted: 1 }).body }
1362
+
1363
+ it 'authenticates correctly using the embedded token' do
1364
+ expect(client.stats).to_not be_nil()
1365
+ end
1366
+ end
1367
+ end
1368
+
1369
+ # RSA4f, RSA8c
1370
+ context 'when the token requested is returned with application/jwt content type' do
1371
+ let(:auth_rest_client) { Ably::Rest::Client.new(default_options.merge(key: api_key)) }
1372
+ let(:auth_params) { { keyName: key_name, keySecret: key_secret, returnType: 'jwt' } }
1373
+ let(:token) { auth_rest_client.auth.request_token({ }, { auth_url: auth_url, auth_params: auth_params }).token }
1374
+ it 'authenticates correctly and pulls stats' do
1375
+ expect(client.stats).to_not be_nil()
1376
+ end
1377
+ end
1378
+ end
1341
1379
  end
1342
1380
  end
@@ -7,7 +7,7 @@ describe Ably::Rest do
7
7
 
8
8
  let(:client_options) { {} }
9
9
  let(:client) do
10
- Ably::Rest::Client.new(client_options.merge(key: 'appid.keyuid:keysecret'))
10
+ Ably::Rest::Client.new(client_options.merge(key: 'appid.keyuid:keysecret', log_retries_as_info: true))
11
11
  end
12
12
 
13
13
  let(:now) { Time.now - 1000 }
@@ -67,7 +67,7 @@ describe Ably::Rest do
67
67
 
68
68
  vary_by_protocol do
69
69
  let(:client) do
70
- Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol)
70
+ Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol, log_retries_as_info: true)
71
71
  end
72
72
 
73
73
  describe 'failed requests' do
@@ -4,7 +4,7 @@ require 'webrick'
4
4
 
5
5
  describe Ably::Rest::Client do
6
6
  vary_by_protocol do
7
- let(:default_options) { { environment: environment, protocol: protocol } }
7
+ let(:default_options) { { environment: environment, protocol: protocol, log_retries_as_info: true } }
8
8
  let(:client_options) { default_options }
9
9
 
10
10
  let(:client) { Ably::Rest::Client.new(client_options) }
@@ -27,6 +27,19 @@ describe Ably::Rest::Client do
27
27
  end
28
28
  end
29
29
 
30
+ context 'with an invalid API key' do
31
+ let(:client) { Ably::Rest::Client.new(client_options.merge(key: 'app.key:secret', log_level: :fatal)) }
32
+
33
+ it 'logs an entry with a help href url matching the code #TI5' do
34
+ begin
35
+ client.channels.get('foo').publish('test')
36
+ raise 'Expected Ably::Exceptions::ResourceMissing'
37
+ rescue Ably::Exceptions::ResourceMissing => err
38
+ expect err.to_s.match(%r{https://help.ably.io/error/40400})
39
+ end
40
+ end
41
+ end
42
+
30
43
  context 'with an explicit string :token' do
31
44
  let(:client) { Ably::Rest::Client.new(client_options.merge(token: random_str)) }
32
45
 
@@ -710,6 +723,109 @@ describe Ably::Rest::Client do
710
723
  expect(@fallback_request_count).to eql(2)
711
724
  end
712
725
  end
726
+
727
+ context 'to fail the primary host, allow a fallback to succeed, then later trigger a fallback to the primary host (#RSC15f)' do
728
+ before do
729
+ @request_count = 0
730
+ @primary_host_request_count = 0
731
+ @web_server = WEBrick::HTTPServer.new(:Port => port, :SSLEnable => false, :AccessLog => [], Logger: WEBrick::Log.new("/dev/null"))
732
+ @web_server.mount_proc "/channels/#{channel_name}/publish" do |req, res|
733
+ @request_count += 1
734
+ if req.header["host"].first.include?(primary_host)
735
+ @primary_host_request_count += 1
736
+ # Fail all requests to the primary host so that a fallback is used
737
+ # Except request 6 which should suceed and clear the fallback host preference
738
+ if @request_count == 6
739
+ res.status = 200
740
+ res['Content-Type'] = 'application/json'
741
+ res.body = '{}'
742
+ else
743
+ res.status = 500
744
+ end
745
+ else
746
+ # Fail the second request (first failed fallback of first request)
747
+ # Fail the third request on the previously succeeded fallback host to trigger an attempt on the primary host
748
+ if [2, 5].include?(@request_count)
749
+ res.status = 500
750
+ else
751
+ res.status = 200
752
+ res['Content-Type'] = 'application/json'
753
+ res.body = '{}'
754
+ end
755
+ end
756
+ end
757
+
758
+ Thread.new do
759
+ @web_server.start
760
+ end
761
+ end
762
+
763
+ let(:client_options) do
764
+ default_options.merge(
765
+ rest_host: primary_host,
766
+ fallback_hosts: fallbacks,
767
+ token: 'fake.token',
768
+ port: port,
769
+ tls: false,
770
+ log_level: :error
771
+ ).merge(additional_client_options)
772
+ end
773
+
774
+ let (:additional_client_options) { {} }
775
+
776
+ it 'succeeds and remembers fallback host preferences across requests' do
777
+ # Send a request, expect primary endpoint to fail, one fallback to fail, second fallback to succeed
778
+ client.channel(channel_name).publish('event', 'data')
779
+ expect(@request_count).to eql(3)
780
+ expect(fallbacks).to include(client.using_preferred_fallback_host?)
781
+ successfull_fallback = client.using_preferred_fallback_host?
782
+ expect(@primary_host_request_count).to eql(1)
783
+
784
+ # Send another request, which should go straight to the fallback as it succeeded previously
785
+ client.channel(channel_name).publish('event', 'data')
786
+ expect(@request_count).to eql(4)
787
+ expect(successfull_fallback).to eql(client.using_preferred_fallback_host?)
788
+ expect(@primary_host_request_count).to eql(1)
789
+
790
+ # A subsequent request should fail to the fallback, go the primary host and succeed
791
+ client.channel(channel_name).publish('event', 'data')
792
+ expect(@request_count).to eql(6)
793
+ expect(client.using_preferred_fallback_host?).to be_falsey
794
+ expect(@primary_host_request_count).to eql(2)
795
+
796
+ # A subsequent request will fail on the primary endpoint, and we expect the fallback to be used again
797
+ client.channel(channel_name).publish('event', 'data')
798
+ expect(@request_count).to eql(8)
799
+ expect(fallbacks).to include(client.using_preferred_fallback_host?)
800
+ successfull_fallback = client.using_preferred_fallback_host?
801
+ expect(@primary_host_request_count).to eql(3)
802
+
803
+ # Send another request, which should go straight to the fallback as it succeeded previously
804
+ client.channel(channel_name).publish('event', 'data')
805
+ expect(@request_count).to eql(9)
806
+ expect(successfull_fallback).to eql(client.using_preferred_fallback_host?)
807
+ expect(@primary_host_request_count).to eql(3)
808
+ end
809
+
810
+ context 'with custom :fallback_retry_timeout' do
811
+ let (:additional_client_options) { { fallback_retry_timeout: 5 } }
812
+
813
+ it 'stops using the preferred fallback after this time' do
814
+ # Send a request, expect primary endpoint to fail, one fallback to fail, second fallback to succeed
815
+ client.channel(channel_name).publish('event', 'data')
816
+ expect(@request_count).to eql(3)
817
+ expect(fallbacks).to include(client.using_preferred_fallback_host?)
818
+ expect(@primary_host_request_count).to eql(1)
819
+
820
+ # Wait for the preferred fallback cache to expire
821
+ sleep 5
822
+
823
+ # Send another request, which should go straight to the primary host again as fallback host is expired
824
+ client.channel(channel_name).publish('event', 'data')
825
+ expect(@primary_host_request_count).to eql(2)
826
+ end
827
+ end
828
+ end
713
829
  end
714
830
  end
715
831
 
@@ -724,7 +840,8 @@ describe Ably::Rest::Client do
724
840
  environment: env,
725
841
  key: api_key,
726
842
  http_max_retry_duration: max_retry_duration,
727
- http_max_retry_count: max_retry_count
843
+ http_max_retry_count: max_retry_count,
844
+ log_level: :fatal,
728
845
  )
729
846
  end
730
847
 
@@ -751,7 +868,7 @@ describe Ably::Rest::Client do
751
868
  end
752
869
 
753
870
  let(:client_options) {
754
- production_options.merge(fallback_hosts: custom_hosts, log_level: :error)
871
+ production_options.merge(fallback_hosts: custom_hosts, log_level: :fatal)
755
872
  }
756
873
 
757
874
  it 'attempts the fallback hosts as this is not an authentication failure' do
@@ -764,7 +881,7 @@ describe Ably::Rest::Client do
764
881
 
765
882
  context 'with an empty array of fallback hosts provided (#RSC15b, #TO3k6)' do
766
883
  let(:client_options) {
767
- production_options.merge(fallback_hosts: [])
884
+ production_options.merge(fallback_hosts: [], log_level: :fatal)
768
885
  }
769
886
 
770
887
  it 'does not attempt the fallback hosts as this is an authentication failure' do
@@ -789,7 +906,7 @@ describe Ably::Rest::Client do
789
906
  end
790
907
 
791
908
  let(:client_options) {
792
- production_options.merge(fallback_hosts: custom_hosts, log_level: :error)
909
+ production_options.merge(fallback_hosts: custom_hosts, log_level: :fatal)
793
910
  }
794
911
 
795
912
  it 'attempts the default fallback hosts as this is an authentication failure' do
@@ -966,7 +1083,7 @@ describe Ably::Rest::Client do
966
1083
  it 'sends a protocol version and lib version header (#G4, #RSC7a, #RSC7b)' do
967
1084
  client.channels.get('foo').publish("event")
968
1085
  expect(publish_message_stub).to have_been_requested
969
- expect(Ably::PROTOCOL_VERSION).to eql('1.0')
1086
+ expect(Ably::PROTOCOL_VERSION).to eql('1.1')
970
1087
  end
971
1088
  end
972
1089
  end
@@ -1084,7 +1201,7 @@ describe Ably::Rest::Client do
1084
1201
  end
1085
1202
 
1086
1203
  context 'option add_request_ids: true and specified fallback hosts', :webmock do
1087
- let(:client_options) { { key: api_key, fallback_hosts_use_default: true, add_request_ids: true, log_level: :error } }
1204
+ let(:client_options) { { key: api_key, fallback_hosts_use_default: true, add_request_ids: true, log_level: :error, log_retries_as_info: true } }
1088
1205
  let(:requests) { [] }
1089
1206
 
1090
1207
  before do
@@ -1140,7 +1257,7 @@ describe Ably::Rest::Client do
1140
1257
 
1141
1258
  context 'failed request logging', :prevent_log_stubbing do
1142
1259
  let(:custom_logger) { TestLogger.new }
1143
- let(:client_options) { default_options.merge(key: api_key, logger: custom_logger) }
1260
+ let(:client_options) { default_options.merge(key: api_key, logger: custom_logger, log_retries_as_info: false) }
1144
1261
 
1145
1262
  it 'is absent when requests do not fail' do
1146
1263
  client.time
@@ -1153,7 +1270,8 @@ describe Ably::Rest::Client do
1153
1270
  rest_host: 'non.existent.domain.local',
1154
1271
  fallback_hosts: [[environment, Ably::Rest::Client::DOMAIN].join('-')],
1155
1272
  key: api_key,
1156
- logger: custom_logger)
1273
+ logger: custom_logger,
1274
+ log_retries_as_info: false)
1157
1275
  end
1158
1276
 
1159
1277
  it 'is present with success message when requests do not actually fail' do
@@ -1169,7 +1287,8 @@ describe Ably::Rest::Client do
1169
1287
  rest_host: 'non.existent.domain.local',
1170
1288
  fallback_hosts: ['non2.existent.domain.local'],
1171
1289
  key: api_key,
1172
- logger: custom_logger)
1290
+ logger: custom_logger,
1291
+ log_retries_as_info: false)
1173
1292
  end
1174
1293
 
1175
1294
  it 'is present when all requests fail' do
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  require 'spec_helper'
3
+ require 'base64'
3
4
  require 'securerandom'
4
5
 
5
6
  describe Ably::Rest::Channel, 'messages' do
@@ -91,11 +92,181 @@ describe Ably::Rest::Channel, 'messages' do
91
92
  end
92
93
  end
93
94
 
95
+ context 'idempotency (#RSL1k)' do
96
+ let(:id) { random_str }
97
+ let(:name) { 'event' }
98
+ let(:data) { random_str }
99
+
100
+ context 'when ID is not included (#RSL1k2)' do
101
+ context 'with Message object' do
102
+ let(:message) { Ably::Models::Message.new(data: data) }
103
+
104
+ it 'publishes the same message three times' do
105
+ 3.times { channel.publish [message] }
106
+ expect(channel.history.items.length).to eql(3)
107
+ end
108
+ end
109
+
110
+ context 'with #publish arguments only' do
111
+ it 'publishes the same message three times' do
112
+ 3.times { channel.publish 'event', data }
113
+ expect(channel.history.items.length).to eql(3)
114
+ end
115
+ end
116
+ end
117
+
118
+ context 'when ID is included (#RSL1k2, #RSL1k5)' do
119
+ context 'with Message object' do
120
+ let(:message) { Ably::Models::Message.new(id: id, data: data) }
121
+
122
+ specify 'three REST publishes result in only one message being published' do
123
+ pending 'idempotency rolled out to global cluster'
124
+
125
+ 3.times { channel.publish [message] }
126
+ expect(channel.history.items.length).to eql(1)
127
+ expect(channel.history.items[0].id).to eql(id)
128
+ end
129
+ end
130
+
131
+ context 'with #publish arguments only' do
132
+ it 'three REST publishes result in only one message being published' do
133
+ pending 'idempotency rolled out to global cluster'
134
+
135
+ 3.times { channel.publish 'event', data, id: id }
136
+ expect(channel.history.items.length).to eql(1)
137
+ end
138
+ end
139
+
140
+ specify 'the ID provided is used for the published messages' do
141
+ channel.publish 'event', data, id: id
142
+ expect(channel.history.items[0].id).to eql(id)
143
+ end
144
+
145
+ specify 'for multiple messages in one publish operation (#RSL1k3)' do
146
+ pending 'idempotency rolled out to global cluster'
147
+
148
+ message_arr = 3.times.map { Ably::Models::Message.new(id: id, data: data) }
149
+ expect { channel.publish message_arr }.to raise_error do |error|
150
+ expect(error.code).to eql(40031) # Invalid publish request (invalid client-specified id), see https://github.com/ably/ably-common/pull/30
151
+ end
152
+ end
153
+
154
+ specify 'for multiple messages in one publish operation with IDs following the required format described in RSL1k1 (#RSL1k3)' do
155
+ pending 'idempotency rolled out to global cluster'
156
+
157
+ message_arr = 3.times.map { |index| Ably::Models::Message.new(id: "#{id}:#{index}", data: data) }
158
+ channel.publish message_arr
159
+ expect(channel.history.items[0].id).to eql("{id}:0")
160
+ expect(channel.history.items[2].id).to eql("{id}:2")
161
+ expect(channel.history.items.length).to eql(3)
162
+ end
163
+ end
164
+
165
+ specify 'idempotent publishing is disabled by default with 1.1 (#TO3n)' do
166
+ client = Ably::Rest::Client.new(key: api_key, protocol: protocol)
167
+ expect(client.idempotent_rest_publishing).to be_falsey
168
+ end
169
+
170
+ specify 'idempotent publishing is enabled by default with 1.2 (#TO3n)' do
171
+ stub_const 'Ably::VERSION', '1.2.0'
172
+ client = Ably::Rest::Client.new(key: api_key, protocol: protocol)
173
+ expect(client.idempotent_rest_publishing).to be_truthy
174
+ end
175
+
176
+ context 'when idempotent publishing is enabled in the client library ClientOptions (#TO3n)' do
177
+ let(:client_options) { default_client_options.merge(idempotent_rest_publishing: true, log_level: :error) }
178
+
179
+ context 'when there is a network failure triggering an automatic retry (#RSL1k4)' do
180
+ def mock_for_two_publish_failures
181
+ @failed_http_posts = 0
182
+ allow(client).to receive(:can_fallback_to_alternate_ably_host?).and_return(true)
183
+ allow_any_instance_of(Faraday::Connection).to receive(:post) do |*args|
184
+ @failed_http_posts += 1
185
+ if @failed_http_posts == 2
186
+ # Ensure the 3rd requests operates as normal
187
+ allow_any_instance_of(Faraday::Connection).to receive(:post).and_call_original
188
+ end
189
+ raise Faraday::ClientError.new('Fake client error')
190
+ end
191
+ end
192
+
193
+ context 'with Message object' do
194
+ let(:message) { Ably::Models::Message.new(data: data) }
195
+ before { mock_for_two_publish_failures }
196
+
197
+ specify 'two REST publish retries result in only one message being published' do
198
+ pending 'idempotency rolled out to global cluster'
199
+
200
+ channel.publish [message]
201
+ expect(channel.history.items.length).to eql(1)
202
+ expect(@failed_http_posts).to eql(2)
203
+ end
204
+ end
205
+
206
+ context 'with #publish arguments only' do
207
+ before { mock_for_two_publish_failures }
208
+
209
+ specify 'two REST publish retries result in only one message being published' do
210
+ pending 'idempotency rolled out to global cluster'
211
+
212
+ channel.publish 'event', data
213
+ expect(channel.history.items.length).to eql(1)
214
+ expect(@failed_http_posts).to eql(2)
215
+ end
216
+ end
217
+
218
+ context 'with explicitly provided message ID' do
219
+ let(:id) { random_str }
220
+
221
+ before { mock_for_two_publish_failures }
222
+
223
+ specify 'two REST publish retries result in only one message being published' do
224
+ pending 'idempotency rolled out to global cluster'
225
+
226
+ channel.publish 'event', data, id: id
227
+ expect(channel.history.items.length).to eql(1)
228
+ expect(channel.history.items[0].id).to eql(id)
229
+ expect(@failed_http_posts).to eql(2)
230
+ end
231
+ end
232
+
233
+ specify 'for multiple messages in one publish operation' do
234
+ pending 'idempotency rolled out to global cluster'
235
+
236
+ message_arr = 3.times.map { Ably::Models::Message.new(data: data) }
237
+ 3.times { channel.publish message_arr }
238
+ expect(channel.history.items.length).to eql(message_arr.length)
239
+ end
240
+ end
241
+
242
+ specify 'the ID is populated with a random ID and serial 0 from this lib (#RSL1k1)' do
243
+ channel.publish 'event'
244
+ expect(channel.history.items[0].id).to match(/^[A-Za-z0-9\+\/]+:0$/)
245
+ base_64_id = channel.history.items[0].id.split(':')[0]
246
+ expect(Base64.decode64(base_64_id).length).to eql(9)
247
+ end
248
+
249
+ context 'when publishing a batch of messages' do
250
+ specify 'the ID is populated with a single random ID and sequence of serials from this lib (#RSL1k1)' do
251
+ pending 'idempotency rolled out to global cluster'
252
+
253
+ message = { name: 'event' }
254
+ channel.publish [message, message, message]
255
+ expect(channel.history.items[0].length).to eql(3)
256
+ expect(channel.history.items[0].id).to match(/^[A-Za-z0-9\+\/]+:0$/)
257
+ expect(channel.history.items[2].id).to match(/^[A-Za-z0-9\+\/]+:2$/)
258
+ base_64_id = channel.history.items[0].id.split(':')[0]
259
+ expect(Base64.decode64(base_64_id).length).to eql(9)
260
+ end
261
+ end
262
+ end
263
+ end
264
+
94
265
  context 'with unsupported data payload content type' do
95
266
  context 'Integer' do
96
267
  let(:data) { 1 }
97
268
 
98
- it 'is raises an UnsupportedDataType 40011 exception' do
269
+ it 'is raises an UnsupportedDataType 40013 exception' do
99
270
  expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
100
271
  end
101
272
  end
@@ -103,7 +274,7 @@ describe Ably::Rest::Channel, 'messages' do
103
274
  context 'Float' do
104
275
  let(:data) { 1.1 }
105
276
 
106
- it 'is raises an UnsupportedDataType 40011 exception' do
277
+ it 'is raises an UnsupportedDataType 40013 exception' do
107
278
  expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
108
279
  end
109
280
  end
@@ -111,7 +282,7 @@ describe Ably::Rest::Channel, 'messages' do
111
282
  context 'Boolean' do
112
283
  let(:data) { true }
113
284
 
114
- it 'is raises an UnsupportedDataType 40011 exception' do
285
+ it 'is raises an UnsupportedDataType 40013 exception' do
115
286
  expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
116
287
  end
117
288
  end
@@ -119,7 +290,7 @@ describe Ably::Rest::Channel, 'messages' do
119
290
  context 'False' do
120
291
  let(:data) { false }
121
292
 
122
- it 'is raises an UnsupportedDataType 40011 exception' do
293
+ it 'is raises an UnsupportedDataType 40013 exception' do
123
294
  expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
124
295
  end
125
296
  end