mirror of https://github.com/caronc/apprise
1103 lines
32 KiB
Python
1103 lines
32 KiB
Python
# BSD 2-Clause License
|
|
#
|
|
# Apprise - Push Notification Library.
|
|
# Copyright (c) 2025, Chris Caron <lead2gold@gmail.com>
|
|
#
|
|
# 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 inspect import cleandoc
|
|
from json import dumps, loads
|
|
|
|
# Disable logging for a cleaner testing output
|
|
import logging
|
|
import os
|
|
from unittest import mock
|
|
|
|
from helpers import AppriseURLTester
|
|
import pytest
|
|
import requests
|
|
|
|
from apprise import Apprise, AppriseAttachment, NotifyType
|
|
from apprise.plugins.slack import NotifySlack
|
|
|
|
logging.disable(logging.CRITICAL)
|
|
|
|
# Attachment Directory
|
|
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), "var")
|
|
|
|
# Our Testing URLs
|
|
apprise_url_tests = (
|
|
(
|
|
"slack://",
|
|
{
|
|
"instance": TypeError,
|
|
},
|
|
),
|
|
(
|
|
"slack://:@/",
|
|
{
|
|
"instance": TypeError,
|
|
},
|
|
),
|
|
(
|
|
"slack://T1JJ3T3L2",
|
|
{
|
|
# Just Token 1 provided
|
|
"instance": TypeError,
|
|
},
|
|
),
|
|
(
|
|
"slack://T1JJ3T3L2/A1BRTD4JD/",
|
|
{
|
|
# Just 2 tokens provided
|
|
"instance": TypeError,
|
|
},
|
|
),
|
|
(
|
|
"slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#hmm/#-invalid-",
|
|
{
|
|
# No username specified; this is still okay as we sub in
|
|
# default; The one invalid channel is skipped when sending a
|
|
# message
|
|
"instance": NotifySlack,
|
|
# There is an invalid channel that we will fail to deliver to
|
|
# as a result the response type will be false
|
|
"response": False,
|
|
"requests_response_text": {
|
|
"ok": False,
|
|
"message": "Bad Channel",
|
|
},
|
|
},
|
|
),
|
|
(
|
|
"slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#channel",
|
|
{
|
|
# No username specified; this is still okay as we sub in
|
|
# default; The one invalid channel is skipped when sending a
|
|
# message
|
|
"instance": NotifySlack,
|
|
# don't include an image by default
|
|
"include_image": False,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
(
|
|
"slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/+id/@id/",
|
|
{
|
|
# + encoded id,
|
|
# @ userid
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
(
|
|
(
|
|
"slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/"
|
|
"?to=#nuxref"
|
|
),
|
|
{
|
|
"instance": NotifySlack,
|
|
# Our expected url(privacy=True) startswith() response:
|
|
"privacy_url": "slack://username@T...2/A...D/T...Q/",
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
(
|
|
("slack://username@T1JJ3T3L2/A1BRTD4JD/"
|
|
"TIiajkdnlazkcOXrIdevi7FQ/#nuxref"),
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
# You can't send to email using webhook
|
|
(
|
|
"slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnl/user@gmail.com",
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
# we'll have a notify response failure in this case
|
|
"notify_response": False,
|
|
},
|
|
),
|
|
# Specify Token on argument string (with username)
|
|
(
|
|
"slack://bot@_/#nuxref?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnadfdajkjkfl/",
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
# Specify Token and channels on argument string (no username)
|
|
(
|
|
("slack://?token=T1JJ3T3L2/A1BRTD4JD"
|
|
"/TIiajkdnlazkcOXrIdevi7FQ/&to=#chan"),
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
# Test webhook that doesn't have a proper response
|
|
(
|
|
("slack://username@T1JJ3T3L2/A1BRTD4JD/"
|
|
"TIiajkdnlazkcOXrIdevi7FQ/#nuxref"),
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "fail",
|
|
# we'll have a notify response failure in this case
|
|
"notify_response": False,
|
|
},
|
|
),
|
|
# Test using a bot-token (also test footer set to no flag)
|
|
(
|
|
"slack://username@xoxb-1234-1234-abc124/#nuxref?footer=no",
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": {
|
|
"ok": True,
|
|
"message": "",
|
|
},
|
|
},
|
|
),
|
|
# Test blocks mode
|
|
(
|
|
(
|
|
"slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/"
|
|
"&to=#chan&blocks=yes&footer=yes"
|
|
),
|
|
{"instance": NotifySlack, "requests_response_text": "ok"},
|
|
),
|
|
(
|
|
(
|
|
"slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/"
|
|
"&to=#chan&blocks=yes&footer=no"
|
|
),
|
|
{"instance": NotifySlack, "requests_response_text": "ok"},
|
|
),
|
|
(
|
|
(
|
|
"slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/"
|
|
"&to=#chan&blocks=yes&footer=yes&image=no"
|
|
),
|
|
{"instance": NotifySlack, "requests_response_text": "ok"},
|
|
),
|
|
(
|
|
(
|
|
"slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/"
|
|
"&to=#chan&blocks=yes&format=text"
|
|
),
|
|
{"instance": NotifySlack, "requests_response_text": "ok"},
|
|
),
|
|
(
|
|
(
|
|
"slack://?token=T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/"
|
|
"&to=#chan&blocks=no&format=text"
|
|
),
|
|
{"instance": NotifySlack, "requests_response_text": "ok"},
|
|
),
|
|
# Test using a bot-token as argument
|
|
(
|
|
"slack://?token=xoxb-1234-1234-abc124&to=#nuxref&footer=no&user=test",
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": {
|
|
"ok": True,
|
|
"message": "",
|
|
},
|
|
# Our expected url(privacy=True) startswith() response:
|
|
"privacy_url": "slack://test@x...4/nuxref/",
|
|
},
|
|
),
|
|
# We contain 1 or more invalid channels, so we'll fail on our notify call
|
|
(
|
|
"slack://?token=xoxb-1234-1234-abc124&to=#nuxref,#$,#-&footer=no",
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": {
|
|
"ok": True,
|
|
"message": "",
|
|
},
|
|
# We fail because of the empty channel #$ and #-
|
|
"notify_response": False,
|
|
},
|
|
),
|
|
(
|
|
"slack://username@xoxb-1234-1234-abc124/#nuxref",
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": {
|
|
"ok": True,
|
|
"message": "",
|
|
},
|
|
# we'll fail to send attachments because we had no 'file' response
|
|
# in our object
|
|
"response": False,
|
|
},
|
|
),
|
|
(
|
|
"slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ",
|
|
{
|
|
# Missing a channel, falls back to webhook channel bindings
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
# Native URL Support, take the slack URL and still build from it
|
|
(
|
|
"https://hooks.slack.com/services/{}/{}/{}".format(
|
|
"A" * 9, "B" * 9, "c" * 24
|
|
),
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
# Native URL Support with arguments
|
|
(
|
|
"https://hooks.slack.com/services/{}/{}/{}?format=text".format(
|
|
"A" * 9, "B" * 9, "c" * 24
|
|
),
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
(
|
|
"slack://username@-INVALID-/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#cool",
|
|
{
|
|
# invalid 1st Token
|
|
"instance": TypeError,
|
|
},
|
|
),
|
|
(
|
|
"slack://username@T1JJ3T3L2/-INVALID-/TIiajkdnlazkcOXrIdevi7FQ/#great",
|
|
{
|
|
# invalid 2rd Token
|
|
"instance": TypeError,
|
|
},
|
|
),
|
|
(
|
|
"slack://username@T1JJ3T3L2/A1BRTD4JD/-INVALID-/#channel",
|
|
{
|
|
# invalid 3rd Token
|
|
"instance": TypeError,
|
|
},
|
|
),
|
|
(
|
|
"slack://l2g@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#usenet",
|
|
{
|
|
"instance": NotifySlack,
|
|
# force a failure
|
|
"response": False,
|
|
"requests_response_code": requests.codes.internal_server_error,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
(
|
|
"slack://respect@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#a",
|
|
{
|
|
"instance": NotifySlack,
|
|
# throw a bizzare code forcing us to fail to look it up
|
|
"response": False,
|
|
"requests_response_code": 999,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
(
|
|
"slack://notify@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#b",
|
|
{
|
|
"instance": NotifySlack,
|
|
# Throws a series of i/o exceptions with this flag
|
|
# is set and tests that we gracfully handle them
|
|
"test_requests_exceptions": True,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
(
|
|
"slack://notify@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#b:100",
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
(
|
|
"slack://notify@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/+124:100",
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
# test a case where we have a channel defined alone (without a thread_ts)
|
|
# that exists after a definition where a thread_ts does exist. this
|
|
# tests the branch of code that ensures we do not pass the same thread_ts
|
|
# twice
|
|
(
|
|
(
|
|
"slack://notify@T1JJ3T3L2/A1BRTD4JD/"
|
|
"TIiajkdnlazkcOXrIdevi7FQ/+124:100/@chan"
|
|
),
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
},
|
|
),
|
|
(
|
|
"slack://notify@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#b:bad",
|
|
{
|
|
"instance": NotifySlack,
|
|
"requests_response_text": "ok",
|
|
# we'll fail because our thread_ts is bad
|
|
"response": False,
|
|
},
|
|
),
|
|
)
|
|
|
|
|
|
def test_plugin_slack_urls():
|
|
"""NotifySlack() Apprise URLs."""
|
|
|
|
# Run our general tests
|
|
AppriseURLTester(tests=apprise_url_tests).run_all()
|
|
|
|
|
|
@mock.patch("requests.request")
|
|
def test_plugin_slack_oauth_access_token(mock_request):
|
|
"""NotifySlack() OAuth Access Token Tests."""
|
|
|
|
# Generate an invalid bot token
|
|
token = "xo-invalid"
|
|
|
|
request = mock.Mock()
|
|
request.content = dumps({
|
|
"ok": True,
|
|
"message": "",
|
|
"channel": "C123456",
|
|
})
|
|
request.status_code = requests.codes.ok
|
|
|
|
# We'll fail to validate the access_token
|
|
with pytest.raises(TypeError):
|
|
NotifySlack(access_token=token)
|
|
|
|
# Generate a (valid) bot token
|
|
token = "xoxb-1234-1234-abc124"
|
|
|
|
# Prepare Mock
|
|
mock_request.return_value = request
|
|
# Variation Initializations
|
|
obj = NotifySlack(access_token=token, targets="#apprise")
|
|
assert isinstance(obj, NotifySlack) is True
|
|
assert isinstance(obj.url(), str) is True
|
|
|
|
# apprise room was found
|
|
assert obj.send(body="test") is True
|
|
|
|
# Test Valid Attachment
|
|
mock_request.reset_mock()
|
|
mock_request.side_effect = [
|
|
request,
|
|
mock.Mock(**{
|
|
"content": dumps({
|
|
"ok": True,
|
|
"upload_url": "https://files.slack.com/upload/v1/ABC123",
|
|
"file_id": "F123ABC456",
|
|
}),
|
|
"status_code": requests.codes.ok,
|
|
}),
|
|
mock.Mock(
|
|
**{"content": b"OK - 123", "status_code": requests.codes.ok}
|
|
),
|
|
mock.Mock(**{
|
|
"content": dumps({
|
|
"ok": True,
|
|
"files": [{"id": "F123ABC456", "title": "slack-test"}],
|
|
}),
|
|
"status_code": requests.codes.ok,
|
|
}),
|
|
]
|
|
|
|
path = os.path.join(TEST_VAR_DIR, "apprise-test.gif")
|
|
attach = AppriseAttachment(path)
|
|
assert (
|
|
obj.notify(
|
|
body="body",
|
|
title="title",
|
|
notify_type=NotifyType.INFO,
|
|
attach=attach,
|
|
)
|
|
is True
|
|
)
|
|
|
|
assert mock_request.call_count == 4
|
|
assert mock_request.call_args_list[0][0][0] == "post"
|
|
assert (
|
|
mock_request.call_args_list[0][0][1]
|
|
== "https://slack.com/api/chat.postMessage"
|
|
)
|
|
assert mock_request.call_args_list[1][0][0] == "get"
|
|
assert (
|
|
mock_request.call_args_list[1][0][1]
|
|
== "https://slack.com/api/files.getUploadURLExternal"
|
|
)
|
|
assert mock_request.call_args_list[2][0][0] == "post"
|
|
assert (
|
|
mock_request.call_args_list[2][0][1]
|
|
== "https://files.slack.com/upload/v1/ABC123"
|
|
)
|
|
assert mock_request.call_args_list[3][0][0] == "post"
|
|
assert (
|
|
mock_request.call_args_list[3][0][1]
|
|
== "https://slack.com/api/files.completeUploadExternal"
|
|
)
|
|
|
|
# Test a valid attachment that throws an Connection Error
|
|
mock_request.return_value = None
|
|
mock_request.side_effect = (
|
|
request,
|
|
requests.ConnectionError(0, "requests.ConnectionError() not handled"),
|
|
)
|
|
assert (
|
|
obj.notify(
|
|
body="body",
|
|
title="title",
|
|
notify_type=NotifyType.INFO,
|
|
attach=attach,
|
|
)
|
|
is False
|
|
)
|
|
|
|
# Test a valid attachment that throws an OSError
|
|
mock_request.return_value = None
|
|
mock_request.side_effect = (request, OSError(0, "OSError"))
|
|
assert (
|
|
obj.notify(
|
|
body="body",
|
|
title="title",
|
|
notify_type=NotifyType.INFO,
|
|
attach=attach,
|
|
)
|
|
is False
|
|
)
|
|
|
|
# Reset our mock object back to how it was
|
|
mock_request.return_value = request
|
|
mock_request.side_effect = None
|
|
|
|
# Test invalid attachment
|
|
path = os.path.join(TEST_VAR_DIR, "/invalid/path/to/an/invalid/file.jpg")
|
|
assert (
|
|
obj.notify(
|
|
body="body",
|
|
title="title",
|
|
notify_type=NotifyType.INFO,
|
|
attach=path,
|
|
)
|
|
is False
|
|
)
|
|
|
|
# Test case where expected return attachment payload is invalid
|
|
mock_request.reset_mock()
|
|
mock_request.side_effect = [
|
|
request,
|
|
mock.Mock(**{
|
|
"content": dumps({
|
|
"ok": False,
|
|
}),
|
|
"status_code": requests.codes.internal_server_error,
|
|
}),
|
|
]
|
|
path = os.path.join(TEST_VAR_DIR, "apprise-test.gif")
|
|
attach = AppriseAttachment(path)
|
|
# We'll fail because of the bad 'file' response
|
|
assert (
|
|
obj.notify(
|
|
body="body",
|
|
title="title",
|
|
notify_type=NotifyType.INFO,
|
|
attach=attach,
|
|
)
|
|
is False
|
|
)
|
|
|
|
# Slack requests pay close attention to the response to determine
|
|
# if things go well... this is not a good JSON response:
|
|
request.content = "{"
|
|
mock_request.reset_mock()
|
|
mock_request.return_value = request
|
|
mock_request.side_effect = None
|
|
|
|
# As a result, we'll fail to send our notification
|
|
assert obj.send(body="test", attach=attach) is False
|
|
|
|
request.content = dumps({
|
|
"ok": False,
|
|
"message": "We failed",
|
|
})
|
|
|
|
# A response from Slack (even with a 200 response) still
|
|
# results in a failure:
|
|
assert obj.send(body="test", attach=attach) is False
|
|
|
|
# Handle exceptions reading our attachment from disk (should it happen)
|
|
mock_request.side_effect = OSError("Attachment Error")
|
|
mock_request.return_value = None
|
|
|
|
# We'll fail now because of an internal exception
|
|
assert obj.send(body="test") is False
|
|
|
|
|
|
@mock.patch("requests.request")
|
|
def test_plugin_slack_webhook_mode(mock_request):
|
|
"""NotifySlack() Webhook Mode Tests."""
|
|
|
|
# Prepare Mock
|
|
mock_request.return_value = requests.Request()
|
|
mock_request.return_value.status_code = requests.codes.ok
|
|
mock_request.return_value.content = b"ok"
|
|
mock_request.return_value.text = "ok"
|
|
|
|
# Initialize some generic (but valid) tokens
|
|
token_a = "A" * 9
|
|
token_b = "B" * 9
|
|
token_c = "c" * 24
|
|
|
|
# Support strings
|
|
channels = "chan1,#chan2,+BAK4K23G5,@user,,,"
|
|
|
|
obj = NotifySlack(
|
|
token_a=token_a, token_b=token_b, token_c=token_c, targets=channels
|
|
)
|
|
assert len(obj.channels) == 4
|
|
|
|
# This call includes an image with it's payload:
|
|
assert (
|
|
obj.notify(body="body", title="title", notify_type=NotifyType.INFO)
|
|
is True
|
|
)
|
|
|
|
# Missing first Token
|
|
with pytest.raises(TypeError):
|
|
NotifySlack(
|
|
token_a=None, token_b=token_b, token_c=token_c, targets=channels
|
|
)
|
|
|
|
# Test include_image
|
|
obj = NotifySlack(
|
|
token_a=token_a,
|
|
token_b=token_b,
|
|
token_c=token_c,
|
|
targets=channels,
|
|
include_image=True,
|
|
)
|
|
|
|
# This call includes an image with it's payload:
|
|
assert (
|
|
obj.notify(body="body", title="title", notify_type=NotifyType.INFO)
|
|
is True
|
|
)
|
|
|
|
|
|
@mock.patch("requests.request")
|
|
@mock.patch("requests.get")
|
|
def test_plugin_slack_send_by_email(mock_get, mock_request):
|
|
"""NotifySlack() Send by Email Tests."""
|
|
|
|
# Generate a (valid) bot token
|
|
token = "xoxb-1234-1234-abc124"
|
|
|
|
request = mock.Mock()
|
|
request.content = dumps(
|
|
{"ok": True, "message": "", "user": {"id": "ABCD1234"}}
|
|
)
|
|
request.status_code = requests.codes.ok
|
|
|
|
# Prepare Mock
|
|
mock_request.return_value = request
|
|
mock_get.return_value = request
|
|
|
|
# Variation Initializations
|
|
obj = NotifySlack(access_token=token, targets="user@gmail.com")
|
|
assert isinstance(obj, NotifySlack) is True
|
|
assert isinstance(obj.url(), str) is True
|
|
|
|
# No calls made yet
|
|
assert mock_request.call_count == 0
|
|
assert mock_get.call_count == 0
|
|
|
|
# Send our notification
|
|
assert (
|
|
obj.notify(body="body", title="title", notify_type=NotifyType.INFO)
|
|
is True
|
|
)
|
|
|
|
# 2 calls were made, one to perform an email lookup, the second
|
|
# was the notification itself
|
|
assert mock_get.call_count == 1
|
|
assert mock_request.call_count == 1
|
|
assert (
|
|
mock_get.call_args_list[0][0][0]
|
|
== "https://slack.com/api/users.lookupByEmail"
|
|
)
|
|
assert (
|
|
mock_request.call_args_list[0][0][1]
|
|
== "https://slack.com/api/chat.postMessage"
|
|
)
|
|
|
|
# Reset our mock object
|
|
mock_request.reset_mock()
|
|
mock_get.reset_mock()
|
|
|
|
# Prepare Mock
|
|
mock_request.return_value = request
|
|
mock_get.return_value = request
|
|
|
|
# Send our notification again (cached copy of user id associated with
|
|
# email is used)
|
|
assert (
|
|
obj.notify(body="body", title="title", notify_type=NotifyType.INFO)
|
|
is True
|
|
)
|
|
|
|
assert mock_get.call_count == 0
|
|
assert mock_request.call_count == 1
|
|
assert (
|
|
mock_request.call_args_list[0][0][1]
|
|
== "https://slack.com/api/chat.postMessage"
|
|
)
|
|
|
|
#
|
|
# Now test a case where we can't look up the valid email
|
|
#
|
|
request.content = dumps({
|
|
"ok": False,
|
|
"message": "",
|
|
})
|
|
|
|
# Reset our mock object
|
|
mock_request.reset_mock()
|
|
mock_get.reset_mock()
|
|
|
|
# Prepare Mock
|
|
mock_request.return_value = request
|
|
mock_get.return_value = request
|
|
|
|
# Variation Initializations
|
|
obj = NotifySlack(access_token=token, targets="user@gmail.com")
|
|
assert isinstance(obj, NotifySlack) is True
|
|
assert isinstance(obj.url(), str) is True
|
|
|
|
# No calls made yet
|
|
assert mock_request.call_count == 0
|
|
assert mock_get.call_count == 0
|
|
|
|
# Send our notification; it will fail because we failed to look up
|
|
# the user id
|
|
assert (
|
|
obj.notify(body="body", title="title", notify_type=NotifyType.INFO)
|
|
is False
|
|
)
|
|
|
|
# We would have failed to look up the email, therefore we wouldn't have
|
|
# even bothered to attempt to send the notification
|
|
assert mock_get.call_count == 1
|
|
assert mock_request.call_count == 0
|
|
assert (
|
|
mock_get.call_args_list[0][0][0]
|
|
== "https://slack.com/api/users.lookupByEmail"
|
|
)
|
|
|
|
#
|
|
# Now test a case where we have a poorly formatted JSON response
|
|
#
|
|
request.content = "}"
|
|
|
|
# Reset our mock object
|
|
mock_request.reset_mock()
|
|
mock_get.reset_mock()
|
|
|
|
# Prepare Mock
|
|
mock_request.return_value = request
|
|
mock_get.return_value = request
|
|
|
|
# Variation Initializations
|
|
obj = NotifySlack(access_token=token, targets="user@gmail.com")
|
|
assert isinstance(obj, NotifySlack) is True
|
|
assert isinstance(obj.url(), str) is True
|
|
|
|
# No calls made yet
|
|
assert mock_request.call_count == 0
|
|
assert mock_get.call_count == 0
|
|
|
|
# Send our notification; it will fail because we failed to look up
|
|
# the user id
|
|
assert (
|
|
obj.notify(body="body", title="title", notify_type=NotifyType.INFO)
|
|
is False
|
|
)
|
|
|
|
# We would have failed to look up the email, therefore we wouldn't have
|
|
# even bothered to attempt to send the notification
|
|
assert mock_get.call_count == 1
|
|
assert mock_request.call_count == 0
|
|
assert (
|
|
mock_get.call_args_list[0][0][0]
|
|
== "https://slack.com/api/users.lookupByEmail"
|
|
)
|
|
|
|
#
|
|
# Now test a case where we have a poorly formatted JSON response
|
|
#
|
|
request.content = "}"
|
|
|
|
# Reset our mock object
|
|
mock_request.reset_mock()
|
|
mock_get.reset_mock()
|
|
|
|
# Prepare Mock
|
|
mock_request.return_value = request
|
|
mock_get.return_value = request
|
|
|
|
# Variation Initializations
|
|
obj = NotifySlack(access_token=token, targets="user@gmail.com")
|
|
assert isinstance(obj, NotifySlack) is True
|
|
assert isinstance(obj.url(), str) is True
|
|
|
|
# No calls made yet
|
|
assert mock_request.call_count == 0
|
|
assert mock_get.call_count == 0
|
|
|
|
# Send our notification; it will fail because we failed to look up
|
|
# the user id
|
|
assert (
|
|
obj.notify(body="body", title="title", notify_type=NotifyType.INFO)
|
|
is False
|
|
)
|
|
|
|
# We would have failed to look up the email, therefore we wouldn't have
|
|
# even bothered to attempt to send the notification
|
|
assert mock_get.call_count == 1
|
|
assert mock_request.call_count == 0
|
|
assert (
|
|
mock_get.call_args_list[0][0][0]
|
|
== "https://slack.com/api/users.lookupByEmail"
|
|
)
|
|
|
|
#
|
|
# Now test a case where we throw an exception trying to perform the lookup
|
|
#
|
|
|
|
request.content = dumps(
|
|
{"ok": True, "message": "", "user": {"id": "ABCD1234"}}
|
|
)
|
|
# Create an unauthorized response
|
|
request.status_code = requests.codes.ok
|
|
|
|
# Reset our mock object
|
|
mock_request.reset_mock()
|
|
mock_get.reset_mock()
|
|
|
|
# Prepare Mock
|
|
mock_request.return_value = request
|
|
mock_get.side_effect = requests.ConnectionError(
|
|
0, "requests.ConnectionError() not handled"
|
|
)
|
|
|
|
# Variation Initializations
|
|
obj = NotifySlack(access_token=token, targets="user@gmail.com")
|
|
assert isinstance(obj, NotifySlack) is True
|
|
assert isinstance(obj.url(), str) is True
|
|
|
|
# No calls made yet
|
|
assert mock_request.call_count == 0
|
|
assert mock_get.call_count == 0
|
|
|
|
# Send our notification; it will fail because we failed to look up
|
|
# the user id
|
|
assert (
|
|
obj.notify(body="body", title="title", notify_type=NotifyType.INFO)
|
|
is False
|
|
)
|
|
|
|
# We would have failed to look up the email, therefore we wouldn't have
|
|
# even bothered to attempt to send the notification
|
|
assert mock_get.call_count == 1
|
|
assert mock_request.call_count == 0
|
|
assert (
|
|
mock_get.call_args_list[0][0][0]
|
|
== "https://slack.com/api/users.lookupByEmail"
|
|
)
|
|
|
|
|
|
@mock.patch("requests.request")
|
|
@mock.patch("requests.get")
|
|
def test_plugin_slack_markdown(mock_get, mock_request):
|
|
"""NotifySlack() Markdown tests."""
|
|
|
|
request = mock.Mock()
|
|
request.content = b"ok"
|
|
request.status_code = requests.codes.ok
|
|
|
|
# Prepare Mock
|
|
mock_request.return_value = request
|
|
mock_get.return_value = request
|
|
|
|
# Variation Initializations
|
|
aobj = Apprise()
|
|
assert aobj.add(
|
|
"slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#channel"
|
|
)
|
|
|
|
body = cleandoc("""
|
|
Here is a <https://slack.com|Slack Link> we want to support as part of it's
|
|
markdown.
|
|
|
|
This one has arguments we want to preserve:
|
|
<https://slack.com?arg=val&arg2=val2|Slack Link>.
|
|
We also want to be able to support <https://slack.com> links without the
|
|
description.
|
|
|
|
Channel Testing
|
|
<!channelA>
|
|
<!channelA|Description>
|
|
|
|
User ID Testing
|
|
<@U1ZQL9N3Y>
|
|
<@U1ZQL9N3Y|heheh>
|
|
""")
|
|
|
|
# Send our notification
|
|
assert aobj.notify(body=body, title="title", notify_type=NotifyType.INFO)
|
|
|
|
# We would have failed to look up the email, therefore we wouldn't have
|
|
# even bothered to attempt to send the notification
|
|
assert mock_get.call_count == 0
|
|
assert mock_request.call_count == 1
|
|
assert (
|
|
mock_request.call_args_list[0][0][1]
|
|
== "https://hooks.slack.com/services/T1JJ3T3L2/A1BRTD4JD/"
|
|
"TIiajkdnlazkcOXrIdevi7FQ"
|
|
)
|
|
|
|
data = loads(mock_request.call_args_list[0][1]["data"])
|
|
assert (
|
|
data["attachments"][0]["text"]
|
|
== "Here is a <https://slack.com|Slack Link> we want to support as"
|
|
" part "
|
|
"of it's\nmarkdown.\n\nThis one has arguments we want to preserve:"
|
|
"\n <https://slack.com?arg=val&arg2=val2|Slack Link>.\n"
|
|
"We also want to be able to support <https://slack.com> "
|
|
"links without the\ndescription."
|
|
"\n\nChannel Testing\n<!channelA>\n<!channelA|Description>\n\n"
|
|
"User ID Testing\n<@U1ZQL9N3Y>\n<@U1ZQL9N3Y|heheh>"
|
|
)
|
|
|
|
|
|
@mock.patch("requests.request")
|
|
def test_plugin_slack_single_thread_reply(mock_request):
|
|
"""NotifySlack() Send Notification as a Reply."""
|
|
|
|
# Generate a (valid) bot token
|
|
token = "xoxb-1234-1234-abc124"
|
|
thread_id = 100
|
|
request = mock.Mock()
|
|
request.content = dumps(
|
|
{"ok": True, "message": "", "user": {"id": "ABCD1234"}}
|
|
)
|
|
request.status_code = requests.codes.ok
|
|
|
|
# Prepare Mock
|
|
mock_request.return_value = request
|
|
|
|
# Variation Initializations
|
|
obj = NotifySlack(access_token=token, targets=[f"#general:{thread_id}"])
|
|
assert isinstance(obj, NotifySlack) is True
|
|
assert isinstance(obj.url(), str) is True
|
|
|
|
# No calls made yet
|
|
assert mock_request.call_count == 0
|
|
|
|
# Send our notification
|
|
assert (
|
|
obj.notify(body="body", title="title", notify_type=NotifyType.INFO)
|
|
is True
|
|
)
|
|
|
|
# Post was made
|
|
assert mock_request.call_count == 1
|
|
assert (
|
|
mock_request.call_args_list[0][0][1]
|
|
== "https://slack.com/api/chat.postMessage"
|
|
)
|
|
assert loads(mock_request.call_args_list[0][1]["data"]).get(
|
|
"thread_ts"
|
|
) == str(thread_id)
|
|
|
|
|
|
@mock.patch("requests.request")
|
|
def test_plugin_slack_multiple_thread_reply(mock_request):
|
|
"""NotifySlack() Send Notification to multiple channels as Reply."""
|
|
|
|
# Generate a (valid) bot token
|
|
token = "xoxb-1234-1234-abc124"
|
|
thread_id_1, thread_id_2 = 100, 200
|
|
request = mock.Mock()
|
|
request.content = dumps(
|
|
{"ok": True, "message": "", "user": {"id": "ABCD1234"}}
|
|
)
|
|
request.status_code = requests.codes.ok
|
|
|
|
# Prepare Mock
|
|
mock_request.return_value = request
|
|
|
|
# Variation Initializations
|
|
obj = NotifySlack(
|
|
access_token=token,
|
|
targets=[f"#general:{thread_id_1}", f"#other:{thread_id_2}"],
|
|
)
|
|
assert isinstance(obj, NotifySlack) is True
|
|
assert isinstance(obj.url(), str) is True
|
|
|
|
# No calls made yet
|
|
assert mock_request.call_count == 0
|
|
|
|
# Send our notification
|
|
assert (
|
|
obj.notify(body="body", title="title", notify_type=NotifyType.INFO)
|
|
is True
|
|
)
|
|
|
|
# Post was made
|
|
assert mock_request.call_count == 2
|
|
assert (
|
|
mock_request.call_args_list[0][0][1]
|
|
== "https://slack.com/api/chat.postMessage"
|
|
)
|
|
assert loads(mock_request.call_args_list[0][1]["data"]).get(
|
|
"thread_ts"
|
|
) == str(thread_id_1)
|
|
assert loads(mock_request.call_args_list[1][1]["data"]).get(
|
|
"thread_ts"
|
|
) == str(thread_id_2)
|
|
|
|
|
|
@mock.patch("requests.request")
|
|
def test_plugin_slack_file_upload_success(mock_request):
|
|
"""Test Slack BOT attachment upload success path."""
|
|
|
|
token = "xoxb-1234-1234-abc124"
|
|
path = os.path.join(TEST_VAR_DIR, "apprise-test.gif")
|
|
attach = AppriseAttachment(path)
|
|
|
|
# Simulate all successful Slack API responses
|
|
mock_request.side_effect = [
|
|
mock.Mock(**{
|
|
"content": dumps({
|
|
"ok": True,
|
|
"channel": "C123456",
|
|
}),
|
|
"status_code": requests.codes.ok,
|
|
}),
|
|
mock.Mock(**{
|
|
"content": dumps({
|
|
"ok": True,
|
|
"upload_url": "https://files.slack.com/upload/v1/ABC123",
|
|
"file_id": "F123ABC456",
|
|
}),
|
|
"status_code": requests.codes.ok,
|
|
}),
|
|
mock.Mock(**{
|
|
"content": b"OK - 123",
|
|
"status_code": requests.codes.ok,
|
|
}),
|
|
mock.Mock(**{
|
|
"content": dumps({
|
|
"ok": True,
|
|
"files": [{"id": "F123ABC456", "title": "apprise-test"}],
|
|
}),
|
|
"status_code": requests.codes.ok,
|
|
}),
|
|
]
|
|
|
|
obj = NotifySlack(access_token=token, targets=["#general"])
|
|
assert obj.notify(
|
|
body="Success path test",
|
|
title="Slack Upload OK",
|
|
notify_type=NotifyType.INFO,
|
|
attach=attach,
|
|
) is True
|
|
|
|
|
|
@mock.patch("requests.request")
|
|
def test_plugin_slack_file_upload_fails_missing_files(mock_request):
|
|
"""Test that file upload fails when 'files' is missing or empty."""
|
|
|
|
token = "xoxb-1234-1234-abc124"
|
|
path = os.path.join(TEST_VAR_DIR, "apprise-test.gif")
|
|
attach = AppriseAttachment(path)
|
|
|
|
# Mock sequence:
|
|
# 1. chat.postMessage returns valid channel
|
|
# 2. files.getUploadURLExternal returns file_id and upload_url
|
|
# 3. Upload returns 'OK'
|
|
# 4. files.completeUploadExternal returns missing/empty 'files'
|
|
|
|
mock_request.side_effect = [
|
|
mock.Mock(**{
|
|
"content": dumps({
|
|
"ok": True,
|
|
"channel": "C555555",
|
|
}),
|
|
"status_code": requests.codes.ok,
|
|
}),
|
|
mock.Mock(**{
|
|
"content": dumps({
|
|
"ok": True,
|
|
"upload_url": "https://files.slack.com/upload/v1/X99999",
|
|
"file_id": "F999XYZ888",
|
|
}),
|
|
"status_code": requests.codes.ok,
|
|
}),
|
|
mock.Mock(**{
|
|
"content": b"OK - 2048",
|
|
"status_code": requests.codes.ok,
|
|
}),
|
|
# <== This response will trigger the error condition
|
|
mock.Mock(**{
|
|
"content": dumps({
|
|
"ok": True,
|
|
"files": [],
|
|
}),
|
|
"status_code": requests.codes.ok,
|
|
}),
|
|
]
|
|
|
|
obj = NotifySlack(access_token=token, targets=["#fail-channel"])
|
|
result = obj.notify(
|
|
body="This should trigger a failed file upload",
|
|
title="Trigger failure",
|
|
notify_type=NotifyType.INFO,
|
|
attach=attach,
|
|
)
|
|
|
|
assert result is False
|