ably 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -1
  3. data/ably.gemspec +4 -3
  4. data/lib/ably.rb +6 -2
  5. data/lib/ably/auth.rb +24 -16
  6. data/lib/ably/exceptions.rb +16 -5
  7. data/lib/ably/{realtime/models → models}/error_info.rb +9 -11
  8. data/lib/ably/models/idiomatic_ruby_wrapper.rb +57 -26
  9. data/lib/ably/{realtime/models → models}/message.rb +45 -38
  10. data/lib/ably/{realtime/models → models}/nil_channel.rb +4 -4
  11. data/lib/ably/{rest/models/paged_resource.rb → models/paginated_resource.rb} +21 -10
  12. data/lib/ably/models/presence_message.rb +126 -0
  13. data/lib/ably/{realtime/models → models}/protocol_message.rb +76 -38
  14. data/lib/ably/models/token.rb +74 -0
  15. data/lib/ably/modules/channels_collection.rb +49 -0
  16. data/lib/ably/modules/conversions.rb +2 -0
  17. data/lib/ably/modules/event_emitter.rb +43 -8
  18. data/lib/ably/modules/event_machine_helpers.rb +1 -0
  19. data/lib/ably/modules/http_helpers.rb +9 -2
  20. data/lib/ably/modules/message_pack.rb +14 -0
  21. data/lib/ably/modules/model_common.rb +29 -0
  22. data/lib/ably/modules/{state.rb → state_emitter.rb} +8 -7
  23. data/lib/ably/realtime.rb +37 -7
  24. data/lib/ably/realtime/channel.rb +154 -31
  25. data/lib/ably/realtime/channels.rb +47 -0
  26. data/lib/ably/realtime/client.rb +39 -33
  27. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +50 -21
  28. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +9 -11
  29. data/lib/ably/realtime/connection.rb +148 -79
  30. data/lib/ably/realtime/connection/connection_state_machine.rb +111 -0
  31. data/lib/ably/realtime/connection/websocket_transport.rb +161 -0
  32. data/lib/ably/realtime/presence.rb +270 -0
  33. data/lib/ably/rest.rb +14 -3
  34. data/lib/ably/rest/channel.rb +3 -3
  35. data/lib/ably/rest/channels.rb +26 -12
  36. data/lib/ably/rest/client.rb +42 -25
  37. data/lib/ably/rest/middleware/exceptions.rb +21 -23
  38. data/lib/ably/rest/middleware/external_exceptions.rb +8 -10
  39. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +17 -0
  40. data/lib/ably/rest/middleware/parse_json.rb +9 -2
  41. data/lib/ably/rest/middleware/parse_message_pack.rb +6 -2
  42. data/lib/ably/rest/presence.rb +4 -4
  43. data/lib/ably/version.rb +1 -1
  44. data/spec/acceptance/realtime/channel_history_spec.rb +125 -0
  45. data/spec/acceptance/realtime/channel_spec.rb +135 -63
  46. data/spec/acceptance/realtime/connection_spec.rb +86 -0
  47. data/spec/acceptance/realtime/message_spec.rb +116 -94
  48. data/spec/acceptance/realtime/presence_history_spec.rb +0 -0
  49. data/spec/acceptance/realtime/presence_spec.rb +277 -0
  50. data/spec/acceptance/rest/auth_spec.rb +351 -347
  51. data/spec/acceptance/rest/base_spec.rb +43 -26
  52. data/spec/acceptance/rest/channel_spec.rb +88 -83
  53. data/spec/acceptance/rest/channels_spec.rb +32 -28
  54. data/spec/acceptance/rest/presence_spec.rb +83 -63
  55. data/spec/acceptance/rest/stats_spec.rb +38 -37
  56. data/spec/acceptance/rest/time_spec.rb +10 -6
  57. data/spec/integration/modules/{state_spec.rb → state_emitter_spec.rb} +16 -2
  58. data/spec/spec_helper.rb +14 -0
  59. data/spec/support/api_helper.rb +4 -0
  60. data/spec/support/model_helper.rb +28 -9
  61. data/spec/support/protocol_msgbus_helper.rb +8 -1
  62. data/spec/support/test_app.rb +24 -14
  63. data/spec/unit/{realtime → models}/error_info_spec.rb +4 -4
  64. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +46 -9
  65. data/spec/unit/models/message_spec.rb +229 -0
  66. data/spec/unit/{rest/paged_resource_spec.rb → models/paginated_resource_spec.rb} +19 -11
  67. data/spec/unit/models/presence_message_spec.rb +230 -0
  68. data/spec/unit/models/protocol_message_spec.rb +280 -0
  69. data/spec/unit/{token_spec.rb → models/token_spec.rb} +18 -22
  70. data/spec/unit/modules/conversions_spec.rb +1 -1
  71. data/spec/unit/modules/event_emitter_spec.rb +36 -4
  72. data/spec/unit/realtime/channel_spec.rb +76 -2
  73. data/spec/unit/realtime/channels_spec.rb +50 -0
  74. data/spec/unit/realtime/client_spec.rb +31 -1
  75. data/spec/unit/realtime/connection_spec.rb +8 -15
  76. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +6 -6
  77. data/spec/unit/realtime/presence_spec.rb +100 -0
  78. data/spec/unit/rest/channels_spec.rb +48 -0
  79. metadata +72 -38
  80. data/lib/ably/realtime/models/shared.rb +0 -17
  81. data/lib/ably/rest/models/message.rb +0 -64
  82. data/lib/ably/rest/models/presence_message.rb +0 -21
  83. data/lib/ably/token.rb +0 -80
  84. data/spec/unit/realtime/message_spec.rb +0 -117
  85. data/spec/unit/realtime/protocol_message_spec.rb +0 -172
  86. data/spec/unit/rest/message_spec.rb +0 -75
