houston 0.2.4 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +28 -8
- data/LICENSE +1 -1
- data/README.md +123 -24
- data/Rakefile +4 -8
- data/bin/apn +70 -5
- data/coverage/assets/0.7.1/application.css +1110 -0
- data/coverage/assets/0.7.1/application.js +626 -0
- data/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
- data/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
- data/coverage/assets/0.7.1/favicon_green.png +0 -0
- data/coverage/assets/0.7.1/favicon_red.png +0 -0
- data/coverage/assets/0.7.1/favicon_yellow.png +0 -0
- data/coverage/assets/0.7.1/loading.gif +0 -0
- data/coverage/assets/0.7.1/magnify.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.8.0/application.css +799 -0
- data/coverage/assets/0.8.0/application.js +1559 -0
- data/coverage/assets/0.8.0/colorbox/border.png +0 -0
- data/coverage/assets/0.8.0/colorbox/controls.png +0 -0
- data/coverage/assets/0.8.0/colorbox/loading.gif +0 -0
- data/coverage/assets/0.8.0/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.8.0/favicon_green.png +0 -0
- data/coverage/assets/0.8.0/favicon_red.png +0 -0
- data/coverage/assets/0.8.0/favicon_yellow.png +0 -0
- data/coverage/assets/0.8.0/loading.gif +0 -0
- data/coverage/assets/0.8.0/magnify.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.8.0/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/index.html +1810 -0
- data/houston-2.2.2.gem +0 -0
- data/houston-2.2.3.gem +0 -0
- data/houston.gemspec +17 -16
- data/lib/houston/client.rb +39 -9
- data/lib/houston/connection.rb +3 -3
- data/lib/houston/notification.rb +87 -6
- data/lib/houston/version.rb +3 -0
- data/lib/houston.rb +1 -4
- data/spec/client_spec.rb +100 -0
- data/spec/notification_spec.rb +315 -0
- data/spec/spec_helper.rb +31 -0
- metadata +119 -32
- data/apple_push_notification_production.pem +0 -136
data/houston-2.2.2.gem
ADDED
Binary file
|
data/houston-2.2.3.gem
ADDED
Binary file
|
data/houston.gemspec
CHANGED
@@ -1,26 +1,27 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
$:.push File.expand_path(
|
3
|
-
require
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'houston/version'
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
|
-
s.name =
|
7
|
-
s.authors = [
|
8
|
-
s.email =
|
9
|
-
s.license =
|
10
|
-
s.homepage =
|
6
|
+
s.name = 'houston'
|
7
|
+
s.authors = ['Mattt Thompson']
|
8
|
+
s.email = 'm@mattt.me'
|
9
|
+
s.license = 'MIT'
|
10
|
+
s.homepage = 'http://nomad-cli.com'
|
11
11
|
s.version = Houston::VERSION
|
12
12
|
s.platform = Gem::Platform::RUBY
|
13
|
-
s.summary =
|
14
|
-
s.description =
|
13
|
+
s.summary = 'Send Apple Push Notifications'
|
14
|
+
s.description = 'Houston is a simple gem for sending Apple Push Notifications. Pass your credentials, construct your message, and send it.'
|
15
15
|
|
16
|
-
s.add_dependency
|
17
|
-
s.add_dependency
|
16
|
+
s.add_dependency 'commander', '~> 4.4'
|
17
|
+
s.add_dependency 'json'
|
18
18
|
|
19
|
-
s.add_development_dependency
|
20
|
-
s.add_development_dependency
|
19
|
+
s.add_development_dependency 'rspec', '~> 3.5'
|
20
|
+
s.add_development_dependency 'rake'
|
21
|
+
s.add_development_dependency 'simplecov'
|
21
22
|
|
22
|
-
s.files = Dir[
|
23
|
+
s.files = Dir['./**/*'].reject { |file| file =~ /\.\/(bin|log|pkg|script|spec|test|vendor)/ }
|
23
24
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
24
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
25
|
-
s.require_paths = [
|
25
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
26
|
+
s.require_paths = ['lib']
|
26
27
|
end
|
data/lib/houston/client.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
module Houston
|
2
|
-
APPLE_PRODUCTION_GATEWAY_URI =
|
3
|
-
APPLE_PRODUCTION_FEEDBACK_URI =
|
2
|
+
APPLE_PRODUCTION_GATEWAY_URI = 'apn://gateway.push.apple.com:2195'
|
3
|
+
APPLE_PRODUCTION_FEEDBACK_URI = 'apn://feedback.push.apple.com:2196'
|
4
4
|
|
5
|
-
APPLE_DEVELOPMENT_GATEWAY_URI =
|
6
|
-
APPLE_DEVELOPMENT_FEEDBACK_URI =
|
5
|
+
APPLE_DEVELOPMENT_GATEWAY_URI = 'apn://gateway.sandbox.push.apple.com:2195'
|
6
|
+
APPLE_DEVELOPMENT_FEEDBACK_URI = 'apn://feedback.sandbox.push.apple.com:2196'
|
7
7
|
|
8
8
|
class Client
|
9
|
-
attr_accessor :gateway_uri, :feedback_uri, :certificate, :passphrase
|
9
|
+
attr_accessor :gateway_uri, :feedback_uri, :certificate, :passphrase, :timeout
|
10
10
|
|
11
11
|
class << self
|
12
12
|
def development
|
@@ -27,36 +27,66 @@ module Houston
|
|
27
27
|
def initialize
|
28
28
|
@gateway_uri = ENV['APN_GATEWAY_URI']
|
29
29
|
@feedback_uri = ENV['APN_FEEDBACK_URI']
|
30
|
-
@certificate =
|
30
|
+
@certificate = certificate_data
|
31
31
|
@passphrase = ENV['APN_CERTIFICATE_PASSPHRASE']
|
32
|
+
@timeout = Float(ENV['APN_TIMEOUT'] || 0.5)
|
32
33
|
end
|
33
34
|
|
34
35
|
def push(*notifications)
|
35
36
|
return if notifications.empty?
|
36
37
|
|
38
|
+
notifications.flatten!
|
39
|
+
|
37
40
|
Connection.open(@gateway_uri, @certificate, @passphrase) do |connection|
|
38
|
-
|
41
|
+
ssl = connection.ssl
|
42
|
+
|
43
|
+
notifications.each_with_index do |notification, index|
|
39
44
|
next unless notification.kind_of?(Notification)
|
40
45
|
next if notification.sent?
|
46
|
+
next unless notification.valid?
|
47
|
+
|
48
|
+
notification.id = index
|
41
49
|
|
42
50
|
connection.write(notification.message)
|
43
51
|
notification.mark_as_sent!
|
52
|
+
|
53
|
+
read_socket, _write_socket = IO.select([ssl], [ssl], [ssl], nil)
|
54
|
+
if (read_socket && read_socket[0])
|
55
|
+
if error = connection.read(6)
|
56
|
+
_command, status, index = error.unpack('ccN')
|
57
|
+
notification.apns_error_code = status
|
58
|
+
notification.mark_as_unsent!
|
59
|
+
end
|
60
|
+
end
|
44
61
|
end
|
45
62
|
end
|
46
63
|
end
|
47
64
|
|
48
|
-
def
|
65
|
+
def unregistered_devices
|
49
66
|
devices = []
|
50
67
|
|
51
68
|
Connection.open(@feedback_uri, @certificate, @passphrase) do |connection|
|
52
69
|
while line = connection.read(38)
|
53
70
|
feedback = line.unpack('N1n1H140')
|
71
|
+
timestamp = feedback[0]
|
54
72
|
token = feedback[2].scan(/.{0,8}/).join(' ').strip
|
55
|
-
devices << token if token
|
73
|
+
devices << { token: token, timestamp: timestamp } if token && timestamp
|
56
74
|
end
|
57
75
|
end
|
58
76
|
|
59
77
|
devices
|
60
78
|
end
|
79
|
+
|
80
|
+
def devices
|
81
|
+
unregistered_devices.collect { |device| device[:token] }
|
82
|
+
end
|
83
|
+
|
84
|
+
def certificate_data
|
85
|
+
if ENV['APN_CERTIFICATE']
|
86
|
+
File.read(ENV['APN_CERTIFICATE'])
|
87
|
+
elsif ENV['APN_CERTIFICATE_DATA']
|
88
|
+
ENV['APN_CERTIFICATE_DATA']
|
89
|
+
end
|
90
|
+
end
|
61
91
|
end
|
62
92
|
end
|
data/lib/houston/connection.rb
CHANGED
@@ -26,8 +26,8 @@ module Houston
|
|
26
26
|
|
27
27
|
def initialize(uri, certificate, passphrase)
|
28
28
|
@uri = URI(uri)
|
29
|
-
@certificate = certificate
|
30
|
-
@passphrase = passphrase
|
29
|
+
@certificate = certificate.to_s
|
30
|
+
@passphrase = passphrase.to_s unless passphrase.nil?
|
31
31
|
end
|
32
32
|
|
33
33
|
def open
|
@@ -45,7 +45,7 @@ module Houston
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def open?
|
48
|
-
not (@ssl
|
48
|
+
not (@ssl && @socket).nil?
|
49
49
|
end
|
50
50
|
|
51
51
|
def close
|
data/lib/houston/notification.rb
CHANGED
@@ -2,8 +2,37 @@ require 'json'
|
|
2
2
|
|
3
3
|
module Houston
|
4
4
|
class Notification
|
5
|
-
|
5
|
+
class APNSError < RuntimeError
|
6
|
+
# See: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW12
|
7
|
+
CODES = {
|
8
|
+
0 => 'No errors encountered',
|
9
|
+
1 => 'Processing error',
|
10
|
+
2 => 'Missing device token',
|
11
|
+
3 => 'Missing topic',
|
12
|
+
4 => 'Missing payload',
|
13
|
+
5 => 'Invalid token size',
|
14
|
+
6 => 'Invalid topic size',
|
15
|
+
7 => 'Invalid payload size',
|
16
|
+
8 => 'Invalid token',
|
17
|
+
10 => 'Shutdown',
|
18
|
+
255 => 'Unknown error'
|
19
|
+
}
|
20
|
+
|
21
|
+
attr_reader :code
|
22
|
+
|
23
|
+
def initialize(code)
|
24
|
+
raise ArgumentError unless CODES.include?(code)
|
25
|
+
super(CODES[code])
|
26
|
+
@code = code
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
MAXIMUM_PAYLOAD_SIZE = 2048
|
31
|
+
|
32
|
+
attr_accessor :token, :alert, :badge, :sound, :category, :content_available, :mutable_content,
|
33
|
+
:custom_data, :id, :expiry, :priority, :url_args, :thread_id
|
6
34
|
attr_reader :sent_at
|
35
|
+
attr_writer :apns_error_code
|
7
36
|
|
8
37
|
alias :device :token
|
9
38
|
alias :device= :token=
|
@@ -13,35 +42,87 @@ module Houston
|
|
13
42
|
@alert = options.delete(:alert)
|
14
43
|
@badge = options.delete(:badge)
|
15
44
|
@sound = options.delete(:sound)
|
45
|
+
@category = options.delete(:category)
|
46
|
+
@expiry = options.delete(:expiry)
|
47
|
+
@id = options.delete(:id)
|
48
|
+
@priority = options.delete(:priority)
|
16
49
|
@content_available = options.delete(:content_available)
|
50
|
+
@mutable_content = options.delete(:mutable_content)
|
51
|
+
@url_args = options.delete(:url_args)
|
52
|
+
@thread_id = options.delete(:thread_id)
|
17
53
|
|
18
54
|
@custom_data = options
|
55
|
+
|
56
|
+
@sent_at = nil
|
57
|
+
@apns_error_code = nil
|
19
58
|
end
|
20
59
|
|
21
60
|
def payload
|
22
|
-
json = {}.merge(@custom_data || {})
|
61
|
+
json = {}.merge(@custom_data || {}).inject({}) { |h, (k, v)| h[k.to_s] = v; h }
|
62
|
+
|
23
63
|
json['aps'] ||= {}
|
24
64
|
json['aps']['alert'] = @alert if @alert
|
25
65
|
json['aps']['badge'] = @badge.to_i rescue 0 if @badge
|
26
66
|
json['aps']['sound'] = @sound if @sound
|
67
|
+
json['aps']['category'] = @category if @category
|
27
68
|
json['aps']['content-available'] = 1 if @content_available
|
69
|
+
json['aps']['mutable-content'] = 1 if @mutable_content
|
70
|
+
json['aps']['url-args'] = @url_args if @url_args
|
71
|
+
json['aps']['thread-id'] = @thread_id if @thread_id
|
28
72
|
|
29
73
|
json
|
30
74
|
end
|
31
75
|
|
32
76
|
def message
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
77
|
+
data = [device_token_item,
|
78
|
+
payload_item,
|
79
|
+
identifier_item,
|
80
|
+
expiration_item,
|
81
|
+
priority_item].compact.join
|
82
|
+
[2, data.bytes.count, data].pack('cNa*')
|
37
83
|
end
|
38
84
|
|
39
85
|
def mark_as_sent!
|
40
86
|
@sent_at = Time.now
|
41
87
|
end
|
42
88
|
|
89
|
+
def mark_as_unsent!
|
90
|
+
@sent_at = nil
|
91
|
+
end
|
92
|
+
|
43
93
|
def sent?
|
44
94
|
!!@sent_at
|
45
95
|
end
|
96
|
+
|
97
|
+
def valid?
|
98
|
+
payload.to_json.bytesize <= MAXIMUM_PAYLOAD_SIZE
|
99
|
+
end
|
100
|
+
|
101
|
+
def error
|
102
|
+
APNSError.new(@apns_error_code) if @apns_error_code && @apns_error_code.nonzero?
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def device_token_item
|
108
|
+
[1, 32, @token.gsub(/[<\s>]/, '')].pack('cnH64')
|
109
|
+
end
|
110
|
+
|
111
|
+
def payload_item
|
112
|
+
json = payload.to_json
|
113
|
+
[2, json.bytes.count, json].pack('cna*')
|
114
|
+
end
|
115
|
+
|
116
|
+
def identifier_item
|
117
|
+
[3, 4, @id].pack('cnN') unless @id.nil?
|
118
|
+
end
|
119
|
+
|
120
|
+
def expiration_item
|
121
|
+
[4, 4, @expiry.to_i].pack('cnN') unless @expiry.nil?
|
122
|
+
end
|
123
|
+
|
124
|
+
def priority_item
|
125
|
+
[5, 1, @priority].pack('cnc') unless @priority.nil?
|
126
|
+
end
|
46
127
|
end
|
47
128
|
end
|
data/lib/houston.rb
CHANGED
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Houston::Client do
|
4
|
+
subject { Houston::Client.development }
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
stub_const('Houston::Connection', MockConnection)
|
8
|
+
end
|
9
|
+
|
10
|
+
context '#development' do
|
11
|
+
subject { Houston::Client.development }
|
12
|
+
|
13
|
+
describe '#gateway_uri' do
|
14
|
+
subject { super().gateway_uri }
|
15
|
+
it { should == Houston::APPLE_DEVELOPMENT_GATEWAY_URI }
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#feedback_uri' do
|
19
|
+
subject { super().feedback_uri }
|
20
|
+
it { should == Houston::APPLE_DEVELOPMENT_FEEDBACK_URI }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context '#production' do
|
25
|
+
subject { Houston::Client.production }
|
26
|
+
|
27
|
+
describe '#gateway_uri' do
|
28
|
+
subject { super().gateway_uri }
|
29
|
+
it { should == Houston::APPLE_PRODUCTION_GATEWAY_URI }
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#feedback_uri' do
|
33
|
+
subject { super().feedback_uri }
|
34
|
+
it { should == Houston::APPLE_PRODUCTION_FEEDBACK_URI }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context '#new' do
|
39
|
+
context 'passing options through ENV' do
|
40
|
+
ENV['APN_GATEWAY_URI'] = 'apn://gateway.example.com'
|
41
|
+
ENV['APN_FEEDBACK_URI'] = 'apn://feedback.example.com'
|
42
|
+
ENV['APN_CERTIFICATE_PASSPHRASE'] = 'passphrase'
|
43
|
+
ENV['APN_TIMEOUT'] = '10.0'
|
44
|
+
|
45
|
+
subject do
|
46
|
+
Houston::Client.new
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#gateway_uri' do
|
50
|
+
subject { super().gateway_uri }
|
51
|
+
it { should == ENV['APN_GATEWAY_URI'] }
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#feedback_uri' do
|
55
|
+
subject { super().feedback_uri }
|
56
|
+
it { should == ENV['APN_FEEDBACK_URI'] }
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#certificate' do
|
60
|
+
subject { super().certificate }
|
61
|
+
it { should be_nil }
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#passphrase' do
|
65
|
+
subject { super().passphrase }
|
66
|
+
it { should == ENV['APN_CERTIFICATE_PASSPHRASE'] }
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '#timeout' do
|
70
|
+
subject { super().timeout }
|
71
|
+
it { should be_a(Float) }
|
72
|
+
it { should == Float(ENV['APN_TIMEOUT']) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#push' do
|
77
|
+
it 'should accept zero arguments' do
|
78
|
+
expect(Houston::Client.development.push()).to be_nil()
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#unregistered_devices' do
|
84
|
+
it 'should correctly parse the feedback response and create a dictionary of unregistered devices with timestamps' do
|
85
|
+
expect(subject.unregistered_devices).to eq [
|
86
|
+
{ token: 'ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5969', timestamp: 443779200 },
|
87
|
+
{ token: 'ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5970', timestamp: 1388678223 }
|
88
|
+
]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#devices' do
|
93
|
+
it 'should correctly parse the feedback response and create an array of unregistered devices' do
|
94
|
+
expect(subject.devices).to eq [
|
95
|
+
'ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5969',
|
96
|
+
'ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5970'
|
97
|
+
]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|