ruby-push-notifications 1.1.0 → 1.2.0

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.
@@ -0,0 +1,208 @@
1
+ module RubyPushNotifications
2
+ module WNS
3
+ # Class that encapsulates the result of a single sent notification to a single
4
+ # Device URL
5
+ # https://msdn.microsoft.com/en-us/library/windows/apps/hh465435.aspx#WNSResponseCodes
6
+ class WNSResult
7
+ # @return [String]. Receiver WNS device URL.
8
+ attr_accessor :device_url
9
+
10
+ # @private X-WNS-NotificationStatus HTTP Header string
11
+ X_NOTIFICATION_STATUS = 'x-wns-notificationstatus'
12
+
13
+ # @private X-WNS-DeviceConnectionStatus HTTP Header string
14
+ X_DEVICE_CONNECTION_STATUS = 'x-wns-deviceconnectionstatus'
15
+
16
+ # @private X-SubscriptionStatus HTTP Header string
17
+ X_STATUS = 'x-wns-status'
18
+ end
19
+
20
+ # Indicates that the notification was successfully sent to the corresponding
21
+ # device URL
22
+ class WNSResultOK < WNSResult
23
+ # @return [String]. The status of the notification received
24
+ # by the Windows Notification Service.
25
+ attr_accessor :notification_status
26
+
27
+ # @return [String]. The connection status of the device.
28
+ attr_accessor :device_connection_status
29
+
30
+ # @return [String]. Notification status.
31
+ attr_accessor :status
32
+
33
+ def initialize(device_url, headers)
34
+ @device_url = device_url
35
+ @notification_status = headers[X_NOTIFICATION_STATUS]
36
+ @device_connection_status = headers[X_DEVICE_CONNECTION_STATUS]
37
+ @status = headers[X_STATUS]
38
+ end
39
+
40
+ def ==(other)
41
+ (other.is_a?(WNSResultOK) &&
42
+ device_url == other.device_url &&
43
+ notification_status == other.notification_status &&
44
+ device_connection_status == other.device_connection_status &&
45
+ status == other.status) || super(other)
46
+ end
47
+ end
48
+
49
+ # This error occurs when the cloud service sends a notification
50
+ # request with a bad XML document or malformed notification URI.
51
+ class MalformedWNSResultError < WNSResult
52
+ def initialize(device_url)
53
+ @device_url = device_url
54
+ end
55
+
56
+ def ==(other)
57
+ (other.is_a?(MalformedWNSResultError) &&
58
+ device_url == other.device_url) || super(other)
59
+ end
60
+ end
61
+
62
+ # Sending this notification is unauthorized.
63
+ class WNSAuthError < WNSResult
64
+ def initialize(device_url)
65
+ @device_url = device_url
66
+ end
67
+
68
+ def ==(other)
69
+ (other.is_a?(WNSAuthError) &&
70
+ device_url == other.device_url) || super(other)
71
+ end
72
+ end
73
+
74
+ # Notification is invalid and is not present on the Push Notification Service.
75
+ class WNSInvalidError < WNSResult
76
+ # @return [String]. The status of the notification received
77
+ # by the Windows Notification Service.
78
+ attr_accessor :notification_status
79
+
80
+ # @return [String]. The connection status of the device.
81
+ attr_accessor :device_connection_status
82
+
83
+ # @return [String]. Notification status.
84
+ attr_accessor :status
85
+
86
+ def initialize(device_url, headers)
87
+ @device_url = device_url
88
+ @notification_status = headers[X_NOTIFICATION_STATUS]
89
+ @device_connection_status = headers[X_DEVICE_CONNECTION_STATUS]
90
+ @status = headers[X_STATUS]
91
+ end
92
+
93
+ def ==(other)
94
+ (other.is_a?(WNSInvalidError) &&
95
+ device_url == other.device_url &&
96
+ notification_status == other.notification_status &&
97
+ device_connection_status == other.device_connection_status &&
98
+ status == other.status) || super(other)
99
+ end
100
+ end
101
+
102
+ # This error occurs when an unauthenticated cloud service has reached
103
+ # the per-day throttling limit for a subscription,
104
+ # or when a cloud service (authenticated or unauthenticated)
105
+ # has sent too many notifications per second.
106
+ class WNSLimitError < WNSResult
107
+ # @return [String]. The status of the notification received
108
+ # by the Windows Notification Service.
109
+ attr_accessor :notification_status
110
+
111
+ # @return [String]. The connection status of the device.
112
+ attr_accessor :device_connection_status
113
+
114
+ # @return [String]. Notification status.
115
+ attr_accessor :status
116
+
117
+ def initialize(device_url, headers)
118
+ @device_url = device_url
119
+ @notification_status = headers[X_NOTIFICATION_STATUS]
120
+ @device_connection_status = headers[X_DEVICE_CONNECTION_STATUS]
121
+ @status = headers[X_STATUS]
122
+ end
123
+
124
+ def ==(other)
125
+ (other.is_a?(WNSLimitError) &&
126
+ device_url == other.device_url &&
127
+ notification_status == other.notification_status &&
128
+ device_connection_status == other.device_connection_status &&
129
+ status == other.status) || super(other)
130
+ end
131
+ end
132
+
133
+ # The device is in a disconnected state.
134
+ class WNSPreConditionError < WNSResult
135
+ # @return [String]. The status of the notification received
136
+ # by the Windows Notification Service.
137
+ attr_accessor :notification_status
138
+
139
+ # @return [String]. The connection status of the device.
140
+ attr_accessor :device_connection_status
141
+
142
+ def initialize(device_url, headers)
143
+ @device_url = device_url
144
+ @notification_status = headers[X_NOTIFICATION_STATUS]
145
+ @device_connection_status = headers[X_DEVICE_CONNECTION_STATUS]
146
+ end
147
+
148
+ def ==(other)
149
+ (other.is_a?(WNSPreConditionError) &&
150
+ device_url == other.device_url &&
151
+ notification_status == other.notification_status &&
152
+ device_connection_status == other.device_connection_status) || super(other)
153
+ end
154
+ end
155
+
156
+ # The device is in a disconnected state.
157
+ class WNSExpiredError < WNSResult
158
+ # @return [String]. The status of the notification received
159
+ # by the Windows Notification Service.
160
+ attr_accessor :notification_status
161
+
162
+ # @return [String]. The connection status of the device.
163
+ attr_accessor :device_connection_status
164
+
165
+ # @return [String]. Notification status.
166
+ attr_accessor :status
167
+
168
+ def initialize(device_url, headers)
169
+ @device_url = device_url
170
+ @notification_status = headers[X_NOTIFICATION_STATUS]
171
+ @device_connection_status = headers[X_DEVICE_CONNECTION_STATUS]
172
+ @status = 'Expired'.freeze
173
+ end
174
+
175
+ def ==(other)
176
+ (other.is_a?(WNSExpiredError) &&
177
+ device_url == other.device_url &&
178
+ notification_status == other.notification_status &&
179
+ device_connection_status == other.device_connection_status &&
180
+ status == other.status) || super(other)
181
+ end
182
+ end
183
+
184
+ # The Push Notification Service is unable to process the request.
185
+ class WNSInternalError < WNSResult
186
+ def initialize(device_url)
187
+ @device_url = device_url
188
+ end
189
+
190
+ def ==(other)
191
+ (other.is_a?(WNSInternalError) &&
192
+ device_url == other.device_url) || super(other)
193
+ end
194
+ end
195
+
196
+ # Unknow Error
197
+ class WNSResultError < WNSResult
198
+ def initialize(device_url)
199
+ @device_url = device_url
200
+ end
201
+
202
+ def ==(other)
203
+ (other.is_a?(WNSResultError) &&
204
+ device_url == other.device_url) || super(other)
205
+ end
206
+ end
207
+ end
208
+ end
@@ -1,7 +1,7 @@
1
1
  # coding: utf-8
