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,121 @@
1
+ module Ably::Rest
2
+ class Push
3
+ # Manage push notification channel subscriptions for devices or client identifiers
4
+ class ChannelSubscriptions
5
+ include Ably::Modules::Conversions
6
+
7
+ # @api private
8
+ attr_reader :client
9
+
10
+ # @api private
11
+ attr_reader :admin
12
+
13
+ def initialize(admin)
14
+ @admin = admin
15
+ @client = admin.client
16
+ end
17
+
18
+ # List channel subscriptions filtered by optional params
19
+ #
20
+ # @param [Hash] params the filter options for the list channel subscription request. At least one of channel, client_id or device_id is required
21
+ # @option params [String] :channel filter by realtime pub/sub channel name
22
+ # @option params [String] :client_id filter by devices registered to a client identifier. If provided with device_id param, a concat operation is used so that any device with this client_id or provided device_id is returned.
23
+ # @option params [String] :device_id filter by unique device ID. If provided with client_id param, a concat operation is used so that any device with this device_id or provided client_id is returned.
24
+ # @option params [Integer] :limit maximum number of subscriptions to retrieve up to 1,000, defaults to 100
25
+ #
26
+ # @return [Ably::Models::PaginatedResult<Ably::Models::PushChannelSubscription>] Paginated list of matching {Ably::Models::PushChannelSubscription}
27
+ #
28
+ def list(params)
29
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
30
+
31
+ if (IdiomaticRubyWrapper(params).keys & [:channel, :client_id, :device_id]).length == 0
32
+ raise ArgumentError, "at least one channel, client_id or device_id filter param must be provided"
33
+ end
34
+
35
+ params = params.clone
36
+
37
+ paginated_options = {
38
+ coerce_into: 'Ably::Models::PushChannelSubscription',
39
+ async_blocking_operations: params.delete(:async_blocking_operations),
40
+ }
41
+
42
+ response = client.get('/push/channelSubscriptions', IdiomaticRubyWrapper(params).as_json)
43
+
44
+ Ably::Models::PaginatedResult.new(response, '', client, paginated_options)
45
+ end
46
+
47
+ # List channels with at least one subscribed device
48
+ #
49
+ # @param [Hash] params the options for the list channels request
50
+ # @option params [Integer] :limit maximum number of channels to retrieve up to 1,000, defaults to 100
51
+ #
52
+ # @return [Ably::Models::PaginatedResult<String>] Paginated list of matching {Ably::Models::PushChannelSubscription}
53
+ #
54
+ def list_channels(params = {})
55
+ params = {} if params.nil?
56
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
57
+
58
+ params = params.clone
59
+
60
+ paginated_options = {
61
+ coerce_into: 'String',
62
+ async_blocking_operations: params.delete(:async_blocking_operations),
63
+ }
64
+
65
+ response = client.get('/push/channels', IdiomaticRubyWrapper(params).as_json)
66
+
67
+ Ably::Models::PaginatedResult.new(response, '', client, paginated_options)
68
+ end
69
+
70
+ # Save push channel subscription for a device or client ID
71
+ #
72
+ # @param [Ably::Models::PushChannelSubscription,Hash] push_channel_subscription the push channel subscription details to save
73
+ #
74
+ # @return [void]
75
+ #
76
+ def save(push_channel_subscription)
77
+ push_channel_subscription_object = PushChannelSubscription(push_channel_subscription)
78
+ raise ArgumentError, "Channel is required yet is empty" if push_channel_subscription_object.channel.to_s.empty?
79
+
80
+ client.post("/push/channelSubscriptions", push_channel_subscription_object.as_json)
81
+ end
82
+
83
+ # Remove a push channel subscription
84
+ #
85
+ # @param [Ably::Models::PushChannelSubscription,Hash] push_channel_subscription the push channel subscription details to remove
86
+ #
87
+ # @return [void]
88
+ #
89
+ def remove(push_channel_subscription)
90
+ push_channel_subscription_object = PushChannelSubscription(push_channel_subscription)
91
+ raise ArgumentError, "Channel is required yet is empty" if push_channel_subscription_object.channel.to_s.empty?
92
+ if push_channel_subscription_object.client_id.to_s.empty? && push_channel_subscription_object.device_id.to_s.empty?
93
+ raise ArgumentError, "Either client_id or device_id must be present"
94
+ end
95
+
96
+ client.delete("/push/channelSubscriptions", push_channel_subscription_object.as_json)
97
+ end
98
+
99
+ # Remove all matching push channel subscriptions
100
+ #
101
+ # @param [Hash] params the filter options for the list channel subscription request. At least one of channel, client_id or device_id is required
102
+ # @option params [String] :channel filter by realtime pub/sub channel name
103
+ # @option params [String] :client_id filter by devices registered to a client identifier. If provided with device_id param, a concat operation is used so that any device with this client_id or provided device_id is returned.
104
+ # @option params [String] :device_id filter by unique device ID. If provided with client_id param, a concat operation is used so that any device with this device_id or provided client_id is returned.
105
+ #
106
+ # @return [void]
107
+ #
108
+ def remove_where(params)
109
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
110
+
111
+ if (IdiomaticRubyWrapper(params).keys & [:channel, :client_id, :device_id]).length == 0
112
+ raise ArgumentError, "at least one channel, client_id or device_id filter param must be provided"
113
+ end
114
+
115
+ params = params.clone
116
+
117
+ client.delete("/push/channelSubscriptions", IdiomaticRubyWrapper(params).as_json)
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,103 @@
1
+ module Ably::Rest
2
+ class Push
3
+ # Manage device registrations for push notifications
4
+ class DeviceRegistrations
5
+ include Ably::Modules::Conversions
6
+
7
+ # @api private
8
+ attr_reader :client
9
+
10
+ # @api private
11
+ attr_reader :admin
12
+
13
+ def initialize(admin)
14
+ @admin = admin
15
+ @client = admin.client
16
+ end
17
+
18
+ # Get registered device by device ID
19
+ #
20
+ # @param [String, Ably::Models::DeviceDetails] device_id the device to retrieve
21
+ #
22
+ # @return [Ably::Models::DeviceDetails] Returns {Ably::Models::DeviceDetails} if a match is found else a {Ably::Exceptions::ResourceMissing} is raised
23
+ #
24
+ def get(device_id)
25
+ device_id = device_id.id if device_id.kind_of?(Ably::Models::DeviceDetails)
26
+ raise ArgumentError, "device_id must be a string or DeviceDetails object" unless device_id.kind_of?(String)
27
+
28
+ DeviceDetails(client.get("/push/deviceRegistrations/#{device_id}").body)
29
+ end
30
+
31
+ # List registered devices filtered by optional params
32
+ #
33
+ # @param [Hash] params the filter options for the list registered device request
34
+ # @option params [String] :client_id filter by devices registered to a client identifier. Cannot be used with +device_id+ param
35
+ # @option params [String] :device_id filter by unique device ID. Cannot be used with +client_id+ param
36
+ # @option params [Integer] :limit maximum number of devices to retrieve up to 1,000, defaults to 100
37
+ #
38
+ # @return [Ably::Models::PaginatedResult<Ably::Models::DeviceDetails>] Paginated list of matching {Ably::Models::DeviceDetails}
39
+ #
40
+ def list(params = {})
41
+ params = {} if params.nil?
42
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
43
+ raise ArgumentError, "device_id filter cannot be specified alongside a client_id filter. Use one or the other" if params[:client_id] && params[:device_id]
44
+
45
+ params = params.clone
46
+
47
+ paginated_options = {
48
+ coerce_into: 'Ably::Models::DeviceDetails',
49
+ async_blocking_operations: params.delete(:async_blocking_operations),
50
+ }
51
+
52
+ response = client.get('/push/deviceRegistrations', IdiomaticRubyWrapper(params).as_json)
53
+
54
+ Ably::Models::PaginatedResult.new(response, '', client, paginated_options)
55
+ end
56
+
57
+ # Save and register device
58
+ #
59
+ # @param [Ably::Models::DeviceDetails, Hash] device the device details to save
60
+ #
61
+ # @return [void]
62
+ #
63
+ def save(device)
64
+ device_details = DeviceDetails(device)
65
+ raise ArgumentError, "Device ID is required yet is empty" if device_details.id.nil? || device_details == ''
66
+
67
+ client.put("/push/deviceRegistrations/#{device_details.id}", device_details.as_json)
68
+ end
69
+
70
+ # Remove device
71
+ #
72
+ # @param [String, Ably::Models::DeviceDetails] device_id the device to remove
73
+ #
74
+ # @return [void]
75
+ #
76
+ def remove(device_id)
77
+ device_id = device_id.id if device_id.kind_of?(Ably::Models::DeviceDetails)
78
+ raise ArgumentError, "device_id must be a string or DeviceDetails object" unless device_id.kind_of?(String)
79
+
80
+ client.delete("/push/deviceRegistrations/#{device_id}", {})
81
+ end
82
+
83
+ # Remove device matching where params
84
+ #
85
+ # @param [Hash] params the filter params for the remove request
86
+ # @option params [String] :client_id remove devices registered to a client identifier. Cannot be used with +device_id+ param
87
+ # @option params [String] :device_id remove device with this unique device ID. Cannot be used with +client_id+ param
88
+ #
89
+ # @return [void]
90
+ #
91
+ def remove_where(params = {})
92
+ filter = if params.kind_of?(Ably::Models::DeviceDetails)
93
+ { 'deviceId' => params.id }
94
+ else
95
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
96
+ raise ArgumentError, "device_id filter cannot be specified alongside a client_id filter. Use one or the other" if params[:client_id] && params[:device_id]
97
+ IdiomaticRubyWrapper(params).as_json
98
+ end
99
+ client.delete("/push/deviceRegistrations", filter)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -1,6 +1,6 @@
1
1
  module Ably
