exponent-server-sdk-jm 0.1.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,680 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'exponent-server-sdk-jm'
5
+ require 'exponent-server-sdk-jm/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
+ # DEPRECATED -- TESTS BELOW HERE RELATE TO CODE THAT WILL BE REMOVED
342
+
343
+ def test_publish_with_success
344
+ @response_mock.expect(:code, 200)
345
+ @response_mock.expect(:body, success_body.to_json)
346
+
347
+ @mock.expect(:post, @response_mock, client_args)
348
+
349
+ @client.publish(messages)
350
+
351
+ @mock.verify
352
+ end
353
+
354
+ def test_publish_with_gzip_success
355
+ @response_mock.expect(:code, 200)
356
+ @response_mock.expect(:body, success_body.to_json)
357
+
358
+ @mock.expect(:post, @response_mock, gzip_client_args)
359
+
360
+ @client_gzip.publish(messages)
361
+
362
+ @mock.verify
363
+ end
364
+
365
+ def test_publish_with_gzip
366
+ @response_mock.expect(:code, 200)
367
+ @response_mock.expect(:body, success_body.to_json)
368
+
369
+ @mock.expect(:post, @response_mock, gzip_client_args)
370
+
371
+ @client_gzip.publish(messages)
372
+
373
+ @mock.verify
374
+ end
375
+
376
+ def test_publish_with_unknown_error
377
+ @response_mock.expect(:code, 400)
378
+ @response_mock.expect(:body, error_body.to_json)
379
+ message = 'An unknown error occurred.'
380
+
381
+ @mock.expect(:post, @response_mock, client_args)
382
+
383
+ exception = assert_raises Exponent::Push::UnknownError do
384
+ @client.publish(messages)
385
+ end
386
+
387
+ assert_equal(message, exception.message)
388
+
389
+ @mock.verify
390
+ end
391
+
392
+ def test_publish_with_device_not_registered_error
393
+ @response_mock.expect(:code, 200)
394
+ @response_mock.expect(:body, not_registered_device_error_body.to_json)
395
+ message = '"ExponentPushToken[42]" is not a registered push notification recipient'
396
+
397
+ @mock.expect(:post, @response_mock, client_args)
398
+
399
+ exception = assert_raises Exponent::Push::DeviceNotRegisteredError do
400
+ @client.publish(messages)
401
+ end
402
+
403
+ assert_equal(message, exception.message)
404
+
405
+ @mock.verify
406
+ end
407
+
408
+ def test_publish_with_message_too_big_error
409
+ @response_mock.expect(:code, 200)
410
+ @response_mock.expect(:body, message_too_big_error_body.to_json)
411
+ message = 'Message too big'
412
+
413
+ @mock.expect(:post, @response_mock, client_args)
414
+
415
+ exception = assert_raises Exponent::Push::MessageTooBigError do
416
+ @client.publish(messages)
417
+ end
418
+
419
+ assert_equal(message, exception.message)
420
+
421
+ @mock.verify
422
+ end
423
+
424
+ def test_publish_with_message_rate_exceeded_error
425
+ @response_mock.expect(:code, 200)
426
+ @response_mock.expect(:body, message_rate_exceeded_error_body.to_json)
427
+ message = 'Message rate exceeded'
428
+
429
+ @mock.expect(:post, @response_mock, client_args)
430
+
431
+ exception = assert_raises Exponent::Push::MessageRateExceededError do
432
+ @client.publish(messages)
433
+ end
434
+
435
+ assert_equal(message, exception.message)
436
+
437
+ @mock.verify
438
+ end
439
+
440
+ def test_publish_with_invalid_credentials_error
441
+ @response_mock.expect(:code, 200)
442
+ @response_mock.expect(:body, invalid_credentials_error_body.to_json)
443
+ message = 'Invalid credentials'
444
+
445
+ @mock.expect(:post, @response_mock, client_args)
446
+
447
+ exception = assert_raises Exponent::Push::InvalidCredentialsError do
448
+ @client.publish(messages)
449
+ end
450
+
451
+ assert_equal(message, exception.message)
452
+
453
+ @mock.verify
454
+ end
455
+
456
+ def test_publish_with_apn_error
457
+ @response_mock.expect(:code, 200)
458
+ @response_mock.expect(:body, apn_error_body.to_json)
459
+
460
+ @mock.expect(:post, @response_mock, client_args)
461
+
462
+ exception = assert_raises Exponent::Push::UnknownError do
463
+ @client.publish(messages)
464
+ end
465
+
466
+ assert_match(/Unknown error format/, exception.message)
467
+
468
+ @mock.verify
469
+ end
470
+
471
+ private
472
+
473
+ def success_body
474
+ { 'data' => [{ 'status' => 'ok' }] }
475
+ end
476
+
477
+ def success_receipt
478
+ 'YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY'
479
+ end
480
+
481
+ def error_receipt
482
+ 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
483
+ end
484
+
485
+ def receipt_success_body
486
+ {
487
+ 'data' => {
488
+ 'YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY' => {
489
+ 'status' => 'ok'
490
+ }
491
+ }
492
+ }
493
+ end
494
+
495
+ def receipt_error_body
496
+ {
497
+ 'data' => {
498
+ 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' => {
499
+ 'status' => 'error',
500
+ 'message' => 'The Apple Push Notification service failed to send the notification',
501
+ 'details' => {
502
+ 'error' => 'DeviceNotRegistered'
503
+ }
504
+ }
505
+ }
506
+ }
507
+ end
508
+
509
+ def multiple_receipts
510
+ {
511
+ 'data' => {
512
+ 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' => {
513
+ 'status' => 'error',
514
+ 'message' => 'The Apple Push Notification service failed to send the notification',
515
+ 'details' => {
516
+ 'error' => 'DeviceNotRegistered'
517
+ }
518
+ },
519
+ 'YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY' => {
520
+ 'status' => 'ok'
521
+ }
522
+ }
523
+ }
524
+ end
525
+
526
+ def error_body
527
+ {
528
+ 'errors' => [{
529
+ 'code' => 'INTERNAL_SERVER_ERROR',
530
+ 'message' => 'An unknown error occurred.'
531
+ }]
532
+ }
533
+ end
534
+
535
+ def message_too_big_error_body
536
+ build_error_body('MessageTooBig', 'Message too big')
537
+ end
538
+
539
+ def not_registered_device_error_body
540
+ build_error_body(
541
+ 'DeviceNotRegistered',
542
+ '"ExponentPushToken[42]" is not a registered push notification recipient'
543
+ )
544
+ end
545
+
546
+ def message_rate_exceeded_error_body
547
+ build_error_body('MessageRateExceeded', 'Message rate exceeded')
548
+ end
549
+
550
+ def invalid_credentials_error_body
551
+ build_error_body('InvalidCredentials', 'Invalid credentials')
552
+ end
553
+
554
+ def apn_error_body
555
+ {
556
+ 'data' => [{
557
+ 'status' => 'error',
558
+ 'message' =>
559
+ 'Could not find APNs credentials for you (your_app). Check whether you are trying to send a notification to a detached app.'
560
+ }]
561
+ }
562
+ end
563
+
564
+ def client_args
565
+ [
566
+ 'https://exp.host/--/api/v2/push/send',
567
+ {
568
+ body: messages.to_json,
569
+ headers: {
570
+ 'Content-Type' => 'application/json',
571
+ 'Accept' => 'application/json'
572
+ },
573
+ accept_encoding: false
574
+ }
575
+ ]
576
+ end
577
+
578
+ def alternative_client_args(messages)
579
+ [
580
+ 'https://exp.host/--/api/v2/push/send',
581
+ {
582
+ body: messages.to_json,
583
+ headers: {
584
+ 'Content-Type' => 'application/json',
585
+ 'Accept' => 'application/json'
586
+ },
587
+ accept_encoding: false
588
+ }
589
+ ]
590
+ end
591
+
592
+ def gzip_client_args
593
+ [
594
+ 'https://exp.host/--/api/v2/push/send',
595
+ {
596
+ body: messages.to_json,
597
+ headers: {
598
+ 'Content-Type' => 'application/json',
599
+ 'Accept' => 'application/json'
600
+ },
601
+ accept_encoding: true
602
+ }
603
+ ]
604
+ end
605
+
606
+ def receipt_client_args(receipt_ids)
607
+ [
608
+ 'https://exp.host/--/api/v2/push/getReceipts',
609
+ {
610
+ body: { ids: receipt_ids }.to_json,
611
+ headers: {
612
+ 'Content-Type' => 'application/json',
613
+ 'Accept' => 'application/json'
614
+ },
615
+ accept_encoding: false
616
+ }
617
+ ]
618
+ end
619
+
620
+ def gzip_receipt_client_args(receipt_ids)
621
+ [
622
+ 'https://exp.host/--/api/v2/push/getReceipts',
623
+ {
624
+ body: { ids: receipt_ids }.to_json,
625
+ headers: {
626
+ 'Content-Type' => 'application/json',
627
+ 'Accept' => 'application/json'
628
+ },
629
+ accept_encoding: true
630
+ }
631
+ ]
632
+ end
633
+
634
+ def alternate_format_messages
635
+ [{
636
+ to: [
637
+ 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]',
638
+ 'ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]'
639
+ ],
640
+ badge: 1,
641
+ sound: 'default',
642
+ body: 'You got a completely unique message from us! /s'
643
+ }]
644
+ end
645
+
646
+ def messages
647
+ [{
648
+ to: 'ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]',
649
+ sound: 'default',
650
+ body: 'Hello world!'
651
+ }, {
652
+ to: 'ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]',
653
+ badge: 1,
654
+ body: "You've got mail"
655
+ }]
656
+ end
657
+
658
+ def too_many_messages
659
+ (0..101).map { create_message }
660
+ end
661
+
662
+ def create_message
663
+ id = (0...22).map { ('a'..'z').to_a[rand(26)] }.join
664
+ {
665
+ to: "ExponentPushToken[#{id}]",
666
+ sound: 'default',
667
+ body: 'Hello world!'
668
+ }
669
+ end
670
+
671
+ def build_error_body(error_code, message)
672
+ {
673
+ 'data' => [{
674
+ 'status' => 'error',
675
+ 'message' => message,
676
+ 'details' => { 'error' => error_code }
677
+ }]
678
+ }
679
+ end
680
+ end