2
2
  Gem::Specification.new do |spec|
3
3
  spec.name = "ruby-push-notifications"
4
- spec.version = "1.1.0"
4
+ spec.version = "1.2.0"
5
5
  spec.authors = ['Carlos Alonso']
6
6
  spec.email = ['info@mrcalonso.com']
7
7
  spec.summary = %q{iOS, Android and Windows Phone Push Notifications made easy!}
@@ -1,4 +1,3 @@
1
-
2
1
  FactoryGirl.define do
3
2
  sequence :apns_token do |i|
4
3
  "ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b596#{i}"
@@ -12,4 +11,7 @@ FactoryGirl.define do
12
11
  "http://s.notify.live.net/u/1/bn1/HmQAAACP-0esPuxBSkzBNNXH4W0lV3lK-stEw6eRfpXX39uYbM7IwehXOTO9pRBjaaGECWOdD_7x5j5U4w4iXG4hGxer/d2luZG93c3Bob25lZGVmYXVsdA/EMDhx32Q5BG0DWnZpuVX1g/kRFAu0-jnhMQ-HG94rXzrbb0wQk#{i}"
13
12
  end
14
13
 
14
+ sequence :wns_device_url do |i|
15
+ "https://hk2.notify.windows.com/?token=AwYAAABe%2bpjShu%2fzcVaaf1NPm%2b2dpiosm7RnmBJGMVkBDiYNXpAEp0mETldEu8axFoamgwb%2fdKCuNSfGGDHZ3RcaO2fcNGfxC6Y4Yp3xTFkDOhv5kNfgSXef7pSP0uwueIpqbWI%3d#{i}"
16
+ end
15
17
  end