2
- VERSION = '1.0.6'
3
- PROTOCOL_VERSION = '1.0'
2
+ VERSION = '1.1.0'
3
+ PROTOCOL_VERSION = '1.1'
4
4
 
5
5
  # Allow a variant to be configured for all instances of this client library
6
6
  # such as ruby-rest-[VERSION]
@@ -13,4 +13,9 @@ module Ably
13
13
  def self.lib_variant
14
14
  @lib_variant
15
15
  end
16
+
17
+ # @api private
18
+ def self.major_minor_version_numeric
19
+ VERSION.gsub(/\.\d+$/, '').to_f
20
+ end
16
21
  end
@@ -607,16 +607,17 @@ describe Ably::Realtime::Auth, :event_machine do
607
607
 
608
608
  context 'when auth fails' do
609
609
  let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :none) }
610
+ let!(:token_string) { client.rest_client.auth.request_token.token }
610
611
 
611
612
  it 'transitions the connection state to the FAILED state (#RSA15c, #RTC8a2, #RTC8a3)' do
612
613
  connection_failed = false
613
614
 
614
615
  client.connection.once(:connected) do
615
- client.auth.authorize(nil, auth_callback: lambda { |token_params| 'invalid.token:will.cause.failure' }).tap do |deferrable|
616
+ client.auth.authorize(nil, auth_callback: lambda { |token_params| "#{app_id}.invalid.token.will.cause.failure" }).tap do |deferrable|
616
617
  deferrable.errback do |error|