@@ -0,0 +1,280 @@
1
+ require 'spec_helper'
2
+ require 'support/model_helper'
3
+
4
+ describe Ably::Models::ProtocolMessage do
5
+ include Ably::Modules::Conversions
6
+ subject { Ably::Models::ProtocolMessage }
7
+
8
+ def new_protocol_message(options)
9
+ subject.new({ action: 1 }.merge(options))
10
+ end
11
+
12
+ it_behaves_like 'a model',
13
+ with_simple_attributes: %w(id channel channel_serial connection_id),
14
+ base_model_options: { action: 1 } do
15
+
16
+ let(:model_args) { [] }
17
+ end
18
+
19
+ context 'initializer action coercion' do
20
+ it 'ignores actions that are Integers' do
21
+ protocol_message = subject.new(action: 14)
22
+ expect(protocol_message.hash[:action]).to eql(14)
23
+ end
24
+
25
+ it 'converts actions to Integers if a symbol' do
26
+ protocol_message = subject.new(action: :message)
27
+ expect(protocol_message.hash[:action]).to eql(15)
28
+ end
29
+
30
+ it 'converts actions to Integers if a ACTION' do
31
+ protocol_message = subject.new(action: Ably::Models::ProtocolMessage::ACTION.Message)
32
+ expect(protocol_message.hash[:action]).to eql(15)
33
+ end
34
+
35
+ it 'raises an argument error if nil' do
36
+ expect { subject.new({}) }.to raise_error(ArgumentError)
37
+ end
38
+ end
39
+
40
+ context 'attributes' do
41
+ let(:unique_value) { SecureRandom.hex }
42
+
43
+ context 'Java naming' do
44
+ let(:protocol_message) { new_protocol_message(channelSerial: unique_value) }
45
+
46
+ it 'converts the attribute to ruby symbol naming convention' do
47
+ expect(protocol_message.channel_serial).to eql(unique_value)
48
+ end
49
+ end
50
+
51
+ context '#action' do
52
+ let(:protocol_message) { new_protocol_message(action: 14) }
53
+
54
+ it 'returns an Enum that behaves like a symbol' do
55
+ expect(protocol_message.action).to eq(:presence)
56
+ end
57
+
58
+ it 'returns an Enum that behaves like a Numeric' do
59
+ expect(protocol_message.action).to eq(14)
60
+ end
61
+
62
+ it 'returns an Enum that behaves like a String' do
63
+ expect(protocol_message.action).to eq('Presence')
64
+ end
65
+
66
+ it 'returns an Enum that matchdes the ACTION constant' do
67
+ expect(protocol_message.action).to eql(Ably::Models::ProtocolMessage::ACTION.Presence)
68
+ end
69
+ end
70
+
71
+ context '#timestamp' do
72
+ let(:protocol_message) { new_protocol_message(timestamp: as_since_epoch(Time.now)) }
73
+ it 'retrieves attribute :timestamp' do
74
+ expect(protocol_message.timestamp).to be_a(Time)
75
+ expect(protocol_message.timestamp.to_i).to be_within(1).of(Time.now.to_i)
76
+ end
77
+ end
78
+
79
+ context '#count' do
80
+ context 'when missing' do
81
+ let(:protocol_message) { new_protocol_message({}) }
82
+ it 'is 1' do
83
+ expect(protocol_message.count).to eql(1)
84
+ end
85
+ end
86
+
87
+ context 'when non numeric' do
88
+ let(:protocol_message) { new_protocol_message(count: 'A') }
89
+ it 'is 1' do
90
+ expect(protocol_message.count).to eql(1)
91
+ end
92
+ end
93
+
94
+ context 'when greater than 1' do
95
+ let(:protocol_message) { new_protocol_message(count: '666') }
96
+ it 'is the value of count' do
97
+ expect(protocol_message.count).to eql(666)
98
+ end
99
+ end
100
+ end
101
+
102
+ context '#message_serial' do
103
+ let(:protocol_message) { new_protocol_message(msg_serial: "55") }
104
+ it 'converts :msg_serial to an Integer' do
105
+ expect(protocol_message.message_serial).to be_a(Integer)
106
+ expect(protocol_message.message_serial).to eql(55)
107
+ end
108
+ end
109
+
110
+ context '#has_message_serial?' do
111
+ context 'without msg_serial' do
112
+ let(:protocol_message) { new_protocol_message({}) }
113
+
114
+ it 'returns false' do
115
+ expect(protocol_message.has_message_serial?).to eql(false)
116
+ end
117
+ end
118
+
119
+ context 'with msg_serial' do
120
+ let(:protocol_message) { new_protocol_message(msg_serial: "55") }
121
+
122
+ it 'returns true' do
123
+ expect(protocol_message.has_message_serial?).to eql(true)
124
+ end
125
+ end
126
+ end
127
+
128
+ context '#connection_serial' do
129
+ let(:protocol_message) { new_protocol_message(connection_serial: "55") }
130
+ it 'converts :connection_serial to an Integer' do
131
+ expect(protocol_message.connection_serial).to be_a(Integer)
132
+ expect(protocol_message.connection_serial).to eql(55)
133
+ end
134
+ end
135
+
136
+ context '#has_connection_serial?' do
137
+ context 'without connection_serial' do
138
+ let(:protocol_message) { new_protocol_message({}) }
139
+
140
+ it 'returns false' do
141
+ expect(protocol_message.has_connection_serial?).to eql(false)
142
+ end
143
+ end
144
+
145
+ context 'with connection_serial' do
146
+ let(:protocol_message) { new_protocol_message(connection_serial: "55") }
147
+
148
+ it 'returns true' do
149
+ expect(protocol_message.has_connection_serial?).to eql(true)
150
+ end
151
+ end
152
+ end
153
+
154
+ context '#serial' do
155
+ context 'with underlying msg_serial' do
156
+ let(:protocol_message) { new_protocol_message(msg_serial: "55") }
157
+ it 'converts :msg_serial to an Integer' do
158
+ expect(protocol_message.serial).to be_a(Integer)
159
+ expect(protocol_message.serial).to eql(55)
160
+ end
161
+ end
162
+
163
+ context 'with underlying connection_serial' do
164
+ let(:protocol_message) { new_protocol_message(connection_serial: "55") }
165
+ it 'converts :connection_serial to an Integer' do
166
+ expect(protocol_message.serial).to be_a(Integer)
167
+ expect(protocol_message.serial).to eql(55)
168
+ end
169
+ end
170
+
171
+ context 'with underlying connection_serial and msg_serial' do
172
+ let(:protocol_message) { new_protocol_message(connection_serial: "99", msg_serial: "11") }
173
+ it 'prefers connection_serial and converts :connection_serial to an Integer' do
174
+ expect(protocol_message.serial).to be_a(Integer)
175
+ expect(protocol_message.serial).to eql(99)
176
+ end
177
+ end
178
+ end
179
+
180
+ context '#has_serial?' do
181
+ context 'without msg_serial or connection_serial' do
182
+ let(:protocol_message) { new_protocol_message({}) }
183
+
184
+ it 'returns false' do
185
+ expect(protocol_message.has_serial?).to eql(false)
186
+ end
187
+ end
188
+
189
+ context 'with msg_serial' do
190
+ let(:protocol_message) { new_protocol_message(msg_serial: "55") }
191
+
192
+ it 'returns true' do
193
+ expect(protocol_message.has_serial?).to eql(true)
194
+ end
195
+ end
196
+
197
+ context 'with connection_serial' do
198
+ let(:protocol_message) { new_protocol_message(connection_serial: "55") }
199
+
200
+ it 'returns true' do
201
+ expect(protocol_message.has_serial?).to eql(true)
202
+ end
203
+ end
204
+ end
205
+
206
+ context '#error' do
207
+ context 'with no error attribute' do
208
+ let(:protocol_message) { new_protocol_message(action: 1) }
209
+
210
+ it 'returns nil' do
211
+ expect(protocol_message.error).to be_nil
212
+ end
213
+ end
214
+
215
+ context 'with nil error' do
216
+ let(:protocol_message) { new_protocol_message(error: nil) }
217
+
218
+ it 'returns nil' do
219
+ expect(protocol_message.error).to be_nil
220
+ end
221
+ end
222
+
223
+ context 'with error' do
224
+ let(:protocol_message) { new_protocol_message(error: { message: 'test_error' }) }
225
+
226
+ it 'returns a valid ErrorInfo object' do
227
+ expect(protocol_message.error).to be_a(Ably::Models::ErrorInfo)
228
+ expect(protocol_message.error.message).to eql('test_error')
229
+ end
230
+ end
231
+ end
232
+ end
233
+
234
+ context '#to_json' do
235
+ let(:json_object) { JSON.parse(model.to_json) }
236
+ let(:message) { { 'name' => 'event', 'clientId' => 'joe', 'timestamp' => as_since_epoch(Time.now) } }
237
+ let(:attached_action) { Ably::Models::ProtocolMessage::ACTION.Attached }
238
+ let(:message_action) { Ably::Models::ProtocolMessage::ACTION.Message }
239
+
240
+ context 'with valid data' do
241
+ let(:model) { new_protocol_message({ :action => attached_action, :channelSerial => 'unique', messages: [message] }) }
242
+
243
+ it 'converts the attribute back to Java mixedCase notation using string keys' do
244
+ expect(json_object["channelSerial"]).to eql('unique')
245
+ end
246
+
247
+ it 'populates the messages' do
248
+ expect(json_object["messages"].first).to include(message)
249
+ end
250
+ end
251
+
252
+ context 'with missing msg_serial for ack message' do
253
+ let(:model) { new_protocol_message({ :action => message_action }) }
254
+
255
+ it 'it raises an exception' do
256
+ expect { model.to_json }.to raise_error TypeError, /msg_serial.*missing/
257
+ end
258
+ end
259
+
260
+ context 'is aliased by #to_s' do
261
+ let(:model) { new_protocol_message({ :action => attached_action, :channelSerial => 'unique', messages: [message], :timestamp => as_since_epoch(Time.now) }) }
262
+
263
+ specify do
264
+ expect(json_object).to eql(JSON.parse("#{model}"))
265
+ end
266
+ end
267
+ end
268
+
269
+ context '#to_msgpack' do
270
+ let(:model) { new_protocol_message({ :connectionSerial => 'unique', messages: [message] }) }
271
+ let(:message) { { 'name' => 'event', 'clientId' => 'joe', 'timestamp' => as_since_epoch(Time.now) } }
272
+ let(:packed) { model.to_msgpack }
273
+ let(:unpacked) { MessagePack.unpack(packed) }
274
+
275
+ it 'returns a unpackable msgpack object' do
276
+ expect(unpacked['connectionSerial']).to eq('unique')
277
+ expect(unpacked['messages'][0]['name']).to eq('event')
278
+ end
279
+ end
280
+ end
@@ -1,38 +1,34 @@
1
1
  require "spec_helper"
