Skip to content

Commit 5dca527

Browse files
Add Pushover Notification Support and Unit Tests (#286)
* Update config_parser.py * Update notify.py * Update README.md * Update config.yaml * Update config_parser.py * Update config_parser.py * Update notify.py * Update test_notify.py * Update test_config_parser.py * Update test_config.yaml * Update test_config_parser.py * Update test_notify.py * Update test_notify.py * Update test_notify.py * Update notify.py * Update notify.py --------- Co-authored-by: Mandar Patil <mandarons@pm.me>
1 parent 56e033d commit 5dca527

File tree

7 files changed

+173
-0
lines changed

7 files changed

+173
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ app:
8383
telegram:
8484
# bot_token: <your Telegram bot token>
8585
# chat_id: <your Telegram user or chat ID>
86+
pushover:
87+
# user_key: <your Pushover user key>
88+
# api_token: <your Pushover api token>
8689
smtp:
8790
## If you want to receive email notifications about expired/missing 2FA credentials then uncomment
8891
# email: "user@test.com"

config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ app:
1717
telegram:
1818
# bot_token: <your Telegram bot token>
1919
# chat_id: <your Telegram user or chat ID>
20+
pushover:
21+
# user_key: <your Pushover user key>
22+
# api_token: <your Pushover api token>
2023
smtp:
2124
# If you want to receive email notifications about expired/missing 2FA credentials then uncomment
2225
# email: "sender@test.com"

src/config_parser.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,25 @@ def get_discord_username(config):
409409
else:
410410
username = get_config_value(config=config, config_path=config_path)
411411
return username
412+
413+
# Get pushover user key
414+
def get_pushover_user_key(config):
415+
"""Return Pushover user key from config."""
416+
user_key = None
417+
config_path = ["app", "pushover", "user_key"]
418+
if not traverse_config_path(config=config, config_path=config_path):
419+
LOGGER.warning(f"Warning: user_key is not found in {config_path_to_string(config_path)}.")
420+
else:
421+
user_key = get_config_value(config=config, config_path=config_path)
422+
return user_key
423+
424+
# Get pushover api token
425+
def get_pushover_api_token(config):
426+
"""Return Pushover API token from config."""
427+
api_token = None
428+
config_path = ["app", "pushover", "api_token"]
429+
if not traverse_config_path(config=config, config_path=config_path):
430+
LOGGER.warning(f"Warning: api_token is not found in {config_path_to_string(config_path)}.")
431+
else:
432+
api_token = get_config_value(config=config, config_path=config_path)
433+
return api_token

src/notify.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,33 @@ def notify_discord(config, message, last_send=None, dry_run=False):
7777
LOGGER.warning("Not sending 2FA notification because Discord is not configured.")
7878
return sent_on
7979

80+
def notify_pushover(config, message, last_send=None, dry_run=False):
81+
"""Send Pushover notification."""
82+
sent_on = None
83+
user_key = config_parser.get_pushover_user_key(config=config)
84+
api_token = config_parser.get_pushover_api_token(config=config)
85+
86+
if last_send and last_send > datetime.datetime.now() - datetime.timedelta(hours=24):
87+
LOGGER.info("Throttling Pushover to once a day")
88+
sent_on = last_send
89+
elif user_key and api_token:
90+
sent_on = datetime.datetime.now()
91+
if not dry_run:
92+
if not post_message_to_pushover(api_token, user_key, message):
93+
sent_on = None
94+
else:
95+
LOGGER.warning("Not sending 2FA notification because Pushover is not configured.")
96+
return sent_on
97+
98+
def post_message_to_pushover(api_token, user_key, message):
99+
"""Post message to Pushover API."""
100+
url = "https://api.pushover.net/1/messages.json"
101+
data = {"token": api_token, "user": user_key, "message": message}
102+
response = requests.post(url, data=data, timeout=10)
103+
if response.status_code == 200:
104+
return True
105+
LOGGER.error(f"Failed to send Pushover notification. Response: {response.text}")
106+
return False
80107

81108
def send(config, username, last_send=None, dry_run=False, region="global"):
82109
"""Send notifications."""
@@ -91,6 +118,7 @@ def send(config, username, last_send=None, dry_run=False, region="global"):
91118
subject = f"icloud-docker: Two step authentication is required for {username}"
92119
notify_telegram(config=config, message=message, last_send=last_send, dry_run=dry_run)
93120
notify_discord(config=config, message=message, last_send=last_send, dry_run=dry_run)
121+
notify_pushover(config=config, message=message, last_send=last_send, dry_run=dry_run)
94122
email = config_parser.get_smtp_email(config=config)
95123
to_email = config_parser.get_smtp_to_email(config=config)
96124
host = config_parser.get_smtp_host(config=config)

tests/data/test_config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ app:
1414
discord:
1515
# webhook_url: <server webhook>
1616
# username: icloud-docker
17+
pushover:
18+
# user_key: <your Pushover user key>
19+
# api_token: <your Pushover api token>
1720
smtp:
1821
# If you want to recieve email notifications about expired/missing 2FA credentials then uncomment
1922
# email: sender@test.com

tests/test_config_parser.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,3 +503,29 @@ def test_get_discord_username(self):
503503
def test_get_discord_username_none_config(self):
504504
"""None config."""
505505
self.assertIsNone(config_parser.get_discord_username(config=None))
506+
507+
def test_get_pushover_user_key(self):
508+
"""Test for getting Pushover user key."""
509+
config = read_config(config_path=tests.CONFIG_PATH)
510+
config["app"]["pushover"] = {"user_key": "pushover_user_key"}
511+
self.assertEqual(
512+
config["app"]["pushover"]["user_key"],
513+
config_parser.get_pushover_user_key(config=config),
514+
)
515+
516+
def test_get_pushover_user_key_none_config(self):
517+
"""None config for Pushover user key."""
518+
self.assertIsNone(config_parser.get_pushover_user_key(config=None))
519+
520+
def test_get_pushover_api_token(self):
521+
"""Test for getting Pushover API token."""
522+
config = read_config(config_path=tests.CONFIG_PATH)
523+
config["app"]["pushover"] = {"api_token": "pushover_api_token"}
524+
self.assertEqual(
525+
config["app"]["pushover"]["api_token"],
526+
config_parser.get_pushover_api_token(config=config),
527+
)
528+
529+
def test_get_pushover_api_token_none_config(self):
530+
"""None config for Pushover API token."""
531+
self.assertIsNone(config_parser.get_pushover_api_token(config=None))

tests/test_notify.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from src.email_message import EmailMessage as Message
99
from src.notify import (
1010
notify_discord,
11+
notify_pushover,
1112
notify_telegram,
1213
post_message_to_discord,
14+
post_message_to_pushover,
1315
post_message_to_telegram,
1416
)
1517

@@ -29,6 +31,7 @@ def setUp(self) -> None:
2931
"password": "password",
3032
},
3133
"telegram": {"bot_token": "bot_token", "chat_id": "chat_id"},
34+
"pushover": {"user_key": "pushover_user_key", "api_token": "pushover_api_token"},
3235
},
3336
}
3437
self.message_body = "message body"
@@ -332,3 +335,88 @@ def test_post_message_to_discord_fail(self):
332335
data={"content": message, "username": "username"},
333336
timeout=10,
334337
)
338+
339+
def test_notify_pushover_success(self):
340+
"""Test for successful Pushover notification."""
341+
with patch("src.notify.post_message_to_pushover") as post_message_mock:
342+
notify_pushover(self.config, self.message_body, None, False)
343+
344+
# Verify that post_message_to_pushover is called with the correct arguments
345+
post_message_mock.assert_called_once_with(
346+
self.config["app"]["pushover"]["api_token"],
347+
self.config["app"]["pushover"]["user_key"],
348+
self.message_body,
349+
)
350+
351+
def test_notify_pushover_fail(self):
352+
"""Test for failed Pushover notification."""
353+
with patch("src.notify.post_message_to_pushover") as post_message_mock:
354+
post_message_mock.return_value = False
355+
notify_pushover(self.config, self.message_body, None, False)
356+
357+
# Verify that post_message_to_pushover is called with the correct arguments
358+
post_message_mock.assert_called_once_with(
359+
self.config["app"]["pushover"]["api_token"],
360+
self.config["app"]["pushover"]["user_key"],
361+
self.message_body,
362+
)
363+
364+
def test_notify_pushover_throttling(self):
365+
"""Test for throttled Pushover notification."""
366+
last_send = datetime.datetime.now() - datetime.timedelta(hours=2)
367+
dry_run = False
368+
369+
with patch("src.notify.post_message_to_pushover") as post_message_mock:
370+
notify_pushover(self.config, self.message_body, last_send, dry_run)
371+
372+
# Verify that post_message_to_pushover is not called when throttled
373+
post_message_mock.assert_not_called()
374+
375+
def test_notify_pushover_dry_run(self):
376+
"""Test for dry run mode in Pushover notification."""
377+
last_send = datetime.datetime.now()
378+
dry_run = True
379+
380+
with patch("src.notify.post_message_to_pushover") as post_message_mock:
381+
notify_pushover(self.config, self.message_body, last_send, dry_run)
382+
383+
# Verify that post_message_to_pushover is not called in dry run mode
384+
post_message_mock.assert_not_called()
385+
386+
def test_notify_pushover_no_config(self):
387+
"""Test for missing Pushover configuration."""
388+
config = {}
389+
last_send = None
390+
dry_run = False
391+
392+
with patch("src.notify.post_message_to_pushover") as post_message_mock:
393+
notify_pushover(config, self.message_body, last_send, dry_run)
394+
395+
# Verify that post_message_to_pushover is not called when Pushover configuration is missing
396+
post_message_mock.assert_not_called()
397+
398+
def test_post_message_to_pushover(self):
399+
"""Test for successful post to Pushover."""
400+
with patch("requests.post") as post_mock:
401+
post_mock.return_value.status_code = 200
402+
post_message_to_pushover("pushover_api_token", "pushover_user_key", "message")
403+
404+
# Verify that post is called with the correct arguments
405+
post_mock.assert_called_once_with(
406+
"https://api.pushover.net/1/messages.json",
407+
data={"token": "pushover_api_token", "user": "pushover_user_key", "message": "message"},
408+
timeout=10,
409+
)
410+
411+
def test_post_message_to_pushover_fail(self):
412+
"""Test for failed post to Pushover."""
413+
with patch("requests.post") as post_mock:
414+
post_mock.return_value.status_code = 400
415+
post_message_to_pushover("pushover_api_token", "pushover_user_key", "message")
416+
417+
# Verify that post is called with the correct arguments
418+
post_mock.assert_called_once_with(
419+
"https://api.pushover.net/1/messages.json",
420+
data={"token": "pushover_api_token", "user": "pushover_user_key", "message": "message"},
421+
timeout=10,
422+
)

0 commit comments

Comments
 (0)