@@ -20,4 +20,11 @@ FactoryGirl.define do
20
20
  initialize_with { new device_urls, data }
21
21
  end
22
22
 
23
+ factory :wns_notification, class: 'RubyPushNotifications::WNS::WNSNotification' do
24
+ device_urls { [generate(:wns_device_url)] }
25
+ data message: { value1: 'hello' }
26
+
27
+ initialize_with { new device_urls, data }
28
+ end
29
+
23
30
  end
@@ -0,0 +1,76 @@
1
+ module RubyPushNotifications
2
+ module WNS
3
+ describe WNSAccess do
4
+ describe '::access_token' do
5
+ let(:sid) { 'sid' }
6
+ let(:secret) { 'secret' }
7
+
8
+ context 'when service return status OK' do
9
+ let(:body) {
10
+ "{\"token_type\":\"bearer\",\"access_token\":\"real_access_token\",\"expires_in\":86400}"
11
+ }
12
+ before do
13
+ stub_request(:post, "https://login.live.com/accesstoken.srf").
14
+ to_return status: [200, 'OK'], body: body
15
+ end
16
+
17
+ it 'returns correct response' do
18
+ response = OpenStruct.new(
19
+ status_code: 200,
20
+ status: 'OK',
21
+ error: nil,
22
+ error_description: nil,
23
+ access_token: 'real_access_token',
24
+ token_ttl: 86400
25
+ )
26
+ expect(WNSAccess.new(sid, secret).get_token.response).to eq(response)
27
+ end
28
+ end
29
+
30
+ context 'when service return an error' do
31
+ context 'when bad credentials' do
32
+ let(:body) {
33
+ "{\"error\":\"invalid_client\",\"error_description\":\"Invalid client id\"}"
34
+ }
35
+ before do
36
+ stub_request(:post, "https://login.live.com/accesstoken.srf").
37
+ to_return status: [400, 'Bad Request'], body: body
38
+ end
39
+
40
+ it 'returns correct response' do
41
+ response = OpenStruct.new(
42
+ status_code: 400,
43
+ status: 'Bad Request',
44
+ error: 'invalid_client',
45
+ error_description: 'Invalid client id',
46
+ access_token: nil,
47
+ token_ttl: nil
48
+ )
49
+ expect(WNSAccess.new(sid, secret).get_token.response).to eq(response)
50
+ end
51
+ end
52
+
53
+ context 'when bad request' do
54
+ let(:body) { '' }
55
+ before do
56
+ stub_request(:post, "https://login.live.com/accesstoken.srf").
57
+ to_return status: [500, 'Internal Server Error'], body: body
58
+ end
59
+
60
+ it 'returns correct response' do
61
+ response = OpenStruct.new(
62
+ status_code: 500,
63
+ status: 'Internal Server Error',
64
+ error: nil,
65
+ error_description: nil,
66
+ access_token: nil,
67
+ token_ttl: nil
68
+ )
69
+ expect(WNSAccess.new(sid, secret).get_token.response).to eq(response)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,53 @@
1
+ module RubyPushNotifications
2
+ module WNS
3
+ describe WNSConnection do
4
+
5
+ describe '::post' do
6
+ let(:body) { 'abc' }
7
+ let(:headers) {
8
+ {
9
+ 'x-wns-notificationstatus' => 'Received',
10
+ 'x-wns-deviceconnectionstatus' => 'Connected',
11
+ 'x-wns-status' => 'Received'
12
+ }
13
+ }
14
+ let(:push_types) { RubyPushNotifications::WNS::WNSConnection::WP_TARGETS }
15
+ let(:access_token) { 'token' }
16
+ let(:device_urls) { [generate(:wns_device_url)] }
17
+ let(:toast_data) { { title: 'Title', message: 'Hello WNS World!', type: :toast } }
18
+ let(:toast_notification) { build :wns_notification, device_urls: device_urls, data: toast_data }
19
+ let(:wns_connection) { RubyPushNotifications::WNS::WNSConnection }
20
+
21
+ before do
22
+ stub_request(:post, %r{hk2.notify.windows.com}).
23
+ to_return status: [200, 'OK'], body: body, headers: headers
24
+ end
25
+
26
+ it 'runs the right request' do
27
+ WNSConnection.post toast_notification, access_token
28
+ headers = {
29
+ wns_connection::CONTENT_TYPE_HEADER => wns_connection::CONTENT_TYPE[:toast],
30
+ wns_connection::X_WNS_TYPE_HEADER => wns_connection::WP_TARGETS[:toast],
31
+ wns_connection::CONTENT_LENGTH_HEADER => toast_notification.as_wns_xml.length.to_s,
32
+ wns_connection::AUTHORIZATION_HEADER => "Bearer #{access_token}",
33
+ wns_connection::REQUEST_FOR_STATUS_HEADER => 'true'
34
+ }
35
+ expect(WebMock).
36
+ to have_requested(:post, toast_notification.device_urls[0]).
37
+ with(body: toast_notification.as_wns_xml, headers: headers).
38
+ once
39
+ end
40
+
41
+ it 'returns the response encapsulated in a Hash object' do
42
+ responses = [
43
+ { device_url: toast_notification.device_urls[0],
44
+ headers: headers,
45
+ code: 200
46
+ }
47
+ ]
48
+ expect(WNSConnection.post toast_notification, access_token).to eq WNSResponse.new(responses)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,177 @@
1
+ module RubyPushNotifications
2
+ module WNS
3
+ describe WNSNotification do
4
+ let(:device_urls) { %w(a b c) }
5
+ let(:raw_data) { { message: { title: 'Title', message: 'Hello WNS World!'} } }
6
+ let(:raw_notification) { build :wns_notification, data: raw_data }
7
+ let(:toast_data) { { title: 'Title', message: 'Hello WNS World!', type: :toast } }
8
+ let(:toast_notification) { build :wns_notification, device_urls: device_urls, data: toast_data }
9
+ let(:tile_data) { { message: 'Hello WNS World!', image: 'http://image.com/image.jpg', type: :tile } }
10
+ let(:tile_notification) { build :wns_notification, device_urls: device_urls, data: tile_data }
11
+ let(:badge_data) { { value: 10, type: :badge } }
12
+ let(:badge_notification) { build :wns_notification, device_urls: device_urls, data: badge_data }
13
+
14
+ describe 'raw xml' do
15
+ it 'builds the right wns raw xml' do
16
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
17
+ xml << '<root><title>Title</title><message>Hello WNS World!</message></root>'
18
+ expect(raw_notification.as_wns_xml).to eq xml
19
+ end
20
+
21
+ context 'without message' do
22
+ let(:raw_data) { {} }
23
+
24
+ it 'builds the right wns raw xml' do
25
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
26
+ xml << '<root></root>'
27
+ expect(raw_notification.as_wns_xml).to eq xml
28
+ end
29
+ end
30
+ end
31
+
32
+ describe 'badge xml' do
33
+ it 'builds the right wns badge xml' do
34
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
35
+ xml << '<badge value="10"/>'
36
+ expect(badge_notification.as_wns_xml).to eq xml
37
+ end
38
+
39
+ context 'without value' do
40
+ let(:badge_data) { { type: :badge } }
41
+
42
+ it 'builds the right wns badge xml' do
43
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
44
+ xml << '<badge value=""/>'
45
+ expect(badge_notification.as_wns_xml).to eq xml
46
+ end
47
+ end
48
+ end
49
+
50
+ describe 'toast xml' do
51
+ it 'builds the right wns toast xml' do
52
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
53
+ xml << '<toast>'
54
+ xml << '<visual>'
55
+ xml << '<binding template="ToastText02">'
56
+ xml << '<text id="1">Title</text>'
57
+ xml << '<text id="2">Hello WNS World!</text>'
58
+ xml << '</binding>'
59
+ xml << '</visual>'
60
+ xml << '</toast>'
61
+ expect(toast_notification.as_wns_xml).to eq xml
62
+ end
63
+
64
+ context 'when params present' do
65
+ let(:toast_data) { super().merge(param: { var1: 'value1', var2: 'value2'}) }
66
+
67
+ it 'builds the right wns toast xml' do
68
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
69
+ xml << '<toast launch="{&quot;var1&quot;:&quot;value1&quot;,&quot;var2&quot;:&quot;value2&quot;}">'
70
+ xml << '<visual>'
71
+ xml << '<binding template="ToastText02">'
72
+ xml << '<text id="1">Title</text>'
73
+ xml << '<text id="2">Hello WNS World!</text>'
74
+ xml << '</binding>'
75
+ xml << '</visual>'
76
+ xml << '</toast>'
77
+ expect(toast_notification.as_wns_xml).to eq xml
78
+ end
79
+ end
80
+
81
+ context 'when template present' do
82
+ let(:toast_data) { super().merge(template: 'custom_template') }
83
+
84
+ it 'builds the right wns toast xml' do
85
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
86
+ xml << '<toast>'
87
+ xml << '<visual>'
88
+ xml << '<binding template="custom_template">'
89
+ xml << '<text id="1">Title</text>'
90
+ xml << '<text id="2">Hello WNS World!</text>'
91
+ xml << '</binding>'
92
+ xml << '</visual>'
93
+ xml << '</toast>'
94
+ expect(toast_notification.as_wns_xml).to eq xml
95
+ end
96
+ end
97
+
98
+ context 'without title and message' do
99
+ let(:toast_data) { { type: :toast } }
100
+
101
+ it 'builds the right wns toast xml' do
102
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
103
+ xml << '<toast>'
104
+ xml << '<visual>'
105
+ xml << '<binding template="ToastText02">'
106
+ xml << '<text id="1"></text>'
107
+ xml << '<text id="2"></text>'
108
+ xml << '</binding>'
109
+ xml << '</visual>'
110
+ xml << '</toast>'
111
+ expect(toast_notification.as_wns_xml).to eq xml
112
+ end
113
+ end
114
+ end
115
+
116
+ describe 'toast xml' do
117
+ it 'builds the right wns tile xml' do
118
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
119
+ xml << '<tile>'
120
+ xml << '<visual>'
121
+ xml << '<binding template="TileWideImageAndText01">'
122
+ xml << '<image src="http://image.com/image.jpg"/>'
123
+ xml << '<text>Hello WNS World!</text>'
124
+ xml << '</binding>'
125
+ xml << '</visual>'
126
+ xml << '</tile>'
127
+ expect(tile_notification.as_wns_xml).to eq xml
128
+ end
129
+
130
+ context 'when template present' do
131
+ let(:tile_data) { super().merge(template: 'custom_template') }
132
+
133
+ it 'builds the right wns toast xml' do
134
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
135
+ xml << '<tile>'
136
+ xml << '<visual>'
137
+ xml << '<binding template="custom_template">'
138
+ xml << '<image src="http://image.com/image.jpg"/>'
139
+ xml << '<text>Hello WNS World!</text>'
140
+ xml << '</binding>'
141
+ xml << '</visual>'
142
+ xml << '</tile>'
143
+ expect(tile_notification.as_wns_xml).to eq xml
144
+ end
145
+ end
146
+
147
+ context 'without image and message' do
148
+ let(:tile_data) { { type: :tile } }
149
+
150
+ it 'builds the right wns toast xml' do
151
+ xml = '<?xml version="1.0" encoding="UTF-8"?>'
152
+ xml << '<tile>'
153
+ xml << '<visual>'
154
+ xml << '<binding template="TileWideImageAndText01">'
155
+ xml << '<image src=""/>'
156
+ xml << '<text></text>'
157
+ xml << '</binding>'
158
+ xml << '</visual>'
159
+ xml << '</tile>'
160
+ expect(tile_notification.as_wns_xml).to eq xml
161
+ end
162
+ end
163
+ end
164
+
165
+ it_behaves_like 'a proper results manager' do
166
+ let(:notification) { build :wns_notification }
167
+ let(:success_count) { 1 }
168
+ let(:failures_count) { 0 }
169
+ let(:device_url) { generate :wns_device_url }
170
+ let(:individual_results) { [WNSResultOK.new(device_url, {})] }
171
+ let(:results) do
172
+ WNSResponse.new [{ code: 200, device_url: device_url, headers: {} }]
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end