# -*- 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 unittest import mock import requests import pytest from json import dumps, loads from apprise import Apprise from apprise.plugins.NotifyWhatsApp import NotifyWhatsApp from helpers import AppriseURLTester # Disable logging for a cleaner testing output import logging logging.disable(logging.CRITICAL) # Our Testing URLs apprise_url_tests = ( ('whatsapp://', { # Not enough details 'instance': TypeError, }), ('whatsapp://:@/', { # invalid Access Token 'instance': TypeError, }), ('whatsapp://{}@_'.format('a' * 32), { # token provided but invalid from 'instance': TypeError, }), ('whatsapp://%20:{}@12345/{}'.format('e' * 32, '4' * 11), { # Invalid template 'instance': TypeError, }), ('whatsapp://{}@{}'.format('b' * 32, 10**9), { # token provided and from but no target no 'instance': NotifyWhatsApp, # Response will fail due to no targets defined 'notify_response': False, }), ('whatsapp://{}:{}@{}/123/{}/abcd/'.format( 'a' * 32, 'b' * 32, '3' * 11, '9' * 15), { # valid everything but target numbers 'instance': NotifyWhatsApp, # Response will fail due to target not being loaded 'notify_response': False, }), ('whatsapp://{}@12345/{}'.format('e' * 32, '4' * 11), { # simple message 'instance': NotifyWhatsApp, # Our expected url(privacy=True) startswith() response: 'privacy_url': 'whatsapp://e...e@1...5/%2B44444444444/', }), ('whatsapp://template:{}@12345/{}'.format('e' * 32, '4' * 11), { # template 'instance': NotifyWhatsApp, # Our expected url(privacy=True) startswith() response: 'privacy_url': 'whatsapp://template:e...e@1...5/%2B44444444444/', }), ('whatsapp://template:{}@12345/{}?lang=fr_CA'.format('e' * 32, '4' * 11), { # template with language over-ride 'instance': NotifyWhatsApp, # Our expected url(privacy=True) startswith() response: 'privacy_url': 'whatsapp://template:e...e@1...5/%2B44444444444/', }), ('whatsapp://{}@12345/{}?template=template&lang=fr_CA'.format( 'e' * 32, '4' * 11), { # template specified as kwarg with language over-ride 'instance': NotifyWhatsApp, # Our expected url(privacy=True) startswith() response: 'privacy_url': 'whatsapp://template:e...e@1...5/%2B44444444444/', }), ('whatsapp://template:{}@12345/{}?lang=1234'.format('e' * 32, '4' * 11), { # template with invalid language over-ride 'instance': TypeError, }), ('whatsapp://template:{}@12345/{}?:1=test&:body=3&:type=2'.format( 'e' * 32, '4' * 11), { # template with kwarg assignments # {{1}} assigned test # {{2}} assigned Apprise Message type (special keyword) # {{3}} assigned Apprise Message body (special keyword) 'instance': NotifyWhatsApp, # Our expected url(privacy=True) startswith() response: 'privacy_url': 'whatsapp://template:e...e@1...5/%2B44444444444/', }), ('whatsapp://template:{}@12345/{}?:invalid=23'.format( 'e' * 32, '4' * 11), { # template with kwarg assignments # Invalid keyword specified; cna only be a digit OR `body' or 'type' 'instance': TypeError, }), ('whatsapp://template:{}@12345/{}?:body='.format( 'e' * 32, '4' * 11), { # template with kwarg assignments # No Body Assigment 'instance': TypeError, }), ('whatsapp://template:{}@12345/{}?:1=Test&:body=1'.format( 'e' * 32, '4' * 11), { # template with kwarg assignments # Ambiguious assignment {{1}} assigned twice 'instance': TypeError, }), ('whatsapp://{}:{}@123456/{}'.format('a' * 32, 'b' * 32, '4' * 11), { # using short-code (6 characters) 'instance': NotifyWhatsApp, }), ('whatsapp://_?token={}&from={}&to={}'.format( 'd' * 32, '5' * 11, '6' * 11), { # use get args to acomplish the same thing 'instance': NotifyWhatsApp, }), ('whatsapp://_?token={}&source={}&to={}'.format( 'd' * 32, '5' * 11, '6' * 11), { # use get args to acomplish the same thing (use source instead of from) 'instance': NotifyWhatsApp, }), ('whatsapp://{}@12345/{}'.format('e' * 32, '4' * 11), { 'instance': NotifyWhatsApp, # throw a bizzare code forcing us to fail to look it up 'response': False, 'requests_response_code': 999, }), ('whatsapp://{}@12345/{}'.format('e' * 32, '4' * 11), { 'instance': NotifyWhatsApp, # 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_whatsapp_urls(): """ NotifyWhatsApp() Apprise URLs """ # Run our general tests AppriseURLTester(tests=apprise_url_tests).run_all() @mock.patch('requests.post') def test_plugin_whatsapp_auth(mock_post): """ NotifyWhatsApp() Auth - account-wide auth token - API key and its own auth token """ response = mock.Mock() response.content = '' response.status_code = requests.codes.ok # Prepare Mock mock_post.return_value = response # Initialize some generic (but valid) tokens token = '{}'.format('b' * 32) from_phone_id = '123456787654321' target = '+1 (555) 987-6543' message_contents = "test" # Variation of initialization without API key obj = Apprise.instantiate( 'whatsapp://{}@{}/{}' .format(token, from_phone_id, target)) assert isinstance(obj, NotifyWhatsApp) is True assert isinstance(obj.url(), str) is True # Send Notification assert obj.send(body=message_contents) is True # Validate expected call parameters assert mock_post.call_count == 1 first_call = mock_post.call_args_list[0] # URL and message parameters are the same for both calls assert first_call[0][0] == \ 'https://graph.facebook.com/v17.0/{}/messages'.format( from_phone_id) response = loads(first_call[1]['data']) assert response['text']['body'] == message_contents assert response['to'] == '+15559876543' assert response['recipient_type'] == 'individual' @mock.patch('requests.post') def test_plugin_whatsapp_edge_cases(mock_post): """ NotifyWhatsApp() Edge Cases """ # Prepare our response response = requests.Request() response.status_code = requests.codes.ok # Prepare Mock mock_post.return_value = response # Initialize some generic (but valid) tokens token = 'b' * 32 from_phone_id = '123456787654321' targets = ('+1 (555) 123-3456', ) # No token specified with pytest.raises(TypeError): NotifyWhatsApp( token=None, from_phone_id=from_phone_id, targets=targets) # No from_phone_id specified with pytest.raises(TypeError): NotifyWhatsApp( token=token, from_phone_id=None, targets=targets) # a error response response.status_code = 400 response.content = dumps({ 'error': { 'code': 21211, 'message': "The 'To' number +1234567 is not a valid phone number.", }, }) mock_post.return_value = response # Initialize our object obj = NotifyWhatsApp( token=token, from_phone_id=from_phone_id, targets=targets) # We will fail with the above error code assert obj.notify('title', 'body', 'info') is False