# -*- coding: utf-8 -*- # BSD 2-Clause License # # Apprise - Push Notification Library. # Copyright (c) 2024, Chris Caron # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from json import loads from unittest import mock import pytest import requests from apprise.plugins.one_signal import NotifyOneSignal from helpers import AppriseURLTester from apprise import Apprise # Disable logging for a cleaner testing output import logging logging.disable(logging.CRITICAL) # Our Testing URLs apprise_url_tests = ( ('onesignal://', { # We failed to identify any valid authentication 'instance': TypeError, }), ('onesignal://:@/', { # We failed to identify any valid authentication 'instance': TypeError, }), ('onesignal://apikey/', { # no app id specified 'instance': TypeError, }), ('onesignal://appid@%20%20/', { # invalid apikey 'instance': TypeError, }), ('onesignal://appid@apikey/playerid/?lang=X', { # invalid language id (must be 2 characters) 'instance': TypeError, }), ('onesignal://appid@apikey/', { # No targets specified; we will initialize but not notify anything 'instance': NotifyOneSignal, 'notify_response': False, }), ('onesignal://appid@apikey/playerid', { # Valid playerid 'instance': NotifyOneSignal, 'privacy_url': 'onesignal://a...d@a...y/playerid', }), ('onesignal://appid@apikey/player', { # Valid player id 'instance': NotifyOneSignal, # don't include an image by default 'include_image': False, }), ('onesignal://appid@apikey/@user?image=no', { # Valid userid, no image 'instance': NotifyOneSignal, }), ('onesignal://appid@apikey/user@email.com/#seg/player/@user/%20/a', { # Valid email, valid playerid, valid user, invalid entry (%20), # and too short of an entry (a) 'instance': NotifyOneSignal, }), ('onesignal://appid@apikey?to=#segment,playerid', { # Test to= 'instance': NotifyOneSignal, }), ('onesignal://appid@apikey/#segment/@user/?batch=yes', { # Test batch= 'instance': NotifyOneSignal, }), ('onesignal://appid@apikey/#segment/@user/?batch=no', { # Test batch= 'instance': NotifyOneSignal, }), ('onesignal://templateid:appid@apikey/playerid', { # Test Template ID 'instance': NotifyOneSignal, }), ('onesignal://appid@apikey/playerid/?lang=es&subtitle=Sub', { # Test Language and Subtitle Over-ride 'instance': NotifyOneSignal, }), ('onesignal://?apikey=abc&template=tp&app=123&to=playerid', { # Test Kwargs 'instance': NotifyOneSignal, }), ('onesignal://?apikey=abc&template=tp&app=123&to=playerid&body=no' '&:key1=val1&:key2=val2', { # Test Kwargs 'instance': NotifyOneSignal, }), ('onesignal://?apikey=abc&template=tp&app=123&to=playerid&body=no' '&+key1=val1&+key2=val2', { # Test Kwargs 'instance': NotifyOneSignal, }), ('onesignal://appid@apikey/#segment/playerid/', { 'instance': NotifyOneSignal, # throw a bizzare code forcing us to fail to look it up 'response': False, 'requests_response_code': 999, }), ('onesignal://appid@apikey/#segment/playerid/', { 'instance': NotifyOneSignal, # Throws a series of connection and transfer exceptions when this flag # is set and tests that we gracfully handle them 'test_requests_exceptions': True, }), ) def test_plugin_onesignal_urls(): """ NotifyOneSignal() Apprise URLs """ # Run our general tests AppriseURLTester(tests=apprise_url_tests).run_all() def test_plugin_onesignal_edge_cases(): """ NotifyOneSignal() Batch Validation """ obj = Apprise.instantiate( 'onesignal://appid@apikey/#segment/@user/playerid/user@email.com' '/?batch=yes') # Validate that it loaded okay assert isinstance(obj, NotifyOneSignal) # all 4 types defined; but even in a batch mode, they can not be # sent in one submission assert len(obj) == 4 # # Users # obj = Apprise.instantiate( 'onesignal://appid@apikey/@user1/@user2/@user3/@user4/?batch=yes') assert isinstance(obj, NotifyOneSignal) # We can lump these together - no problem assert len(obj) == 1 # Same query, but no batch mode set obj = Apprise.instantiate( 'onesignal://appid@apikey/@user1/@user2/@user3/@user4/?batch=no') assert isinstance(obj, NotifyOneSignal) # Individual queries assert len(obj) == 4 # # Segments # obj = Apprise.instantiate( 'onesignal://appid@apikey/#segment1/#seg2/#seg3/#seg4/?batch=yes') assert isinstance(obj, NotifyOneSignal) # We can lump these together - no problem assert len(obj) == 1 # Same query, but no batch mode set obj = Apprise.instantiate( 'onesignal://appid@apikey/#segment1/#seg2/#seg3/#seg4/?batch=no') assert isinstance(obj, NotifyOneSignal) # Individual queries assert len(obj) == 4 # # Player ID's # obj = Apprise.instantiate( 'onesignal://appid@apikey/pid1/pid2/pid3/pid4/?batch=yes') assert isinstance(obj, NotifyOneSignal) # We can lump these together - no problem assert len(obj) == 1 # Same query, but no batch mode set obj = Apprise.instantiate( 'onesignal://appid@apikey/pid1/pid2/pid3/pid4/?batch=no') assert isinstance(obj, NotifyOneSignal) # Individual queries assert len(obj) == 4 # # Emails # emails = ('abc@yahoo.ca', 'def@yahoo.ca', 'ghi@yahoo.ca', 'jkl@yahoo.ca') obj = Apprise.instantiate( 'onesignal://appid@apikey/{}/?batch=yes'.format('/'.join(emails))) assert isinstance(obj, NotifyOneSignal) # We can lump these together - no problem assert len(obj) == 1 # Same query, but no batch mode set obj = Apprise.instantiate( 'onesignal://appid@apikey/{}/?batch=no'.format('/'.join(emails))) assert isinstance(obj, NotifyOneSignal) # Individual queries assert len(obj) == 4 # # Mixed # emails = ('abc@yahoo.ca', 'def@yahoo.ca', 'ghi@yahoo.ca', 'jkl@yahoo.ca') users = ('@user1', '@user2', '@user3', '@user4') players = ('player1', 'player2', 'player3', 'player4') segments = ('#seg1', '#seg2', '#seg3', '#seg4') path = '{}/{}/{}/{}'.format( '/'.join(emails), '/'.join(users), '/'.join(players), '/'.join(segments)) obj = Apprise.instantiate( 'onesignal://appid@apikey/{}/?batch=yes'.format(path)) assert isinstance(obj, NotifyOneSignal) # We can lump these together - no problem assert len(obj) == 4 # Same query, but no batch mode set obj = Apprise.instantiate( 'onesignal://appid@apikey/{}/?batch=no'.format(path)) assert isinstance(obj, NotifyOneSignal) # Individual queries assert len(obj) == 16 # custom must be a dictionary with pytest.raises(TypeError): NotifyOneSignal( app='appid', apikey='key', targets=['@user'], custom='not-a-dict') # postback must be a dictionary with pytest.raises(TypeError): NotifyOneSignal( app='appid', apikey='key', targets=['@user'], custom=[], postback='not-a-dict') @mock.patch('requests.post') def test_plugin_onesignal_notifications(mock_post): """ OneSignal() Notifications Support """ # Prepare Mock mock_post.return_value = requests.Request() mock_post.return_value.status_code = requests.codes.ok # Load URL with Template instance = Apprise.instantiate( 'onesignal://templateid:appid@apikey/@user/?:key1=value1&+key3=value3') # Validate that it loaded okay assert isinstance(instance, NotifyOneSignal) response = instance.notify("hello world") assert response is True assert mock_post.call_count == 1 assert mock_post.call_args_list[0][0][0] == \ 'https://api.onesignal.com/notifications' details = mock_post.call_args_list[0] payload = loads(details[1]['data']) assert payload == { 'app_id': 'appid', 'contents': {'en': 'hello world'}, 'content_available': True, 'template_id': 'templateid', 'custom_data': {'key1': 'value1'}, 'data': {'key3': 'value3'}, 'large_icon': 'https://github.com/caronc/apprise' '/raw/master/apprise/assets/themes/default/apprise-info-72x72.png', 'small_icon': 'https://github.com/caronc/apprise' '/raw/master/apprise/assets/themes/default/apprise-info-32x32.png', 'include_external_user_ids': ['@user']} mock_post.reset_mock() # Load URL with Template and disable body instance = Apprise.instantiate( 'onesignal://templateid:appid@apikey/@user/?contents=no') # Validate that it loaded okay assert isinstance(instance, NotifyOneSignal) response = instance.notify("hello world") assert response is True assert mock_post.call_count == 1 assert mock_post.call_args_list[0][0][0] == \ 'https://api.onesignal.com/notifications' details = mock_post.call_args_list[0] payload = loads(details[1]['data']) assert payload == { 'app_id': 'appid', 'content_available': True, 'template_id': 'templateid', 'large_icon': 'https://github.com/caronc/apprise' '/raw/master/apprise/assets/themes/default/apprise-info-72x72.png', 'small_icon': 'https://github.com/caronc/apprise' '/raw/master/apprise/assets/themes/default/apprise-info-32x32.png', 'include_external_user_ids': ['@user']} # Now set a title mock_post.reset_mock() response = instance.notify("hello world", title="mytitle") assert response is True assert mock_post.call_count == 1 assert mock_post.call_args_list[0][0][0] == \ 'https://api.onesignal.com/notifications' details = mock_post.call_args_list[0] payload = loads(details[1]['data']) assert payload == { 'app_id': 'appid', 'headings': {'en': 'mytitle'}, 'content_available': True, 'template_id': 'templateid', 'large_icon': 'https://github.com/caronc/apprise' '/raw/master/apprise/assets/themes/default/apprise-info-72x72.png', 'small_icon': 'https://github.com/caronc/apprise' '/raw/master/apprise/assets/themes/default/apprise-info-32x32.png', 'include_external_user_ids': ['@user']} # Test without decoding parameters instance = Apprise.instantiate( 'onesignal://templateid:appid@apikey/@user/' '?:par=b64:eyJhIjoxLCJiIjoyfQ==&decode=no') assert isinstance(instance, NotifyOneSignal) and \ instance.custom_data == {"par": "b64:eyJhIjoxLCJiIjoyfQ=="} # Now same with loading parameters instance = Apprise.instantiate( 'onesignal://templateid:appid@apikey/@user/' '?:par=b64:eyJhIjoxLCJiIjoyfQ==&decode=yes') assert isinstance(instance, NotifyOneSignal) and \ instance.custom_data == {"par": {"a": 1, "b": 2}} # Test bad data in general instance = Apprise.instantiate( 'onesignal://templateid:appid@apikey/@user/' '?:par=garbage1&decode=yes') assert isinstance(instance, NotifyOneSignal) and \ instance.custom_data == {"par": 'garbage1'} instance = Apprise.instantiate( 'onesignal://templateid:appid@apikey/@user/' '?:par=b64:garbage2&decode=yes') assert isinstance(instance, NotifyOneSignal) and \ instance.custom_data == {"par": 'b64:garbage2'} instance = Apprise.instantiate( 'onesignal://templateid:appid@apikey/@user/' '?:par=b64:garbage3==&decode=yes') assert isinstance(instance, NotifyOneSignal) and \ instance.custom_data == {"par": 'b64:garbage3=='} # Now same with not-base64 parameters instance = Apprise.instantiate( 'onesignal://templateid:appid@apikey/@user/' '?:par=eyJhIjoxLCJiIjoyfQ==&:par2=123&decode=yes') assert isinstance(instance, NotifyOneSignal) and \ instance.custom_data == { "par": "eyJhIjoxLCJiIjoyfQ==", "par2": "123" } # Test incorrect base64 parameters. Second one has incorrect padding url = 'onesignal://templateid:appid@apikey/@user/' \ '?:par=b64:1234=&:par2=b64:eyJhIjoxLCJiIjoyfQ&' \ ':par3=b64:eyJhIjoxLCJiIjoyfQ==&decode=yes' instance = Apprise.instantiate(url) assert isinstance(instance, NotifyOneSignal) and instance.custom_data == { "par": "b64:1234=", "par2": "b64:eyJhIjoxLCJiIjoyfQ", "par3": {"a": 1, "b": 2} }