2
2
 
3
- describe Ably::Token do
3
+ describe Ably::Models::Token do
4
+ subject { Ably::Models::Token }
5
+
6
+ it_behaves_like 'a model', with_simple_attributes: %w(id capability client_id nonce) do
7
+ let(:model_args) { [] }
8
+ end
9
+
4
10
  context 'defaults' do
5
11
  let(:one_hour) { 60 * 60 }
6
12
  let(:all_capabilities) { { "*" => ["*"] } }
7
13
 
8
14
  it 'should default TTL to 1 hour' do
9
- expect(Ably::Token::DEFAULTS[:ttl]).to eql(one_hour)
15
+ expect(Ably::Models::Token::DEFAULTS[:ttl]).to eql(one_hour)
10
16
  end
11
17
 
12
18
  it 'should default capability to all' do
13
- expect(Ably::Token::DEFAULTS[:capability]).to eql(all_capabilities)
19
+ expect(Ably::Models::Token::DEFAULTS[:capability]).to eql(all_capabilities)
14
20
  end
15
21
 
16
22
  it 'should only have defaults for :ttl and :capability' do
17
- expect(Ably::Token::DEFAULTS.keys).to contain_exactly(:ttl, :capability)
23
+ expect(Ably::Models::Token::DEFAULTS.keys).to contain_exactly(:ttl, :capability)
18
24
  end
