expo-push 1.0.2

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,550 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'expo-push'
5
+ require 'expo-push/too_many_messages_error'
6
+
7
+ class ExponentServerSdkTest < Minitest::Test
8
+ def setup
9
+ @mock = MiniTest::Mock.new
10
+ @response_mock = MiniTest::Mock.new
11
+ @client = Exponent::Push::Client.new(http_client: @mock)
12
+ @client_gzip = Exponent::Push::Client.new(http_client: @mock, gzip: true)
13
+ end
14
+
15
+ def test_send_messages_with_success
16
+ @response_mock.expect(:code, 200)
17
+ @response_mock.expect(:body, success_body.to_json)
18
+
19
+ @mock.expect(:post, @response_mock, client_args)
20
+
21
+ response = @client.send_messages(messages)
22
+ assert_equal(response.errors?, false)
23
+
24
+ @mock.verify
25
+ end
26
+
27
+ def test_send_messages_alternate_message_format_with_success
28
+ @response_mock.expect(:code, 200)
29
+ @response_mock.expect(:body, success_body.to_json)
30
+
31
+ alternate_messages = alternate_format_messages
32
+ @mock.expect(:post, @response_mock, alternative_client_args(alternate_messages))
33
+
34
+ response = @client.send_messages(alternate_messages)
35
+ assert_equal(response.errors?, false)
36
+
37
+ @mock.verify
38
+ end
39
+
40
+ def test_send_messages_with_gzip_success
41
+ @response_mock.expect(:code, 200)
42
+ @response_mock.expect(:body, success_body.to_json)
43
+
44
+ @mock.expect(:post, @response_mock, gzip_client_args)
45
+
46
+ response = @client_gzip.send_messages(messages)
47
+ assert_equal(response.errors?, false)
48
+
49
+ @mock.verify
50
+ end
51
+
52
+ def test_send_messages_with_empty_string_response_body
53
+ @response_mock.expect(:code, 400)
54
+ @response_mock.expect(:body, '')
55
+
56
+ @mock.expect(:post, @response_mock, client_args)
57
+
58
+ exception = assert_raises Exponent::Push::UnknownError do
59
+ handler = @client.send_messages(messages)
60
+ # this first assertion is just stating that errors will be false when
61
+ # an exception is thrown on the request, not the content of the request
62
+ # 400/500 level errors are not delivery errors, they are functionality errors
63
+ assert_equal(handler.response.errors?, false)
64
+ assert_equal(handler.response.body, {})
65
+ assert_equal(handler.response.code, 400)
66
+ end
67
+
68
+ assert_match(/Unknown error format/, exception.message)
69
+
70
+ @mock.verify
71
+ end
72
+
73
+ def test_send_messages_with_nil_response_body
74
+ @response_mock.expect(:code, 400)
75
+ @response_mock.expect(:body, nil)
76
+
77
+ @mock.expect(:post, @response_mock, client_args)
78
+
79
+ exception = assert_raises Exponent::Push::UnknownError do
80
+ handler = @client.send_messages(messages)
81
+ # this first assertion is just stating that errors will be false when
82
+ # an exception is thrown on the request, not the content of the request
83
+ # 400/500 level errors are not delivery errors, they are functionality errors
84
+ assert_equal(handler.response.errors?, false)
85
+ assert_equal(handler.response.body, {})
86
+ assert_equal(handler.response.code, 400)
87
+ end
88
+
89
+ assert_match(/Unknown error format/, exception.message)
90
+
91
+ @mock.verify
92
+ end
93
+
94
+ def test_send_messages_with_gzip_empty_string_response
95
+ @response_mock.expect(:code, 400)
96
+ @response_mock.expect(:body, '')
97
+
98
+ @mock.expect(:post, @response_mock, gzip_client_args)
99
+
100
+ exception = assert_raises Exponent::Push::UnknownError do
101
+ handler = @client_gzip.send_messages(messages)
102
+ # this first assertion is just stating that errors will be false when
103
+ # an exception is thrown on the request, not the content of the request
104
+ # 400/500 level errors are not delivery errors, they are functionality errors
105
+ assert_equal(handler.response.errors?, false)
106
+ assert_equal(handler.response.body, {})
107
+ assert_equal(handler.response.code, 400)
108
+ end
109
+
110
+ assert_match(/Unknown error format/, exception.message)
111
+
112
+ @mock.verify
113
+ end
114
+
115
+ def test_send_messages_with_gzip_nil_response_body
116
+ @response_mock.expect(:code, 400)
117
+ @response_mock.expect(:body, nil)
118
+
119
+ @mock.expect(:post, @response_mock, gzip_client_args)
120
+
121
+ exception = assert_raises Exponent::Push::UnknownError do
122
+ handler = @client_gzip.send_messages(messages)
123
+ # this first assertion is just stating that errors will be false when
124
+ # an exception is thrown on the request, not the content of the request
125
+ # 400/500 level errors are not delivery errors, they are functionality errors
126
+ assert_equal(handler.response.errors?, false)
127
+ assert_equal(handler.response.body, {})
128
+ assert_equal(handler.response.code, 400)
129
+ end
130
+
131
+ assert_match(/Unknown error format/, exception.message)
132
+
133
+ @mock.verify
134
+ end
135
+
136
+ def test_send_messages_with_unknown_error
137
+ @response_mock.expect(:code, 400)
138
+ @response_mock.expect(:body, error_body.to_json)
139
+
140
+ @mock.expect(:post, @response_mock, client_args)
141
+
142
+ exception = assert_raises Exponent::Push::UnknownError do
143
+ @client.send_messages(messages)
144
+ end
145
+
146
+ assert_equal("Unknown error format: #{error_body.to_json}", exception.message)
147
+
148
+ @mock.verify
149
+ end
150
+
151
+ def test_send_messages_with_gzip_unknown_error
152
+ @response_mock.expect(:code, 400)
153
+ @response_mock.expect(:body, error_body.to_json)
154
+
155
+ @mock.expect(:post, @response_mock, gzip_client_args)
156
+
157
+ exception = assert_raises Exponent::Push::UnknownError do
158
+ @client_gzip.send_messages(messages)
159
+ end
160
+
161
+ assert_match(/Unknown error format/, exception.message)
162
+
163
+ @mock.verify
164
+ end
165
+
166
+ def test_send_messages_with_device_not_registered_error
167
+ @response_mock.expect(:code, 200)
168
+ @response_mock.expect(:body, not_registered_device_error_body.to_json)
169
+ token = 'ExponentPushToken[42]'
170
+ message = "\"#{token}\" is not a registered push notification recipient"
171
+
172
+ @mock.expect(:post, @response_mock, client_args)
173
+
174
+ response_handler = @client.send_messages(messages)
175
+ assert_equal(message, response_handler.errors.first.message)
176
+ assert(response_handler.errors.first.instance_of?(Exponent::Push::DeviceNotRegisteredError))
177
+ assert(response_handler.invalid_push_tokens.include?(token))
178
+ assert(response_handler.errors?)
179
+
180
+ @mock.verify
181
+ end
182
+
183
+ def test_send_messages_too_many_messages
184
+ message = 'Only 100 message objects at a time allowed.'
185
+
186
+ e = assert_raises TooManyMessagesError do
187
+ @client.send_messages(too_many_messages)
188
+ end
189
+
190
+ assert_equal(e.message, message)
191
+ end
192
+
193
+ def test_send_messages_with_message_too_big_error
194
+ @response_mock.expect(:code, 200)
195
+ @response_mock.expect(:body, message_too_big_error_body.to_json)
196
+ message = 'Message too big'
197
+
198
+ @mock.expect(:post, @response_mock, client_args)
199
+
200
+ response_handler = @client.send_messages(messages)
201
+ assert(response_handler.errors.first.instance_of?(Exponent::Push::MessageTooBigError))
202
+ assert_equal(message, response_handler.errors.first.message)
203
+ assert(response_handler.errors?)
204
+
205
+ @mock.verify
206
+ end
207
+
208
+ def test_send_messages_with_message_rate_exceeded_error
209
+ @response_mock.expect(:code, 200)
210
+ @response_mock.expect(:body, message_rate_exceeded_error_body.to_json)
211
+ message = 'Message rate exceeded'
212
+
213
+ @mock.expect(:post, @response_mock, client_args)
214
+
215
+ response_handler = @client.send_messages(messages)
216
+ assert(response_handler.errors.first.instance_of?(Exponent::Push::MessageRateExceededError))
217
+ assert_equal(message, response_handler.errors.first.message)
218
+
219
+ @mock.verify
220
+ end
221
+
222
+ def test_send_messages_with_invalid_credentials_error
223
+ @response_mock.expect(:code, 200)
224
+ @response_mock.expect(:body, invalid_credentials_error_body.to_json)
225
+ message = 'Invalid credentials'
226
+
227
+ @mock.expect(:post, @response_mock, client_args)
228
+
229
+ response_handler = @client.send_messages(messages)
230
+ assert(response_handler.errors.first.instance_of?(Exponent::Push::InvalidCredentialsError))
231
+ assert_equal(message, response_handler.errors.first.message)
232
+
233
+ @mock.verify
234
+ end
235
+
236
+ def test_send_messages_with_apn_error
237
+ @response_mock.expect(:code, 200)
238
+ @response_mock.expect(:body, apn_error_body.to_json)
239
+
240
+ @mock.expect(:post, @response_mock, client_args)
241
+
242
+ response_handler = @client.send_messages(messages)
243
+ assert(response_handler.errors.first.instance_of?(Exponent::Push::UnknownError))
244
+ assert_match(/Unknown error format/, response_handler.errors.first.message)
245
+
246
+ @mock.verify
247
+ end
248
+
249
+ def test_get_receipts_with_success_receipt
250
+ @response_mock.expect(:code, 200)
251
+ @response_mock.expect(:body, receipt_success_body.to_json)
252
+ receipt_ids = [success_receipt]
253
+
254
+ @mock.expect(:post, @response_mock, receipt_client_args(receipt_ids))
255
+
256
+ response_handler = @client.verify_deliveries(receipt_ids)
257
+ assert_match(success_receipt, response_handler.receipt_ids.first)
258
+
259
+ @mock.verify
260
+ end
261
+
262
+ def test_get_receipts_with_error_receipt
263
+ @response_mock.expect(:code, 200)
264
+ @response_mock.expect(:body, receipt_error_body.to_json)
265
+ receipt_ids = [error_receipt]
266
+
267
+ @mock.expect(:post, @response_mock, receipt_client_args(receipt_ids))
268
+
269
+ response_handler = @client.verify_deliveries(receipt_ids)
270
+ assert_match(error_receipt, response_handler.receipt_ids.first)
271
+ assert_equal(true, response_handler.errors?)
272
+ assert_equal(1, response_handler.errors.count)
273
+ assert(response_handler.errors.first.instance_of?(Exponent::Push::DeviceNotRegisteredError))
274
+
275
+ @mock.verify
276
+ end
277
+
278
+ def test_get_receipts_with_variable_success_receipts
279
+ @response_mock.expect(:code, 200)
280
+ @response_mock.expect(:body, multiple_receipts.to_json)
281
+ receipt_ids = [error_receipt, success_receipt]
282
+
283
+ @mock.expect(:post, @response_mock, receipt_client_args(receipt_ids))
284
+
285
+ response_handler = @client.verify_deliveries(receipt_ids)
286
+ assert_match(error_receipt, response_handler.receipt_ids.first)
287
+ assert_match(success_receipt, response_handler.receipt_ids.last)
288
+ assert_equal(true, response_handler.errors?)
289
+ assert_equal(1, response_handler.errors.count)
290
+ assert(response_handler.errors.first.instance_of?(Exponent::Push::DeviceNotRegisteredError))
291
+
292
+ @mock.verify
293
+ end
294
+
295
+ def test_get_receipts_with_gzip_success_receipt
296
+ @response_mock.expect(:code, 200)
297
+ @response_mock.expect(:body, receipt_success_body.to_json)
298
+ receipt_ids = [success_receipt]
299
+
300
+ @mock.expect(:post, @response_mock, gzip_receipt_client_args(receipt_ids))
301
+
302
+ response_handler = @client_gzip.verify_deliveries(receipt_ids)
303
+ assert_match(success_receipt, response_handler.receipt_ids.first)
304
+
305
+ @mock.verify
306
+ end
307
+
308
+ def test_get_receipts_with_gzip_error_receipt
309
+ @response_mock.expect(:code, 200)
310
+ @response_mock.expect(:body, receipt_error_body.to_json)
311
+ receipt_ids = [error_receipt]
312
+
313
+ @mock.expect(:post, @response_mock, gzip_receipt_client_args(receipt_ids))
314
+
315
+ response_handler = @client_gzip.verify_deliveries(receipt_ids)
316
+ assert_match(error_receipt, response_handler.receipt_ids.first)
317
+ assert_equal(true, response_handler.errors?)
318
+ assert_equal(1, response_handler.errors.count)
319
+ assert(response_handler.errors.first.instance_of?(Exponent::Push::DeviceNotRegisteredError))
320
+
321
+ @mock.verify
322
+ end
323
+
324
+ def test_get_receipts_with_gzip_variable_success_receipts
325
+ @response_mock.expect(:code, 200)
326
+ @response_mock.expect(:body, multiple_receipts.to_json)
327
+ receipt_ids = [error_receipt, success_receipt]
328
+
329
+ @mock.expect(:post, @response_mock, gzip_receipt_client_args(receipt_ids))
330
+
331
+ response_handler = @client_gzip.verify_deliveries(receipt_ids)
332
+ assert_match(error_receipt, response_handler.receipt_ids.first)
333
+ assert_match(success_receipt, response_handler.receipt_ids.last)
334
+ assert_equal(true, response_handler.errors?)
335
+ assert_equal(1, response_handler.errors.count)
336
+ assert(response_handler.errors.first.instance_of?(Exponent::Push::DeviceNotRegisteredError))
337
+
338
+ @mock.verify
339
+ end
340
+
341
+ private
342
+
343
+ def success_body
344
+ { 'data' => [{ 'status' => 'ok' }] }
345
+ end
346
+
347
+ def success_receipt
348
+ 'YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY'
349
+ end
350
+
351
+ def error_receipt
352
+ 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
353
+ end
354
+
355
+ def receipt_success_body
356
+ {
357
+ 'data' => {
358
+ 'YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY' => {
359
+ 'status' => 'ok'
360
+ }
361
+ }
362
+ }
363
+ end
364
+
365
+ def receipt_error_body
366
+ {
367
+ 'data' => {
368
+ 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' => {
369
+ 'status' => 'error',
370
+ 'message' => 'The Apple Push Notification service failed to send the notification',
371
+ 'details' => {
372
+ 'error' => 'DeviceNotRegistered'
373
+ }
374
+ }
375
+ }
376
+ }
377
+ end
378
+
379
+ def multiple_receipts
380
+ {
381
+ 'data' => {
382
+ 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' => {
383
+ 'status' => 'error',
384
+ 'message' => 'The Apple Push Notification service failed to send the notification',
385
+ 'details' => {
386
+ 'error' => 'DeviceNotRegistered'
387
+ }
388
+ },
389
+ 'YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY' => {
390
+ 'status' => 'ok'
391
+ }
392
+ }
393
+ }
394
+ end
395
+
396
+ def error_body
397
+ {
398
+ 'errors' => [{
399
+ 'code' => 'INTERNAL_SERVER_ERROR',
400
+ 'message' => 'An unknown error occurred.'
401
+ }]
402
+ }
403
+ end
404
+
405
+ def message_too_big_error_body
406
+ build_error_body('MessageTooBig', 'Message too big')
407
+ end
408
+
409
+ def not_registered_device_error_body
410
+ build_error_body(
411
+ 'DeviceNotRegistered',
412
+ '"ExponentPushToken[42]" is not a registered push notification recipient'
413
+ )
414
+ end
415
+
416
+ def message_rate_exceeded_error_body
417
+ build_error_body('MessageRateExceeded', 'Message rate exceeded')
418
+ end
419
+
420
+ def invalid_credentials_error_body
421
+ build_error_body('InvalidCredentials', 'Invalid credentials')
422
+ end
423
+
424
+ def apn_error_body
425
+ {
426
+ 'data' => [{
427
+ 'status' => 'error',
428
+ 'message' =>
429
+ 'Could not find APNs credentials for you (your_app). Check whether you are trying to send a notification to a detached app.'
430
+ }]
431
+ }
432
+ end
433
+
434
+ def client_args
435
+ [
436
+ 'https://exp.host/--/api/v2/push/send',
437
+ {
438
+ body: messages.to_json,
439
+ headers: {
440
+ 'Content-Type' => 'application/json',
441
+ 'Accept' => 'application/json'
442
+ },
443
+ accept_encoding: false
444
+ }
445
+ ]
446
+ end
447
+
448
+ def alternative_client_args(messages)
449
+ [
450
+ 'https://exp.host/--/api/v2/push/send',
451
+ {
452
+ body: messages.to_json,
453
+ headers: {
454
+ 'Content-Type' => 'application/json',
455
+ 'Accept' => 'application/json'
456
+ },
457
+ accept_encoding: false
458
+ }
459
+ ]
460
+ end
461
+
462
+ def gzip_client_args
463
+ [
464
+ 'https://exp.host/--/api/v2/push/send',
465
+ {
466
+ body: messages.to_json,
467
+ headers: {
468
+ 'Content-Type' => 'application/json',
469
+ 'Accept' => 'application/json'
470
+ },
471
+ accept_encoding: true
472
+ }
473
+ ]
474
+ end
475
+
476
+ def receipt_client_args(receipt_ids)
477
+ [
478
+ 'https://exp.host/--/api/v2/push/getReceipts',
479
+ {
480
+ body: { ids: receipt_ids }.to_json,
481
+ headers: {
482
+ 'Content-Type' => 'application/json',
483
+ 'Accept' => 'application/json'
484
+ },
485
+ accept_encoding: false
486
+ }
487
+ ]
488
+ end
489
+
490
+ def gzip_receipt_client_args(receipt_ids)
491
+ [
492
+ 'https://exp.host/--/api/v2/push/getReceipts',
493
+ {
494
+ body: { ids: receipt_ids }.to_json,
495
+ headers: {
496
+ 'Content-Type' => 'application/json',
497
+ 'Accept' => 'application/json'
498
+ },
499
+ accept_encoding: true
500
+ }
501
+ ]
502
+ end
503
+
504
+ def alternate_format_messages
505
+ [{
506
+ to: [
507
+ 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]',
508
+ 'ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]'
509
+ ],
510
+ badge: 1,
511
+ sound: 'default',
512
+ body: 'You got a completely unique message from us! /s'
513
+ }]
514
+ end
515
+
516
+ def messages
517
+ [{
518
+ to: 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]',
519
+ sound: 'default',
520
+ body: 'Hello world!'
521
+ }, {
522
+ to: 'ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]',
523
+ badge: 1,
524
+ body: "You've got mail"
525
+ }]
526
+ end
527
+
528
+ def too_many_messages
529
+ (0..101).map { create_message }
530
+ end
531
+
532
+ def create_message
533
+ id = (0...22).map { ('a'..'z').to_a[rand(26)] }.join
534
+ {
535
+ to: "ExponentPushToken[#{id}]",
536
+ sound: 'default',
537
+ body: 'Hello world!'
538
+ }
539
+ end
540
+
541
+ def build_error_body(error_code, message)
542
+ {
543
+ 'data' => [{
544
+ 'status' => 'error',
545
+ 'message' => message,
546
+ 'details' => { 'error' => error_code }
547
+ }]
548
+ }
549
+ end
550
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: expo-push
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Jesse Ruder
8
+ - Pablo Gomez
9
+ - Mike Taylor
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2022-11-01 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: typhoeus
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '1.4'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '1.4'
29
+ - !ruby/object:Gem::Dependency
30
+ name: bundler
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ - !ruby/object:Gem::Dependency
44
+ name: minitest
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ - !ruby/object:Gem::Dependency
58
+ name: rake
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ - !ruby/object:Gem::Dependency
72
+ name: rubocop
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ description: A ruby gem to communicate with the Expo Push service.
86
+ email:
87
+ - jesse@sixfivezero.net
88
+ - pablonahuelgomez@gmail.com
89
+ - mike.taylor@growmaple.com
90
+ executables: []
91
+ extensions: []
92
+ extra_rdoc_files: []
93
+ files:
94
+ - ".gitignore"
95
+ - ".rubocop.yml"
96
+ - ".travis.yml"
97
+ - Gemfile
98
+ - LICENSE.txt
99
+ - README.md
100
+ - Rakefile
101
+ - examples/getting_started.rb
102
+ - expo-push.gemspec
103
+ - lib/expo-push.rb
104
+ - lib/expo-push/too_many_messages_error.rb
105
+ - lib/expo-push/version.rb
106
+ - test/expo-push-test.rb
107
+ homepage: https://github.com/growmaple/expo-push
108
+ licenses:
109
+ - MIT
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.1.6
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Expo Push
130
+ test_files:
131
+ - test/expo-push-test.rb