ruby-push-notifications 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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