19
25
  end
20
26
 
21
27
  context 'attributes' do
22
28
  let(:unique_value) { 'unique_value' }
23
29
 
24
- %w(id capability client_id nonce).each do |attribute|
25
- context "##{attribute}" do
26
- subject { Ably::Token.new({ attribute.to_sym => unique_value }) }
27
-
28
- it "retrieves attribute :#{attribute}" do
29
- expect(subject.public_send(attribute)).to eql(unique_value)
30
- end
31
- end
32
- end
33
-
34
30
  context '#key_id' do
35
- subject { Ably::Token.new({ key: unique_value }) }
31
+ subject { Ably::Models::Token.new({ key: unique_value }) }
36
32
  it 'retrieves attribute :key' do
37
33
  expect(subject.key_id).to eql(unique_value)
38
34
  end
@@ -41,7 +37,7 @@ describe Ably::Token do
41
37
  { :issued_at => :issued_at, :expires_at => :expires }.each do |method_name, attribute|
42
38
  let(:time) { Time.now }
43
39
  context "##{method_name}" do
44
- subject { Ably::Token.new({ attribute.to_sym => time.to_i }) }
40
+ subject { Ably::Models::Token.new({ attribute.to_sym => time.to_i }) }
45
41
 
46
42
  it "retrieves attribute :#{attribute} as Time" do
