ably-rest 0.7.1 → 0.7.3
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.
- checksums.yaml +13 -5
- data/.gitmodules +1 -1
- data/.rspec +1 -0
- data/.travis.yml +7 -3
- data/SPEC.md +495 -419
- data/ably-rest.gemspec +19 -5
- data/lib/ably-rest.rb +9 -1
- data/lib/submodules/ably-ruby/.gitignore +6 -0
- data/lib/submodules/ably-ruby/.rspec +1 -0
- data/lib/submodules/ably-ruby/.ruby-version.old +1 -0
- data/lib/submodules/ably-ruby/.travis.yml +10 -0
- data/lib/submodules/ably-ruby/Gemfile +4 -0
- data/lib/submodules/ably-ruby/LICENSE.txt +22 -0
- data/lib/submodules/ably-ruby/README.md +122 -0
- data/lib/submodules/ably-ruby/Rakefile +34 -0
- data/lib/submodules/ably-ruby/SPEC.md +1794 -0
- data/lib/submodules/ably-ruby/ably.gemspec +36 -0
- data/lib/submodules/ably-ruby/lib/ably.rb +12 -0
- data/lib/submodules/ably-ruby/lib/ably/auth.rb +438 -0
- data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +69 -0
- data/lib/submodules/ably-ruby/lib/ably/logger.rb +102 -0
- data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +37 -0
- data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +223 -0
- data/lib/submodules/ably-ruby/lib/ably/models/message.rb +132 -0
- data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +108 -0
- data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base64.rb +40 -0
- data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/cipher.rb +83 -0
- data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/json.rb +34 -0
- data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/utf8.rb +26 -0
- data/lib/submodules/ably-ruby/lib/ably/models/nil_logger.rb +20 -0
- data/lib/submodules/ably-ruby/lib/ably/models/paginated_resource.rb +173 -0
- data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +147 -0
- data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +210 -0
- data/lib/submodules/ably-ruby/lib/ably/models/stat.rb +161 -0
- data/lib/submodules/ably-ruby/lib/ably/models/token.rb +74 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/ably.rb +15 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +62 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/channels_collection.rb +69 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +100 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +69 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +202 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +128 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/event_machine_helpers.rb +26 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/http_helpers.rb +41 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/message_pack.rb +14 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +41 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +153 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +57 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/statesman_monkey_patch.rb +33 -0
- data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +74 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime.rb +64 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +298 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +92 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +69 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/channels.rb +50 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +184 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +184 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +70 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +445 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +368 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +91 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +188 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/models/nil_channel.rb +30 -0
- data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +564 -0
- data/lib/submodules/ably-ruby/lib/ably/rest.rb +43 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +104 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/channels.rb +44 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +396 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/middleware/encoder.rb +49 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +41 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/middleware/external_exceptions.rb +24 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +17 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +58 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_json.rb +27 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +27 -0
- data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +92 -0
- data/lib/submodules/ably-ruby/lib/ably/util/crypto.rb +105 -0
- data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +43 -0
- data/lib/submodules/ably-ruby/lib/ably/version.rb +3 -0
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +154 -0
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +558 -0
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +119 -0
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +575 -0
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +785 -0
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +457 -0
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +55 -0
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1001 -0
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +23 -0
- data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +27 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +564 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +165 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +134 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +41 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +273 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/encoders_spec.rb +185 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +247 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +292 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/stats_spec.rb +172 -0
- data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +15 -0
- data/lib/submodules/ably-ruby/spec/resources/crypto-data-128.json +56 -0
- data/lib/submodules/ably-ruby/spec/resources/crypto-data-256.json +56 -0
- data/lib/submodules/ably-ruby/spec/rspec_config.rb +57 -0
- data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +212 -0
- data/lib/submodules/ably-ruby/spec/shared/model_behaviour.rb +86 -0
- data/lib/submodules/ably-ruby/spec/shared/protocol_msgbus_behaviour.rb +36 -0
- data/lib/submodules/ably-ruby/spec/spec_helper.rb +20 -0
- data/lib/submodules/ably-ruby/spec/support/api_helper.rb +60 -0
- data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +104 -0
- data/lib/submodules/ably-ruby/spec/support/markdown_spec_formatter.rb +118 -0
- data/lib/submodules/ably-ruby/spec/support/private_api_formatter.rb +36 -0
- data/lib/submodules/ably-ruby/spec/support/protocol_helper.rb +32 -0
- data/lib/submodules/ably-ruby/spec/support/random_helper.rb +15 -0
- data/lib/submodules/ably-ruby/spec/support/rest_testapp_before_retry.rb +15 -0
- data/lib/submodules/ably-ruby/spec/support/test_app.rb +113 -0
- data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +68 -0
- data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +146 -0
- data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +18 -0
- data/lib/submodules/ably-ruby/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +349 -0
- data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/base64_spec.rb +181 -0
- data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
- data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/json_spec.rb +135 -0
- data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/utf8_spec.rb +56 -0
- data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +389 -0
- data/lib/submodules/ably-ruby/spec/unit/models/paginated_resource_spec.rb +288 -0
- data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +386 -0
- data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +315 -0
- data/lib/submodules/ably-ruby/spec/unit/models/stat_spec.rb +113 -0
- data/lib/submodules/ably-ruby/spec/unit/models/token_spec.rb +86 -0
- data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +124 -0
- data/lib/submodules/ably-ruby/spec/unit/modules/conversions_spec.rb +72 -0
- data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +272 -0
- data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +184 -0
- data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +283 -0
- data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +206 -0
- data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +81 -0
- data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +30 -0
- data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +33 -0
- data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +36 -0
- data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +111 -0
- data/lib/submodules/ably-ruby/spec/unit/realtime/realtime_spec.rb +9 -0
- data/lib/submodules/ably-ruby/spec/unit/realtime/websocket_transport_spec.rb +25 -0
- data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +109 -0
- data/lib/submodules/ably-ruby/spec/unit/rest/channels_spec.rb +79 -0
- data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +53 -0
- data/lib/submodules/ably-ruby/spec/unit/rest/rest_spec.rb +10 -0
- data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +87 -0
- data/lib/submodules/ably-ruby/spec/unit/util/pub_sub_spec.rb +86 -0
- metadata +182 -27
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
require 'ably/models/message_encoders/utf8'
|
|
4
|
+
|
|
5
|
+
describe Ably::Models::MessageEncoders::Utf8 do
|
|
6
|
+
let(:string_ascii) { 'string'.encode(Encoding::ASCII_8BIT) }
|
|
7
|
+
let(:string_utf8) { 'string'.encode(Encoding::UTF_8) }
|
|
8
|
+
|
|
9
|
+
let(:client) { instance_double('Ably::Realtime::Client') }
|
|
10
|
+
|
|
11
|
+
subject { Ably::Models::MessageEncoders::Utf8.new(client) }
|
|
12
|
+
|
|
13
|
+
context '#decode' do
|
|
14
|
+
before do
|
|
15
|
+
subject.decode message, {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context 'message with utf8 payload' do
|
|
19
|
+
let(:message) { { data: string_ascii, encoding: 'utf-8' } }
|
|
20
|
+
|
|
21
|
+
it 'sets the encoding' do
|
|
22
|
+
expect(message[:data]).to eq(string_utf8)
|
|
23
|
+
expect(message[:data].encoding).to eql(Encoding::UTF_8)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'strips the encoding' do
|
|
27
|
+
expect(message[:encoding]).to be_nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'message with utf8 payload before other payloads' do
|
|
32
|
+
let(:message) { { data: string_utf8, encoding: 'json/utf-8' } }
|
|
33
|
+
|
|
34
|
+
it 'sets the encoding' do
|
|
35
|
+
expect(message[:data]).to eql(string_utf8)
|
|
36
|
+
expect(message[:data].encoding).to eql(Encoding::UTF_8)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'strips the encoding' do
|
|
40
|
+
expect(message[:encoding]).to eql('json')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
context 'message with another payload' do
|
|
45
|
+
let(:message) { { data: string_ascii, encoding: 'json' } }
|
|
46
|
+
|
|
47
|
+
it 'leaves the message data intact' do
|
|
48
|
+
expect(message[:data]).to eql(string_ascii)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'leaves the encoding intact' do
|
|
52
|
+
expect(message[:encoding]).to eql('json')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
require 'spec_helper'
|
|
3
|
+
require 'shared/model_behaviour'
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'msgpack'
|
|
6
|
+
|
|
7
|
+
describe Ably::Models::Message do
|
|
8
|
+
include Ably::Modules::Conversions
|
|
9
|
+
|
|
10
|
+
subject { Ably::Models::Message }
|
|
11
|
+
let(:protocol_message_timestamp) { as_since_epoch(Time.now) }
|
|
12
|
+
let(:protocol_message) { Ably::Models::ProtocolMessage.new(action: 1, timestamp: protocol_message_timestamp) }
|
|
13
|
+
|
|
14
|
+
it_behaves_like 'a model', with_simple_attributes: %w(name client_id data encoding) do
|
|
15
|
+
let(:model_args) { [protocol_message] }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context '#timestamp' do
|
|
19
|
+
let(:model) { subject.new({}, protocol_message) }
|
|
20
|
+
|
|
21
|
+
it 'retrieves attribute :timestamp as Time object from ProtocolMessage' do
|
|
22
|
+
expect(model.timestamp).to be_a(Time)
|
|
23
|
+
expect(model.timestamp.to_i).to be_within(1).of(Time.now.to_i)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
context '#connection_id attribute' do
|
|
28
|
+
let(:protocol_connection_id) { random_str }
|
|
29
|
+
let(:protocol_message) { Ably::Models::ProtocolMessage.new('connectionId' => protocol_connection_id, action: 1, timestamp: protocol_message_timestamp) }
|
|
30
|
+
let(:model_connection_id) { random_str }
|
|
31
|
+
|
|
32
|
+
context 'when this model has a connectionId attribute' do
|
|
33
|
+
context 'but no protocol message' do
|
|
34
|
+
let(:model) { subject.new('connectionId' => model_connection_id ) }
|
|
35
|
+
|
|
36
|
+
it 'uses the model value' do
|
|
37
|
+
expect(model.connection_id).to eql(model_connection_id)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
context 'with a protocol message with a different connectionId' do
|
|
42
|
+
let(:model) { subject.new({ 'connectionId' => model_connection_id }, protocol_message) }
|
|
43
|
+
|
|
44
|
+
it 'uses the model value' do
|
|
45
|
+
expect(model.connection_id).to eql(model_connection_id)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
context 'when this model has no connectionId attribute' do
|
|
51
|
+
context 'and no protocol message' do
|
|
52
|
+
let(:model) { subject.new({ }) }
|
|
53
|
+
|
|
54
|
+
it 'uses the model value' do
|
|
55
|
+
expect(model.connection_id).to be_nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
context 'with a protocol message with a connectionId' do
|
|
60
|
+
let(:model) { subject.new({ }, protocol_message) }
|
|
61
|
+
|
|
62
|
+
it 'uses the model value' do
|
|
63
|
+
expect(model.connection_id).to eql(protocol_connection_id)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'Java naming', :api_private do
|
|
70
|
+
let(:model) { subject.new({ clientId: 'joe' }, protocol_message) }
|
|
71
|
+
|
|
72
|
+
it 'converts the attribute to ruby symbol naming convention' do
|
|
73
|
+
expect(model.client_id).to eql('joe')
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
context 'initialized with' do
|
|
78
|
+
%w(name client_id encoding).each do |attribute|
|
|
79
|
+
context ":#{attribute}" do
|
|
80
|
+
let(:encoded_value) { value.encode(encoding) }
|
|
81
|
+
let(:value) { random_str }
|
|
82
|
+
let(:options) { { attribute.to_sym => encoded_value } }
|
|
83
|
+
let(:model) { subject.new(options, protocol_message) }
|
|
84
|
+
let(:model_attribute) { model.public_send(attribute) }
|
|
85
|
+
|
|
86
|
+
context 'as UTF_8 string' do
|
|
87
|
+
let(:encoding) { Encoding::UTF_8 }
|
|
88
|
+
|
|
89
|
+
it 'is permitted' do
|
|
90
|
+
expect(model_attribute).to eql(encoded_value)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'remains as UTF-8' do
|
|
94
|
+
expect(model_attribute.encoding).to eql(encoding)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
context 'as SHIFT_JIS string' do
|
|
99
|
+
let(:encoding) { Encoding::SHIFT_JIS }
|
|
100
|
+
|
|
101
|
+
it 'gets converted to UTF-8' do
|
|
102
|
+
expect(model_attribute.encoding).to eql(Encoding::UTF_8)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'is compatible with original encoding' do
|
|
106
|
+
expect(model_attribute.encode(encoding)).to eql(encoded_value)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
context 'as ASCII_8BIT string' do
|
|
111
|
+
let(:encoding) { Encoding::ASCII_8BIT }
|
|
112
|
+
|
|
113
|
+
it 'gets converted to UTF-8' do
|
|
114
|
+
expect(model_attribute.encoding).to eql(Encoding::UTF_8)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'is compatible with original encoding' do
|
|
118
|
+
expect(model_attribute.encode(encoding)).to eql(encoded_value)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
context 'as Integer' do
|
|
123
|
+
let(:encoded_value) { 1 }
|
|
124
|
+
|
|
125
|
+
it 'raises an argument error' do
|
|
126
|
+
expect { model_attribute }.to raise_error ArgumentError, /must be a String/
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
context 'as Nil' do
|
|
131
|
+
let(:encoded_value) { nil }
|
|
132
|
+
|
|
133
|
+
it 'is permitted' do
|
|
134
|
+
expect(model_attribute).to be_nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
context '#to_json', :api_private do
|
|
142
|
+
let(:json_object) { JSON.parse(model.to_json) }
|
|
143
|
+
|
|
144
|
+
context 'with valid data' do
|
|
145
|
+
let(:model) { subject.new({ name: 'test', clientId: 'joe' }, protocol_message) }
|
|
146
|
+
|
|
147
|
+
it 'converts the attribute back to Java mixedCase notation using string keys' do
|
|
148
|
+
expect(json_object["clientId"]).to eql('joe')
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
context 'with invalid data' do
|
|
153
|
+
let(:model) { subject.new({ clientId: 'joe' }, protocol_message) }
|
|
154
|
+
|
|
155
|
+
it 'raises an exception' do
|
|
156
|
+
expect { model.to_json }.to raise_error RuntimeError, /cannot generate a valid Hash/
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
context 'with binary data' do
|
|
161
|
+
let(:data) { MessagePack.pack(random_str(32)) }
|
|
162
|
+
let(:model) { subject.new({ name: 'test', data: data }, protocol_message) }
|
|
163
|
+
|
|
164
|
+
it 'encodes as Base64 so that it can be converted to UTF-8 automatically by JSON#dump' do
|
|
165
|
+
expect(json_object["data"]).to eql(::Base64.encode64(data))
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'adds Base64 encoding' do
|
|
169
|
+
expect(json_object["encoding"]).to eql('base64')
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
context 'from REST request with embedded fields', :api_private do
|
|
175
|
+
let(:id) { random_str }
|
|
176
|
+
let(:protocol_message_id) { random_str }
|
|
177
|
+
let(:message_time) { Time.now + 60 }
|
|
178
|
+
let(:message_timestamp) { as_since_epoch(message_time) }
|
|
179
|
+
let(:protocol_time) { Time.now }
|
|
180
|
+
let(:protocol_timestamp) { as_since_epoch(protocol_time) }
|
|
181
|
+
|
|
182
|
+
let(:protocol_message) do
|
|
183
|
+
Ably::Models::ProtocolMessage.new({
|
|
184
|
+
action: :message,
|
|
185
|
+
timestamp: protocol_timestamp,
|
|
186
|
+
id: protocol_message_id
|
|
187
|
+
})
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
context 'with protocol message' do
|
|
191
|
+
let(:model) { subject.new({ id: id, timestamp: message_timestamp }, protocol_message) }
|
|
192
|
+
|
|
193
|
+
specify '#id prefers embedded ID' do
|
|
194
|
+
expect(model.id).to eql(id)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
specify '#timestamp prefers embedded timestamp' do
|
|
198
|
+
expect(model.timestamp.to_i).to be_within(1).of(message_time.to_i)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
context 'without protocol message' do
|
|
203
|
+
let(:model) { subject.new(id: id, timestamp: message_timestamp) }
|
|
204
|
+
|
|
205
|
+
specify '#id uses embedded ID' do
|
|
206
|
+
expect(model.id).to eql(id)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
specify '#timestamp uses embedded timestamp' do
|
|
210
|
+
expect(model.timestamp.to_i).to be_within(1).of(message_time.to_i)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
context 'part of ProtocolMessage', :api_private do
|
|
216
|
+
let(:ably_time) { Time.now + 5 }
|
|
217
|
+
let(:message_serial) { random_int_str(1_000_000) }
|
|
218
|
+
let(:connection_id) { random_str }
|
|
219
|
+
|
|
220
|
+
let(:message_0_payload) do
|
|
221
|
+
{
|
|
222
|
+
'string_key' => 'string_value',
|
|
223
|
+
1 => 2,
|
|
224
|
+
true => false
|
|
225
|
+
}
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
let(:message_0_json) do
|
|
229
|
+
{
|
|
230
|
+
name: 'zero',
|
|
231
|
+
data: message_0_payload
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
let(:message_1_json) do
|
|
236
|
+
{
|
|
237
|
+
name: 'one',
|
|
238
|
+
data: 'simple string'
|
|
239
|
+
}
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
let(:protocol_message_id) { random_str }
|
|
243
|
+
let(:protocol_message) do
|
|
244
|
+
Ably::Models::ProtocolMessage.new({
|
|
245
|
+
action: :message,
|
|
246
|
+
timestamp: ably_time.to_i,
|
|
247
|
+
msg_serial: message_serial,
|
|
248
|
+
id: protocol_message_id,
|
|
249
|
+
messages: [
|
|
250
|
+
message_0_json, message_1_json
|
|
251
|
+
]
|
|
252
|
+
})
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
let(:message_0) { protocol_message.messages.first }
|
|
256
|
+
let(:message_1) { protocol_message.messages.last }
|
|
257
|
+
|
|
258
|
+
it 'should generate a message ID from the index, serial and connection id' do
|
|
259
|
+
expect(message_0.id).to eql("#{protocol_message_id}:0")
|
|
260
|
+
expect(message_1.id).to eql("#{protocol_message_id}:1")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it 'should not modify the data payload' do
|
|
264
|
+
expect(message_0.data['string_key']).to eql('string_value')
|
|
265
|
+
expect(message_0.data[1]).to eql(2)
|
|
266
|
+
expect(message_0.data[true]).to eql(false)
|
|
267
|
+
expect(message_0.data).to eql(message_0_payload)
|
|
268
|
+
|
|
269
|
+
expect(message_1.data).to eql('simple string')
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it 'should not allow changes to the payload' do
|
|
273
|
+
expect { message_0.data["test"] = true }.to raise_error RuntimeError, /can't modify frozen.*Hash/
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
context 'with identical message objects' do
|
|
277
|
+
let(:protocol_message) do
|
|
278
|
+
Ably::Models::ProtocolMessage.new({
|
|
279
|
+
action: :message,
|
|
280
|
+
timestamp: ably_time.to_i,
|
|
281
|
+
msg_serial: message_serial,
|
|
282
|
+
id: protocol_message_id,
|
|
283
|
+
messages: [
|
|
284
|
+
message_0_json, message_0_json, message_0_json
|
|
285
|
+
]
|
|
286
|
+
})
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it 'provide a unique ID:index' do
|
|
290
|
+
expect(protocol_message.messages.map(&:id).uniq.count).to eql(3)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
it 'recognises the index based on the object ID as opposed to message payload' do
|
|
294
|
+
expect(protocol_message.messages.first.id).to match(/0$/)
|
|
295
|
+
expect(protocol_message.messages.last.id).to match(/2$/)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
context 'Message conversion method', :api_private do
|
|
301
|
+
let(:json) { { name: 'test', data: 'conversion' } }
|
|
302
|
+
|
|
303
|
+
context 'with JSON' do
|
|
304
|
+
context 'without ProtocolMessage' do
|
|
305
|
+
subject { Ably::Models.Message(json) }
|
|
306
|
+
|
|
307
|
+
it 'returns a Message object' do
|
|
308
|
+
expect(subject).to be_a(Ably::Models::Message)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it 'initializes with the JSON' do
|
|
312
|
+
expect(subject.name).to eql('test')
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it 'raises an exception when accessing ProtocolMessage' do
|
|
316
|
+
expect { subject.protocol_message }.to raise_error RuntimeError
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
it 'has no ProtocolMessage' do
|
|
320
|
+
expect(subject.assigned_to_protocol_message?).to eql(false)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
context 'with ProtocolMessage' do
|
|
325
|
+
subject { Ably::Models.Message(json, protocol_message) }
|
|
326
|
+
|
|
327
|
+
it 'returns a Message object' do
|
|
328
|
+
expect(subject).to be_a(Ably::Models::Message)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
it 'initializes with the JSON' do
|
|
332
|
+
expect(subject.name).to eql('test')
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it 'provides access to ProtocolMessage' do
|
|
336
|
+
expect(subject.protocol_message).to eql(protocol_message)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it 'has a ProtocolMessage' do
|
|
340
|
+
expect(subject.assigned_to_protocol_message?).to eql(true)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
context 'with another Message' do
|
|
346
|
+
let(:message) { Ably::Models::Message.new(json) }
|
|
347
|
+
|
|
348
|
+
context 'without ProtocolMessage' do
|
|
349
|
+
subject { Ably::Models.Message(message) }
|
|
350
|
+
|
|
351
|
+
it 'returns a Message object' do
|
|
352
|
+
expect(subject).to be_a(Ably::Models::Message)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it 'initializes with the JSON' do
|
|
356
|
+
expect(subject.name).to eql('test')
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
it 'raises an exception when accessing ProtocolMessage' do
|
|
360
|
+
expect { subject.protocol_message }.to raise_error RuntimeError
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
it 'has no ProtocolMessage' do
|
|
364
|
+
expect(subject.assigned_to_protocol_message?).to eql(false)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
context 'with ProtocolMessage' do
|
|
369
|
+
subject { Ably::Models.Message(message, protocol_message) }
|
|
370
|
+
|
|
371
|
+
it 'returns a Message object' do
|
|
372
|
+
expect(subject).to be_a(Ably::Models::Message)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
it 'initializes with the JSON' do
|
|
376
|
+
expect(subject.name).to eql('test')
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
it 'provides access to ProtocolMessage' do
|
|
380
|
+
expect(subject.protocol_message).to eql(protocol_message)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
it 'has a ProtocolMessage' do
|
|
384
|
+
expect(subject.assigned_to_protocol_message?).to eql(true)
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'ostruct'
|
|
3
|
+
|
|
4
|
+
describe Ably::Models::PaginatedResource do
|
|
5
|
+
let(:paginated_resource_class) { Ably::Models::PaginatedResource }
|
|
6
|
+
let(:headers) { Hash.new }
|
|
7
|
+
let(:client) do
|
|
8
|
+
instance_double('Ably::Rest::Client').tap do |client|
|
|
9
|
+
allow(client).to receive(:get).and_return(http_response)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
let(:body) do
|
|
13
|
+
[
|
|
14
|
+
{ id: 0 },
|
|
15
|
+
{ id: 1 }
|
|
16
|
+
]
|
|
17
|
+
end
|
|
18
|
+
let(:http_response) do
|
|
19
|
+
instance_double('Faraday::Response', {
|
|
20
|
+
body: body,
|
|
21
|
+
headers: headers
|
|
22
|
+
})
|
|
23
|
+
end
|
|
24
|
+
let(:base_url) { 'http://rest.ably.io/channels/channel_name' }
|
|
25
|
+
let(:full_url) { "#{base_url}/whatever?param=exists" }
|
|
26
|
+
let(:paginated_resource_options) { Hash.new }
|
|
27
|
+
let(:first_paged_request) { paginated_resource_class.new(http_response, full_url, client, paginated_resource_options) }
|
|
28
|
+
subject { first_paged_request }
|
|
29
|
+
|
|
30
|
+
it 'returns correct length from body' do
|
|
31
|
+
expect(subject.length).to eql(body.length)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'supports alias methods for length' do
|
|
35
|
+
expect(subject.count).to eql(subject.length)
|
|
36
|
+
expect(subject.size).to eql(subject.length)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'is Enumerable' do
|
|
40
|
+
expect(subject).to be_kind_of(Enumerable)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'is iterable' do
|
|
44
|
+
expect(subject.map { |d| d }).to eql(body)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context '#each' do
|
|
48
|
+
it 'returns an enumerator' do
|
|
49
|
+
expect(subject.each).to be_a(Enumerator)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'yields each item' do
|
|
53
|
+
items = []
|
|
54
|
+
subject.each do |item|
|
|
55
|
+
items << item
|
|
56
|
+
end
|
|
57
|
+
expect(items).to eq(body)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'provides [] accessor method' do
|
|
62
|
+
expect(subject[0][:id]).to eql(body[0][:id])
|
|
63
|
+
expect(subject[1][:id]).to eql(body[1][:id])
|
|
64
|
+
expect(subject[2]).to be_nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
specify '#first gets the first item in page' do
|
|
68
|
+
expect(subject.first[:id]).to eql(body[0][:id])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
specify '#last gets the last item in page' do
|
|
72
|
+
expect(subject.last[:id]).to eql(body[1][:id])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'with coercion', :api_private do
|
|
76
|
+
let(:paginated_resource_options) { { coerce_into: 'OpenStruct' } }
|
|
77
|
+
|
|
78
|
+
it 'returns coerced objects' do
|
|
79
|
+
expect(subject.first).to be_a(OpenStruct)
|
|
80
|
+
expect(subject.first.id).to eql(body.first[:id])
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
context 'paged transformations', :api_private do
|
|
85
|
+
let(:headers) do
|
|
86
|
+
{
|
|
87
|
+
'link' => [
|
|
88
|
+
'<./history?index=1>; rel="next"'
|
|
89
|
+
].join(', ')
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
let(:paged_client) do
|
|
93
|
+
instance_double('Ably::Rest::Client').tap do |client|
|
|
94
|
+
allow(client).to receive(:get).and_return(http_response_page2)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
let(:body_page2) do
|
|
98
|
+
[
|
|
99
|
+
{ id: 2 },
|
|
100
|
+
{ id: 3 }
|
|
101
|
+
]
|
|
102
|
+
end
|
|
103
|
+
let(:http_response_page2) do
|
|
104
|
+
instance_double('Faraday::Response', {
|
|
105
|
+
body: body_page2,
|
|
106
|
+
headers: headers
|
|
107
|
+
})
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
context 'with each block' do
|
|
111
|
+
subject do
|
|
112
|
+
paginated_resource_class.new(http_response, full_url, paged_client, paginated_resource_options) do |resource|
|
|
113
|
+
resource[:added_attribute_from_block] = "id:#{resource[:id]}"
|
|
114
|
+
resource
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'calls the block for each resource after retrieving the resources' do
|
|
119
|
+
expect(subject[0][:added_attribute_from_block]).to eql("id:#{body[0][:id]}")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'calls the block for each resource on second page after retrieving the resources' do
|
|
123
|
+
page_1_first_id = subject[0][:id]
|
|
124
|
+
next_page = subject.next_page
|
|
125
|
+
|
|
126
|
+
expect(next_page[0][:added_attribute_from_block]).to eql("id:#{body_page2[0][:id]}")
|
|
127
|
+
expect(next_page[0][:id]).to_not eql(page_1_first_id)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if defined?(Ably::Realtime)
|
|
132
|
+
context 'with option async_blocking_operations: true' do
|
|
133
|
+
include RSpec::EventMachine
|
|
134
|
+
|
|
135
|
+
subject do
|
|
136
|
+
paginated_resource_class.new(http_response, full_url, paged_client, async_blocking_operations: true)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
context '#next_page' do
|
|
140
|
+
it 'returns a deferrable object' do
|
|
141
|
+
run_reactor do
|
|
142
|
+
expect(subject.next_page).to be_a(EventMachine::Deferrable)
|
|
143
|
+
stop_reactor
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'allows a success callback block to be added' do
|
|
148
|
+
run_reactor do
|
|
149
|
+
subject.next_page do |paginated_resource|
|
|
150
|
+
expect(paginated_resource).to be_a(Ably::Models::PaginatedResource)
|
|
151
|
+
stop_reactor
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
context '#first_page' do
|
|
158
|
+
it 'calls the errback callback when first page headers are missing' do
|
|
159
|
+
run_reactor do
|
|
160
|
+
subject.next_page do |paginated_resource|
|
|
161
|
+
deferrable = subject.first_page
|
|
162
|
+
deferrable.errback do |error|
|
|
163
|
+
expect(error).to be_a(Ably::Exceptions::InvalidPageError)
|
|
164
|
+
stop_reactor
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
context 'with non paged http response' do
|
|
175
|
+
it 'is the first page' do
|
|
176
|
+
expect(subject).to be_first_page
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'is the last page' do
|
|
180
|
+
expect(subject).to be_last_page
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'does not support pagination' do
|
|
184
|
+
expect(subject.supports_pagination?).to_not eql(true)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it 'raises an exception when accessing next page' do
|
|
188
|
+
expect { subject.next_page }.to raise_error Ably::Exceptions::InvalidPageError, /Paging header link next/
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'raises an exception when accessing first page' do
|
|
192
|
+
expect { subject.first_page }.to raise_error Ably::Exceptions::InvalidPageError, /Paging header link first/
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
context 'with paged http response' do
|
|
197
|
+
let(:base_url) { 'http://rest.ably.io/channels/channel_name' }
|
|
198
|
+
let(:full_url) { "#{base_url}/messages" }
|
|
199
|
+
let(:headers) do
|
|
200
|
+
{
|
|
201
|
+
'link' => [
|
|
202
|
+
'<./history?index=0>; rel="first"',
|
|
203
|
+
'<./history?index=0>; rel="current"',
|
|
204
|
+
'<./history?index=1>; rel="next"'
|
|
205
|
+
].join(', ')
|
|
206
|
+
}
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it 'is the first page' do
|
|
210
|
+
expect(subject).to be_first_page
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it 'is not the last page' do
|
|
214
|
+
expect(subject).to_not be_last_page
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'supports pagination' do
|
|
218
|
+
expect(subject.supports_pagination?).to eql(true)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
context 'accessing next page' do
|
|
222
|
+
let(:next_body) do
|
|
223
|
+
[ { id: 2 } ]
|
|
224
|
+
end
|
|
225
|
+
let(:next_headers) do
|
|
226
|
+
{
|
|
227
|
+
'link' => [
|
|
228
|
+
'<./history?index=0>; rel="first"',
|
|
229
|
+
'<./history?index=1>; rel="current"'
|
|
230
|
+
].join(', ')
|
|
231
|
+
}
|
|
232
|
+
end
|
|
233
|
+
let(:next_http_response) do
|
|
234
|
+
double('http_response', {
|
|
235
|
+
body: next_body,
|
|
236
|
+
headers: next_headers
|
|
237
|
+
})
|
|
238
|
+
end
|
|
239
|
+
let(:subject) { first_paged_request.next_page }
|
|
240
|
+
|
|
241
|
+
before do
|
|
242
|
+
expect(client).to receive(:get).with("#{base_url}/history?index=1").and_return(next_http_response).once
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it 'returns another PaginatedResource' do
|
|
246
|
+
expect(subject).to be_a(paginated_resource_class)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it 'retrieves the next page of results' do
|
|
250
|
+
expect(subject.length).to eql(next_body.length)
|
|
251
|
+
expect(subject[0][:id]).to eql(next_body[0][:id])
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it 'is not the first page' do
|
|
255
|
+
expect(subject).to_not be_first_page
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'is the last page' do
|
|
259
|
+
expect(subject).to be_last_page
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it 'raises an exception if trying to access the last page when it is the last page' do
|
|
263
|
+
expect(subject).to be_last_page
|
|
264
|
+
expect { subject.next_page }.to raise_error Ably::Exceptions::InvalidPageError, /There are no more pages/
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
context 'and then first page' do
|
|
268
|
+
before do
|
|
269
|
+
expect(client).to receive(:get).with("#{base_url}/history?index=0").and_return(http_response).once
|
|
270
|
+
end
|
|
271
|
+
subject { first_paged_request.next_page.first_page }
|
|
272
|
+
|
|
273
|
+
it 'returns a PaginatedResource' do
|
|
274
|
+
expect(subject).to be_a(paginated_resource_class)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it 'retrieves the first page of results' do
|
|
278
|
+
expect(subject.length).to eql(body.length)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it 'is the first page' do
|
|
282
|
+
expect(subject).to be_first_page
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|