617
618
  EventMachine.add_timer(0.2) do
618
619
  expect(connection_failed).to eql(true)
619
- expect(error.message).to match(/Invalid accessToken/i)
620
+ expect(error.message).to match(/invalid.*accessToken/i)
620
621
  expect(error.code).to eql(40005)
621
622
  stop_reactor
622
623
  end
@@ -626,7 +627,7 @@ describe Ably::Realtime::Auth, :event_machine do
626
627
  end
627
628
 
628
629
  client.connection.once(:failed) do
629
- expect(client.connection.error_reason.message).to match(/Invalid accessToken/i)
630
+ expect(client.connection.error_reason.message).to match(/invalid.*accessToken/i)
630
631
  expect(client.connection.error_reason.code).to eql(40005)
631
632
  connection_failed = true
632
633
  end
@@ -749,11 +750,8 @@ describe Ably::Realtime::Auth, :event_machine do
749
750
  end
750
751
 
751
752
  context 'when received' do
752
- # Ably in all environments other than locla will send AUTH 30 seconds before expiry
753
- # We set the TTL to 33s and wait (3s window)
754
- # In local env, that window is 5 seconds instead of 30 seconds
755
- let(:local_offset) { ENV['ABLY_ENV'] == 'local' ? 25 : 0 }
756
- let(:client_options) { default_options.merge(use_token_auth: :true, default_token_params: { ttl: 33 - local_offset }) }
753
+ # Ably will send AUTH 30 seconds before expiry
754
+ let(:client_options) { default_options.merge(use_token_auth: :true, default_token_params: { ttl: 33 }) }
757
755
 
758
756
  it 'should immediately start a new authentication process (#RTN22)' do
759
757
  client.connection.once(:connected) do
@@ -1031,5 +1029,232 @@ describe Ably::Realtime::Auth, :event_machine do
1031
1029
  end
1032
1030
  end
1033
1031
  end