47
43
  expect(subject.public_send(method_name)).to be_a(Time)
@@ -51,10 +47,10 @@ describe Ably::Token do
51
47
  end
52
48
 
53
49
  context '#expired?' do
54
- let(:expire_time) { Time.now + Ably::Token::TOKEN_EXPIRY_BUFFER }
50
+ let(:expire_time) { Time.now + Ably::Models::Token::TOKEN_EXPIRY_BUFFER }
55
51
 
56
52
  context 'once grace period buffer has passed' do
57
- subject { Ably::Token.new(expires: expire_time - 1) }
53
+ subject { Ably::Models::Token.new(expires: expire_time - 1) }
58
54
 
59
55
  it 'is true' do
60
56
  expect(subject.expired?).to eql(true)
@@ -62,7 +58,7 @@ describe Ably::Token do
62
58
  end
63
59
 
64
60
  context 'within grace period buffer' do
65
- subject { Ably::Token.new(expires: expire_time + 1) }
61
+ subject { Ably::Models::Token.new(expires: expire_time + 1) }
66
62
 
67
63
  it 'is false' do
68
64
  expect(subject.expired?).to eql(false)
@@ -75,16 +71,16 @@ describe Ably::Token do
75
71
  let(:token_attributes) { { id: 'unique' } }
76
72
 
77
73
  it 'is true when attributes are the same' do
78
- new_token = -> { Ably::Token.new(token_attributes) }
74
+ new_token = -> { Ably::Models::Token.new(token_attributes) }
79
75
  expect(new_token[]).to eq(new_token[])
80
76
  end
81
77
 
82
78
  it 'is false when attributes are not the same' do
83
- expect(Ably::Token.new(id: 1)).to_not eq(Ably::Token.new(id: 2))
79
+ expect(Ably::Models::Token.new(id: 1)).to_not eq(Ably::Models::Token.new(id: 2))
84
80
  end
85
81
 
86
82
  it 'is false when class type differs' do
87
- expect(Ably::Token.new(id: 1)).to_not eq(nil)
83
+ expect(Ably::Models::Token.new(id: 1)).to_not eq(nil)
88
84
  end
89
85
  end
90
86
  end
@@ -49,7 +49,7 @@ describe Ably::Modules::Conversions do
49
49
  let(:seconds) { Time.new.to_f }
50
50
 
51
51
  it 'converts to Time from milliseconds by default' do
52
- expect(subject.as_time_from_epoch(millisecond).to_f).to be_within(0.001).of(time.to_f)
52
+ expect(subject.as_time_from_epoch(millisecond).to_f).to be_within(0.01).of(time.to_f)
53
53
  end
54
54
 
55
55
  it 'converts to Time from seconds' do
@@ -16,16 +16,18 @@ describe Ably::Modules::EventEmitter do
16
16
 
17
17
  context 'event fan out' do
18
18
  specify do
19
- expect(obj).to receive(:received_message).with(msg).twice
20
19
  2.times do
21
20
  subject.on(:message) { |msg| obj.received_message msg }
22
21
  end
22
+
23
+ expect(obj).to receive(:received_message).with(msg).twice
23
24
  subject.trigger :message, msg
24
25
  end
25
26
 
26
27
  it 'sends only messages to matching event names' do
27
- expect(obj).to receive(:received_message).with(msg).once
28
28
  subject.on(:valid) { |msg| obj.received_message msg }
29
+
30
+ expect(obj).to receive(:received_message).with(msg).once
29
31
  subject.trigger :valid, msg
30
32
  subject.trigger :ignored, msg
31
33
  subject.trigger 'valid', msg
@@ -37,19 +39,49 @@ describe Ably::Modules::EventEmitter do
37
39
  end
38
40
 
39
41
  it 'calls the provided proc to coerce the event name' do
40
- expect(obj).to receive(:received_message).with(msg).once
41
42
  subject.on('valid') { |msg| obj.received_message msg }
43
+
44
+ expect(obj).to receive(:received_message).with(msg).once
42
45
  subject.trigger :valid, msg
43
46
  end
44
47
  end
45
48
 
46
49
  context 'without coercion' do
47
50
  it 'only matches event names on type matches' do
48
- expect(obj).to_not receive(:received_message).with(msg)
49
51
  subject.on('valid') { |msg| obj.received_message msg }
52
+
53
+ expect(obj).to_not receive(:received_message).with(msg)
50
54
  subject.trigger :valid, msg
51
55
  end
52
56
  end
57
+
58
+ context 'subscribe to multiple events' do
59
+ it 'with the same block' do
60
+ subject.on(:click, :hover) { |msg| obj.received_message msg }
61
+
62
+ expect(obj).to receive(:received_message).with(msg).twice
63
+
64
+ subject.trigger :click, msg
65
+ subject.trigger :hover, msg
66
+ end
67
+ end
68
+ end
69
+
70
+ context '#once' do
71
+ it 'calls the block the first time an event is emitted only' do
72
+ block_called = 0
73
+ subject.once('event') { block_called += 1 }
74
+ 3.times { subject.trigger 'event', 'data' }
75
+ expect(block_called).to eql(1)
76
+ end
77
+
78
+ it 'does not remove other blocks after it is called' do
79
+ block_called = 0
80
+ subject.once('event') { block_called += 1 }
81
+ subject.on('event') { block_called += 1 }
82
+ 3.times { subject.trigger 'event', 'data' }
83
+ expect(block_called).to eql(4)
84
+ end
53
85
  end
54
86
 
55
87
  context '#off' do