1032
+
1033
+ context 'when using JWT' do
1034
+ let(:auth_url) { 'https://echo.ably.io/createJWT' }
1035
+ let(:auth_params) { { keyName: key_name, keySecret: key_secret } }
1036
+ let(:channel_name) { "test_JWT_#{random_str}" }
1037
+ let(:message_name) { 'message_JWT' }
1038
+
1039
+ # RSA8g
1040
+ context 'when using auth_url' do
1041
+ let(:client_options) { default_options.merge(auth_url: auth_url, auth_params: auth_params) }
1042
+
1043
+ context 'when credentials are valid' do
1044
+ it 'client successfully fetches a channel and publishes a message' do
1045
+ channel = client.channels.get(channel_name)
1046
+ channel.subscribe do |message|
1047
+ expect(message.name).to eql(message_name)
1048
+ stop_reactor
1049
+ end
1050
+ channel.publish message_name
1051
+ end
1052
+ end
1053
+
1054
+ context 'when credentials are wrong' do
1055
+ let(:auth_params) { { keyName: key_name, keySecret: 'invalid' } }
1056
+
1057
+ it 'disconnected includes and invalid signature message' do
1058
+ client.connection.once(:disconnected) do |state_change|
1059
+ expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil
1060
+ expect(state_change.reason.code).to eql(40144)
1061
+ stop_reactor
1062
+ end
1063
+ client.connect
1064
+ end
1065
+ end
1066
+
1067
+ context 'when token is expired' do
1068
+ let(:token_duration) { 5 }
1069
+ let(:auth_params) { { keyName: key_name, keySecret: key_secret, expiresIn: token_duration } }
1070
+ it 'receives a 40142 error from the server' do
1071
+ client.connection.once(:connected) do
1072
+ client.connection.once(:disconnected) do |state_change|
1073
+ expect(state_change.reason).to be_a(Ably::Models::ErrorInfo)
1074
+ expect(state_change.reason.message).to match(/(expire)/i)
1075
+ expect(state_change.reason.code).to eql(40142)
1076
+ stop_reactor
1077
+ end
1078
+ end
1079
+ end
1080
+ end
1081
+ end
1082
+
1083
+ # RSA8g
1084
+ context 'when using auth_callback' do
1085
+ let(:token_callback) do
1086
+ lambda do |token_params|
1087
+ Ably::Rest::Client.new(default_options).auth.request_token({}, { auth_url: auth_url, auth_params: auth_params }).token
1088
+ end
1089
+ end
1090
+ let(:client_options) { default_options.merge(auth_callback: token_callback) }
1091
+ WebMock.allow_net_connect!
1092
+ WebMock.disable!
1093
+ context 'when credentials are valid' do
1094
+
1095
+ it 'authentication succeeds and client can post a message' do
1096
+ channel = client.channels.get(channel_name)
1097
+ channel.subscribe do |message|
1098
+ expect(message.name).to eql(message_name)
1099
+ stop_reactor
1100
+ end
1101
+ channel.publish(message_name) do
1102
+ # assert_requested :get, Addressable::Template.new("#{auth_url}{?keyName,keySecret}")
1103
+ end
1104
+ end
1105
+ end
1106
+
1107
+ context 'when credentials are invalid' do
1108
+ let(:auth_params) { { keyName: key_name, keySecret: 'invalid' } }
1109
+
1110
+ it 'authentication fails and reason for disconnection is invalid signature' do
1111
+ client.connection.once(:disconnected) do |state_change|
1112
+ expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil
1113
+ expect(state_change.reason.code).to eql(40144)
1114
+ stop_reactor
1115
+ end
1116
+ client.connect
1117
+ end
1118
+ end
1119
+ end
1120
+
1121
+ context 'when the client is initialized with ClientOptions and the token is a JWT token' do
1122
+ let(:client_options) { { token: token, environment: environment, protocol: protocol } }
1123
+
1124
+ context 'when credentials are valid' do
1125
+ let(:token) { Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}").body }
1126
+
1127
+ it 'posts successfully to a channel' do
1128
+ channel = client.channels.get(channel_name)
1129
+ channel.subscribe do |message|
1130
+ expect(message.name).to eql(message_name)
1131
+ stop_reactor
1132
+ end
1133
+ channel.publish(message_name)
1134
+ end
1135
+ end
1136
+
1137
+ context 'when credentials are invalid' do
1138
+ let(:key_secret) { 'invalid' }
1139
+ let(:token) { Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}").body }
1140
+
1141
+ it 'fails with an invalid signature error' do
1142
+ client.connection.once(:disconnected) do |state_change|
1143
+ expect(state_change.reason.message.match(/invalid signature/i)).to_not be_nil
1144
+ expect(state_change.reason.code).to eql(40144)
1145
+ stop_reactor
1146
+ end
1147
+ client.connect
1148
+ end
1149
+ end
1150
+ end
1151
+
1152
+ context 'when JWT token expires' do
1153
+ before do
1154
+ stub_const 'Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER', 0 # allow token to be used even if about to expire
1155
+ stub_const 'Ably::Auth::TOKEN_DEFAULTS', Ably::Auth::TOKEN_DEFAULTS.merge(renew_token_buffer: 0) # Ensure tokens issued expire immediately after issue
1156
+ end
1157
+ let(:token_callback) do
1158
+ lambda do |token_params|
1159
+ # Ably in all environments other than production will send AUTH 5 seconds before expiry, so
1160
+ # we generate a JWT that expires in 5s so that the window for Realtime to send has passed
1161
+ tokenResponse = Faraday.get "#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&expiresIn=5"
1162
+ tokenResponse.body
1163
+ end
1164
+ end
1165
+ let(:client_options) { default_options.merge(use_token_auth: true, auth_callback: token_callback) }
1166
+
1167
+ # RTC8a
1168
+ it 'client disconnects, a new token is requested via auth_callback and the client gets reconnected' do
1169
+ client.connection.once(:connected) do
1170
+ original_token = auth.current_token_details
1171
+ original_conn_id = client.connection.id
1172
+
1173
+ client.connection.once(:disconnected) do |state_change|
1174
+ expect(state_change.reason.code).to eql(40142)
1175
+
1176
+ client.connection.once(:connected) do
1177
+ expect(original_token).to_not eql(auth.current_token_details)
1178
+ expect(original_conn_id).to eql(client.connection.id)
1179
+ stop_reactor
1180
+ end
1181
+ end
1182
+ end
1183
+ end
1184
+
1185
+ context 'and an AUTH procol message is received' do
1186
+ let(:token_callback) do
1187
+ lambda do |token_params|
1188
+ # Ably in all environments other than local will send AUTH 30 seconds before expiry
1189
+ # We set the TTL to 35s so there's room to receive an AUTH protocol message
1190
+ tokenResponse = Faraday.get "#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&expiresIn=35"
1191
+ tokenResponse.body
1192
+ end
1193
+ end
1194
+
1195
+ # RTC8a, RTC8a4
1196
+ it 'client reauths correctly without going through a disconnection' do
1197
+ client.connection.once(:connected) do
1198
+ original_token = client.auth.current_token_details
1199
+ received_auth = false
1200
+
1201
+ client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
1202
+ received_auth = true if protocol_message.action == :auth
1203
+ end
1204
+
1205
+ client.connection.once(:update) do
1206
+ expect(received_auth).to be_truthy
1207
+ expect(original_token).to_not eql(client.auth.current_token_details)
1208
+ stop_reactor
1209
+ end
1210
+ end
1211
+ end
1212
+ end
1213
+ end
1214
+
1215
+ context 'when the JWT token request includes a client_id' do
1216
+ let(:client_id) { random_str }
1217
+ let(:auth_callback) do
1218
+ lambda do |token_params|
1219
+ Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&client_id=#{client_id}").body
1220
+ end
1221
+ end
1222
+ let(:client_options) { default_options.merge(auth_callback: auth_callback) }
1223
+
1224
+ it 'the client_id is the same that was specified in the auth_callback that generated the JWT token' do
1225
+ client.connection.once(:connected) do
1226
+ expect(client.auth.client_id).to eql(client_id)
1227
+ stop_reactor
1228
+ end
1229
+ end
1230
+ end
1231
+
1232
+ context 'when the JWT token request includes a subscribe-only capability' do
1233
+ let(:channel_with_publish_permissions) { "test_JWT_with_publish_#{random_str}" }
1234
+ let(:basic_capability) { JSON.dump(channel_name => ['subscribe'], channel_with_publish_permissions => ['publish']) }
1235
+ let(:auth_callback) do
1236
+ lambda do |token_params|
1237
+ Faraday.get("#{auth_url}?keyName=#{key_name}&keySecret=#{key_secret}&capability=#{URI.escape(basic_capability)}").body
1238
+ end
1239
+ end
1240
+ let(:client_options) { default_options.merge(auth_callback: auth_callback) }
1241
+
1242
+ it 'client fails to publish to a channel with subscribe-only capability and publishes successfully on a channel with permissions' do
1243
+ client.connection.once(:connected) do
1244
+ forbidden_channel = client.channels.get(channel_name)
1245
+ allowed_channel = client.channels.get(channel_with_publish_permissions)
1246
+ forbidden_channel.publish('not-allowed').errback do |error|
1247
+ expect(error.code).to eql(40160)
1248
+ expect(error.message).to match(/permission denied/)
1249
+
1250
+ allowed_channel.publish(message_name) do |message|
1251
+ expect(message.name).to eql(message_name)
1252
+ stop_reactor
1253
+ end
1254
+ end
1255
+ end
1256
+ end
1257
+ end
1258
+ end
1034
1259
  end
1035
1260
  end