Explorar o código

Add a Pushover monitoring hook.

Merge pull request #86 from tony1661/pushover-branch.
Dan Helfman hai 6 meses
pai
achega
9807549f88

+ 1 - 0
README.md

@@ -66,6 +66,7 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
 <a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
 <a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
 <a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
 <a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
 <a href="https://www.pagerduty.com/"><img src="docs/static/pagerduty.png" alt="PagerDuty" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
 <a href="https://www.pagerduty.com/"><img src="docs/static/pagerduty.png" alt="PagerDuty" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
+<a href="https://www.pushover.net/"><img src="docs/static/pushover.png" alt="Pushover" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
 <a href="https://ntfy.sh/"><img src="docs/static/ntfy.png" alt="ntfy" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
 <a href="https://ntfy.sh/"><img src="docs/static/ntfy.png" alt="ntfy" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
 <a href="https://grafana.com/oss/loki/"><img src="docs/static/loki.png" alt="Loki" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
 <a href="https://grafana.com/oss/loki/"><img src="docs/static/loki.png" alt="Loki" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
 <a href="https://github.com/caronc/apprise/wiki"><img src="docs/static/apprise.png" alt="Apprise" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
 <a href="https://github.com/caronc/apprise/wiki"><img src="docs/static/apprise.png" alt="Apprise" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>

+ 259 - 0
borgmatic/config/schema.yaml

@@ -1626,6 +1626,265 @@ properties:
                 example:
                 example:
                     - start
                     - start
                     - finish
                     - finish
+    pushover:
+        type: object
+        required: ['token', 'user']
+        additionalProperties: false
+        properties:
+            token:
+                type: string
+                description: |
+                    Your application's API token.
+                example: 7ms6TXHpTokTou2P6x4SodDeentHRa
+            user:
+                type: string
+                description: |
+                    Your user/group key (or that of your target user), viewable 
+                    when logged into your dashboard: often referred to as 
+                    USER_KEY in Pushover documentation and code examples.
+                example: hwRwoWsXMBWwgrSecfa9EfPey55WSN
+            start:
+                type: object
+                properties:
+                    message:
+                        type: string
+                        description: |
+                            Message to be sent to the user or group. If omitted
+                            the default is the name of the state.
+                        example: A backup job has started.
+                    priority:
+                        type: integer
+                        description: |
+                            A value of -2, -1, 0 (default), 1 or 2 that 
+                            indicates the message priority.
+                        example: 0
+                    expire:
+                        type: integer
+                        description: |
+                            How many seconds your notification will continue 
+                            to be retried (every retry seconds). Defaults to
+                            600. This settings only applies to priority 2
+                            notifications.
+                        example: 600
+                    retry:
+                        type: integer
+                        description: |
+                            The retry parameter specifies how often 
+                            (in seconds) the Pushover servers will send the 
+                            same notification to the user. Defaults to 30. This
+                            settings only applies to priority 2 notifications.
+                        example: 30
+                    device:
+                        type: string
+                        description: |
+                            The name of one of your devices to send just to 
+                            that device instead of all devices.
+                        example: pixel8
+                    html:
+                        type: boolean
+                        description: |
+                            Set to True to enable HTML parsing of the message.
+                            Set to False for plain text.
+                        example: True
+                    sound:
+                        type: string
+                        description: |
+                            The name of a supported sound to override your 
+                            default sound choice. All options can be found 
+                            here: https://pushover.net/api#sounds 
+                        example: bike
+                    title:
+                        type: string
+                        description: |
+                            Your message's title, otherwise your app's name is 
+                            used.
+                        example: A backup job has started.
+                    ttl:
+                        type: integer
+                        description: |
+                            The number of seconds that the message will live, 
+                            before being deleted automatically. The ttl 
+                            parameter is ignored for messages with a priority.
+                            value of 2.
+                        example: 3600
+                    url:
+                        type: string
+                        description: |
+                            A supplementary URL to show with your message.
+                        example: https://pushover.net/apps/xxxxx-borgbackup
+                    url_title:
+                        type: string
+                        description: |
+                            A title for the URL specified as the url parameter,
+                            otherwise just the URL is shown.
+                        example: Pushover Link
+            finish:
+                type: object
+                type: object
+                properties:
+                    message:
+                        type: string
+                        description: |
+                            Message to be sent to the user or group. If omitted
+                            the default is the name of the state.
+                        example: A backup job has finished.
+                    priority:
+                        type: integer
+                        description: |
+                            A value of -2, -1, 0 (default), 1 or 2 that 
+                            indicates the message priority.
+                        example: 0
+                    expire:
+                        type: integer
+                        description: |
+                            How many seconds your notification will continue 
+                            to be retried (every retry seconds). Defaults to
+                            600. This settings only applies to priority 2
+                            notifications.
+                        example: 600
+                    retry:
+                        type: integer
+                        description: |
+                            The retry parameter specifies how often 
+                            (in seconds) the Pushover servers will send the 
+                            same notification to the user. Defaults to 30. This
+                            settings only applies to priority 2 notifications.
+                        example: 30
+                    device:
+                        type: string
+                        description: |
+                            The name of one of your devices to send just to 
+                            that device instead of all devices.
+                        example: pixel8
+                    html:
+                        type: boolean
+                        description: |
+                            Set to True to enable HTML parsing of the message.
+                            Set to False for plain text.
+                        example: True
+                    sound:
+                        type: string
+                        description: |
+                            The name of a supported sound to override your 
+                            default sound choice. All options can be found 
+                            here: https://pushover.net/api#sounds 
+                        example: bike
+                    title:
+                        type: string
+                        description: |
+                            Your message's title, otherwise your app's name is 
+                            used.
+                        example: A backup job has started.
+                    ttl:
+                        type: integer
+                        description: |
+                            The number of seconds that the message will live, 
+                            before being deleted automatically. The ttl 
+                            parameter is ignored for messages with a priority.
+                            value of 2.
+                        example: 3600
+                    url:
+                        type: string
+                        description: |
+                            A supplementary URL to show with your message.
+                        example: https://pushover.net/apps/xxxxx-borgbackup
+                    url_title:
+                        type: string
+                        description: |
+                            A title for the URL specified as the url parameter,
+                            otherwise just the URL is shown.
+                        example: Pushover Link
+            fail:
+                type: object
+                properties:
+                    message:
+                        type: string
+                        description: |
+                            Message to be sent to the user or group. If omitted
+                            the default is the name of the state.
+                        example: A backup job has failed.
+                    priority:
+                        type: integer
+                        description: |
+                            A value of -2, -1, 0 (default), 1 or 2 that 
+                            indicates the message priority.
+                        example: 0
+                    expire:
+                        type: integer
+                        description: |
+                            How many seconds your notification will continue 
+                            to be retried (every retry seconds). Defaults to
+                            600. This settings only applies to priority 2
+                            notifications.
+                        example: 600
+                    retry:
+                        type: integer
+                        description: |
+                            The retry parameter specifies how often 
+                            (in seconds) the Pushover servers will send the 
+                            same notification to the user. Defaults to 30. This
+                            settings only applies to priority 2 notifications.
+                        example: 30
+                    device:
+                        type: string
+                        description: |
+                            The name of one of your devices to send just to 
+                            that device instead of all devices.
+                        example: pixel8
+                    html:
+                        type: boolean
+                        description: |
+                            Set to True to enable HTML parsing of the message.
+                            Set to False for plain text.
+                        example: True
+                    sound:
+                        type: string
+                        description: |
+                            The name of a supported sound to override your 
+                            default sound choice. All options can be found 
+                            here: https://pushover.net/api#sounds 
+                        example: bike
+                    title:
+                        type: string
+                        description: |
+                            Your message's title, otherwise your app's name is 
+                            used.
+                        example: A backup job has started.
+                    ttl:
+                        type: integer
+                        description: |
+                            The number of seconds that the message will live, 
+                            before being deleted automatically. The ttl 
+                            parameter is ignored for messages with a priority.
+                            value of 2.
+                        example: 3600
+                    url:
+                        type: string
+                        description: |
+                            A supplementary URL to show with your message.
+                        example: https://pushover.net/apps/xxxxx-borgbackup
+                    url_title:
+                        type: string
+                        description: |
+                            A title for the URL specified as the url parameter,
+                            otherwise just the URL is shown.
+                        example: Pushover Link
+            states:
+                type: array
+                items:
+                    type: string
+                    enum:
+                        - start
+                        - finish
+                        - fail
+                    uniqueItems: true
+                description: |
+                    List of one or more monitoring states to ping for: "start",
+                    "finish", and/or "fail". Defaults to pinging for failure
+                    only.
+                example:
+                    - start
+                    - finish
     zabbix:
     zabbix:
         type: object
         type: object
         additionalProperties: false
         additionalProperties: false

+ 2 - 0
borgmatic/hooks/dispatch.py

@@ -12,6 +12,7 @@ from borgmatic.hooks import (
     ntfy,
     ntfy,
     pagerduty,
     pagerduty,
     postgresql,
     postgresql,
+    pushover,
     sqlite,
     sqlite,
     uptimekuma,
     uptimekuma,
     zabbix,
     zabbix,
@@ -31,6 +32,7 @@ HOOK_NAME_TO_MODULE = {
     'ntfy': ntfy,
     'ntfy': ntfy,
     'pagerduty': pagerduty,
     'pagerduty': pagerduty,
     'postgresql_databases': postgresql,
     'postgresql_databases': postgresql,
+    'pushover': pushover,
     'sqlite_databases': sqlite,
     'sqlite_databases': sqlite,
     'uptime_kuma': uptimekuma,
     'uptime_kuma': uptimekuma,
     'zabbix': zabbix,
     'zabbix': zabbix,

+ 1 - 0
borgmatic/hooks/monitor.py

@@ -8,6 +8,7 @@ MONITOR_HOOK_NAMES = (
     'loki',
     'loki',
     'ntfy',
     'ntfy',
     'pagerduty',
     'pagerduty',
+    'pushover',
     'uptime_kuma',
     'uptime_kuma',
     'zabbix',
     'zabbix',
 )
 )

+ 86 - 0
borgmatic/hooks/pushover.py

@@ -0,0 +1,86 @@
+import logging
+
+import requests
+
+logger = logging.getLogger(__name__)
+
+
+EMERGENCY_PRIORITY = 2
+
+
+def initialize_monitor(
+    ping_url, config, config_filename, monitoring_log_level, dry_run
+):  # pragma: no cover
+    '''
+    No initialization is necessary for this monitor.
+    '''
+    pass
+
+
+def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
+    '''
+    Post a message to the configured Pushover application.
+    If this is a dry run, then don't actually update anything.
+    '''
+
+    run_states = hook_config.get('states', ['fail'])
+
+    if state.name.lower() not in run_states:
+        return
+
+    dry_run_label = ' (dry run; not actually updating)' if dry_run else ''
+
+    state_config = hook_config.get(state.name.lower(), {})
+
+    token = hook_config.get('token')
+    user = hook_config.get('user')
+
+    logger.info(f'{config_filename}: Updating Pushover{dry_run_label}')
+
+    if state_config.get('priority') == EMERGENCY_PRIORITY:
+        if 'expire' not in state_config:
+            logger.info(f'{config_filename}: Setting expire to default (10min).')
+            state_config['expire'] = 600
+        if 'retry' not in state_config:
+            logger.info(f'{config_filename}: Setting retry to default (30sec).')
+            state_config['retry'] = 30
+    else:
+        if 'expire' in state_config or 'retry' in state_config:
+            raise ValueError(
+                'The configuration parameters retry and expire should not be set when priority is not equal to 2. Please remove them from the configuration.'
+            )
+
+    state_config = {
+        key: (int(value) if key in 'html' else value) for key, value in state_config.items()
+    }
+
+    data = dict(
+        {
+            'token': token,
+            'user': user,
+            'message': state.name.lower(),  # default to state name. Can be overwritten in state_config loop below.
+        },
+        **state_config,
+    )
+
+    if not dry_run:
+        logging.getLogger('urllib3').setLevel(logging.ERROR)
+        try:
+            response = requests.post(
+                'https://api.pushover.net/1/messages.json',
+                headers={'Content-type': 'application/x-www-form-urlencoded'},
+                data=data,
+            )
+            if not response.ok:
+                response.raise_for_status()
+        except requests.exceptions.RequestException as error:
+            logger.warning(f'{config_filename}: Pushover error: {error}')
+
+
+def destroy_monitor(
+    ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run
+):  # pragma: no cover
+    '''
+    No destruction is necessary for this monitor.
+    '''
+    pass

+ 71 - 0
docs/how-to/monitor-your-backups.md

@@ -46,6 +46,7 @@ them as backups happen:
  * [Healthchecks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
  * [Healthchecks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
  * [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook)
  * [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook)
  * [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
  * [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
+ * [Pushover](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pushover-hook)
  * [Uptime Kuma](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook)
  * [Uptime Kuma](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook)
  * [Zabbix](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook)
  * [Zabbix](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook)
 
 
@@ -290,6 +291,76 @@ If you have any issues with the integration, [please contact
 us](https://torsion.org/borgmatic/#support-and-contributing).
 us](https://torsion.org/borgmatic/#support-and-contributing).
 
 
 
 
+## Pushover hook
+
+<span class="minilink minilink-addedin">New in version 1.9.2</span>
+[Pushover](https://pushover.net) makes it easy to get real-time notifications 
+on your Android, iPhone, iPad, and Desktop (Android Wear and Apple Watch, 
+too!).
+
+First, create a Pushover account and login on your mobile device. Create an
+Application in your Pushover dashboard.
+
+Then, configure borgmatic with your user's unique "User Key" found in your 
+Pushover dashboard and the unique "API Token" from the created Application.
+
+Here's a basic example:
+
+
+```yaml
+pushover:
+    token: 7ms6TXHpTokTou2P6x4SodDeentHRa
+    user: hwRwoWsXMBWwgrSecfa9EfPey55WSN
+```
+
+
+With this configuration, borgmatic creates a Pushover event for your service
+whenever borgmatic fails, but only when any of the `create`, `prune`, `compact`,
+or `check` actions are run. Note that borgmatic does not contact Pushover
+when a backup starts or when it ends without error by default.
+
+You can configure Pushover to have custom parameters declared for borgmatic's
+`start`, `fail` and `finish` hooks states.
+
+Here's a more advanced example:
+
+
+```yaml
+pushover:
+    token: 7ms6TXHpTokTou2P6x4SodDeentHRa
+    user: hwRwoWsXMBWwgrSecfa9EfPey55WSN
+    start:
+        message: "Backup <b>Started</b>"
+        priority: -2
+        title: "Backup Started"
+        html: True
+        ttl: 10  # Message will be deleted after 10 seconds.
+    fail:
+        message: "Backup <font color='#ff6961'>Failed</font>"
+        priority: 2  # Requests acknowledgement for messages.
+        expire: 1200  # Used only for priority 2. Default is 1200 seconds.
+        retry: 30  # Used only for priority 2. Default is 30 seconds.
+        device: "pixel8"
+        title: "Backup Failed"
+        html: True
+        sound: "siren"
+        url: "https://ticketing-system.example.com/login"
+        url_title: "Login to ticketing system"
+    finish:
+        message: "Backup <font color='#77dd77'>Finished</font>"
+        priority: 0
+        title: "Backup Finished"
+        html: True
+        ttl: 60
+        url: "https://ticketing-system.example.com/login"
+        url_title: "Login to ticketing system"
+    states:
+        - start
+        - finish
+        - fail
+```
+
+
 ## ntfy hook
 ## ntfy hook
 
 
 <span class="minilink minilink-addedin">New in version 1.6.3</span>
 <span class="minilink minilink-addedin">New in version 1.6.3</span>

BIN=BIN
docs/static/pushover.png


+ 537 - 0
tests/unit/hooks/test_pushover.py

@@ -0,0 +1,537 @@
+import pytest
+from flexmock import flexmock
+
+import borgmatic.hooks.monitor
+from borgmatic.hooks import pushover as module
+
+
+def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_send_to_pushover():
+    '''
+    This test should be the minimum working configuration. The "message"
+    should be auto populated with the default value which is the state name.
+    '''
+    hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'fail',
+        },
+    ).and_return(flexmock(ok=True)).once()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.FAIL,
+        monitoring_log_level=1,
+        dry_run=False,
+    )
+
+
+def test_ping_monitor_config_with_minimum_config_start_state_backup_not_send_to_pushover_exit_early():
+    '''
+    This test should exit early since the hook config does not specify the
+    'start' state. Only the 'fail' state is enabled by default.
+    '''
+    hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').never()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.START,
+        monitoring_log_level=1,
+        dry_run=False,
+    )
+
+
+def test_ping_monitor_start_state_backup_default_message_successfully_send_to_pushover():
+    '''
+    This test should send a notification to Pushover on backup start
+    since the state has been configured. It should default to sending
+    the name of the state as the 'message' since it is not
+    explicitly declared in the state config.
+    '''
+    hook_config = {
+        'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+        'user': '983hfe0of902lkjfa2amanfgui',
+        'states': {'start', 'fail', 'finish'},
+    }
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'start',
+        },
+    ).and_return(flexmock(ok=True)).once()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.START,
+        monitoring_log_level=1,
+        dry_run=False,
+    )
+
+
+def test_ping_monitor_start_state_backup_custom_message_successfully_send_to_pushover():
+    '''
+    This test should send a notification to Pushover on backup start
+    since the state has been configured. It should send a custom
+    'message' since it is explicitly declared in the state config.
+    '''
+    hook_config = {
+        'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+        'user': '983hfe0of902lkjfa2amanfgui',
+        'states': {'start', 'fail', 'finish'},
+        'start': {'message': 'custom start message'},
+    }
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'custom start message',
+        },
+    ).and_return(flexmock(ok=True)).once()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.START,
+        monitoring_log_level=1,
+        dry_run=False,
+    )
+
+
+def test_ping_monitor_start_state_backup_default_message_with_priority_emergency_uses_expire_and_retry_defaults():
+    '''
+    This simulates priority level 2 being set but expiry and retry are
+    not declared. This should set retry and expiry to their defaults.
+    '''
+    hook_config = {
+        'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+        'user': '983hfe0of902lkjfa2amanfgui',
+        'states': {'start', 'fail', 'finish'},
+        'start': {'priority': 2},
+    }
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'start',
+            'priority': 2,
+            'retry': 30,
+            'expire': 600,
+        },
+    ).and_return(flexmock(ok=True)).once()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.START,
+        monitoring_log_level=1,
+        dry_run=False,
+    )
+
+
+def test_ping_monitor_start_state_backup_default_message_with_priority_emergency_declared_with_expire_no_retry_success():
+    '''
+    This simulates priority level 2 and expiry being set but retry is
+    not declared. This should set retry to the default.
+    '''
+    hook_config = {
+        'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+        'user': '983hfe0of902lkjfa2amanfgui',
+        'states': {'start', 'fail', 'finish'},
+        'start': {'priority': 2, 'expire': 600},
+    }
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'start',
+            'priority': 2,
+            'retry': 30,
+            'expire': 600,
+        },
+    ).and_return(flexmock(ok=True)).once()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.START,
+        monitoring_log_level=1,
+        dry_run=False,
+    )
+
+
+def test_ping_monitor_start_state_backup_default_message_with_priority_emergency_declared_no_expire_with_retry_success():
+    '''
+    This simulates priority level 2  and retry being set but expire is
+    not declared. This should set expire to the default.
+    '''
+    hook_config = {
+        'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+        'user': '983hfe0of902lkjfa2amanfgui',
+        'states': {'start', 'fail', 'finish'},
+        'start': {'priority': 2, 'retry': 30},
+    }
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'start',
+            'priority': 2,
+            'retry': 30,
+            'expire': 600,
+        },
+    ).and_return(flexmock(ok=True)).once()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.START,
+        monitoring_log_level=1,
+        dry_run=False,
+    )
+
+
+def test_ping_monitor_start_state_backup_default_message_with_priority_high_declared_expire_and_retry_ignored_success():
+    '''
+    This simulates priority level 1, retry and expiry being set. Since expire
+    and retry are only used for priority level 2, they should not be included
+    in the request sent to Pushover. This test verifies that a ValueError is
+    raised.
+    '''
+    hook_config = {
+        'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+        'user': '983hfe0of902lkjfa2amanfgui',
+        'states': {'start', 'fail', 'finish'},
+        'start': {'priority': 1, 'expire': 30, 'retry': 30},
+    }
+
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').never()
+    with pytest.raises(ValueError):
+        module.ping_monitor(
+            hook_config,
+            {},
+            'config.yaml',
+            borgmatic.hooks.monitor.State.START,
+            monitoring_log_level=1,
+            dry_run=False,
+        )
+
+
+def test_ping_monitor_start_state_backup_based_on_documentation_advanced_example_success():
+    '''
+    Here is a test of what is provided in the monitor-your-backups.md file
+    as an 'advanced example'. This test runs the start state.
+    '''
+    hook_config = {
+        'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+        'user': '983hfe0of902lkjfa2amanfgui',
+        'states': {'start', 'fail', 'finish'},
+        'start': {
+            'message': 'Backup <b>Started</b>',
+            'priority': -2,
+            'title': 'Backup Started',
+            'html': 1,
+            'ttl': 10,
+        },
+        'fail': {
+            'message': 'Backup <font color="#ff6961">Failed</font>',
+            'priority': 2,
+            'expire': 600,
+            'retry': 30,
+            'device': 'pixel8',
+            'title': 'Backup Failed',
+            'html': 1,
+            'sound': 'siren',
+            'url': 'https://ticketing-system.example.com/login',
+            'url_title': 'Login to ticketing system',
+        },
+        'finish': {
+            'message': 'Backup <font color="#77dd77">Finished</font>',
+            'priority': 0,
+            'title': 'Backup Finished',
+            'html': 1,
+            'ttl': 60,
+            'url': 'https://ticketing-system.example.com/login',
+            'url_title': 'Login to ticketing system',
+        },
+    }
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'Backup <b>Started</b>',
+            'priority': -2,
+            'title': 'Backup Started',
+            'html': 1,
+            'ttl': 10,
+        },
+    ).and_return(flexmock(ok=True)).once()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.START,
+        monitoring_log_level=1,
+        dry_run=False,
+    )
+
+
+def test_ping_monitor_fail_state_backup_based_on_documentation_advanced_example_success():
+    '''
+    Here is a test of what is provided in the monitor-your-backups.md file
+    as an 'advanced example'. This test runs the fail state.
+    '''
+    hook_config = {
+        'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+        'user': '983hfe0of902lkjfa2amanfgui',
+        'states': {'start', 'fail', 'finish'},
+        'start': {
+            'message': 'Backup <b>Started</b>',
+            'priority': -2,
+            'title': 'Backup Started',
+            'html': 1,
+            'ttl': 10,
+        },
+        'fail': {
+            'message': 'Backup <font color="#ff6961">Failed</font>',
+            'priority': 2,
+            'expire': 600,
+            'retry': 30,
+            'device': 'pixel8',
+            'title': 'Backup Failed',
+            'html': 1,
+            'sound': 'siren',
+            'url': 'https://ticketing-system.example.com/login',
+            'url_title': 'Login to ticketing system',
+        },
+        'finish': {
+            'message': 'Backup <font color="#77dd77">Finished</font>',
+            'priority': 0,
+            'title': 'Backup Finished',
+            'html': 1,
+            'ttl': 60,
+            'url': 'https://ticketing-system.example.com/login',
+            'url_title': 'Login to ticketing system',
+        },
+    }
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'Backup <font color="#ff6961">Failed</font>',
+            'priority': 2,
+            'expire': 600,
+            'retry': 30,
+            'device': 'pixel8',
+            'title': 'Backup Failed',
+            'html': 1,
+            'sound': 'siren',
+            'url': 'https://ticketing-system.example.com/login',
+            'url_title': 'Login to ticketing system',
+        },
+    ).and_return(flexmock(ok=True)).once()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.FAIL,
+        monitoring_log_level=1,
+        dry_run=False,
+    )
+
+
+def test_ping_monitor_finish_state_backup_based_on_documentation_advanced_example_success():
+    '''
+    Here is a test of what is provided in the monitor-your-backups.md file
+    as an 'advanced example'. This test runs the finish state.
+    '''
+    hook_config = {
+        'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+        'user': '983hfe0of902lkjfa2amanfgui',
+        'states': {'start', 'fail', 'finish'},
+        'start': {
+            'message': 'Backup <b>Started</b>',
+            'priority': -2,
+            'title': 'Backup Started',
+            'html': 1,
+            'ttl': 10,
+        },
+        'fail': {
+            'message': 'Backup <font color="#ff6961">Failed</font>',
+            'priority': 2,
+            'expire': 600,
+            'retry': 30,
+            'device': 'pixel8',
+            'title': 'Backup Failed',
+            'html': 1,
+            'sound': 'siren',
+            'url': 'https://ticketing-system.example.com/login',
+            'url_title': 'Login to ticketing system',
+        },
+        'finish': {
+            'message': 'Backup <font color="#77dd77">Finished</font>',
+            'priority': 0,
+            'title': 'Backup Finished',
+            'html': 1,
+            'ttl': 60,
+            'url': 'https://ticketing-system.example.com/login',
+            'url_title': 'Login to ticketing system',
+        },
+    }
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'Backup <font color="#77dd77">Finished</font>',
+            'priority': 0,
+            'title': 'Backup Finished',
+            'html': 1,
+            'ttl': 60,
+            'url': 'https://ticketing-system.example.com/login',
+            'url_title': 'Login to ticketing system',
+        },
+    ).and_return(flexmock(ok=True)).once()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.FINISH,
+        monitoring_log_level=1,
+        dry_run=False,
+    )
+
+
+def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_send_to_pushover_dryrun():
+    '''
+    This test should be the minimum working configuration. The "message"
+    should be auto populated with the default value which is the state name.
+    '''
+    hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'}
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'fail',
+        },
+    ).and_return(flexmock(ok=True)).never()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.FAIL,
+        monitoring_log_level=1,
+        dry_run=True,
+    )
+
+
+def test_ping_monitor_config_incorrect_state_exit_early():
+    '''
+    This test should exit early since the start state is not declared in the configuration.
+    '''
+    hook_config = {
+        'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+        'user': '983hfe0of902lkjfa2amanfgui',
+    }
+    flexmock(module.logger).should_receive('warning').never()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'start',
+        },
+    ).and_return(flexmock(ok=True)).never()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.START,
+        monitoring_log_level=1,
+        dry_run=True,
+    )
+
+
+def test_ping_monitor_push_post_error_exits_early():
+    '''
+    This test simulates the Pushover servers not responding with a 200 OK. We
+    should raise for status and warn then exit.
+    '''
+    hook_config = hook_config = {
+        'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+        'user': '983hfe0of902lkjfa2amanfgui',
+    }
+
+    push_response = flexmock(ok=False)
+    push_response.should_receive('raise_for_status').and_raise(
+        module.requests.ConnectionError
+    ).once()
+    flexmock(module.requests).should_receive('post').with_args(
+        'https://api.pushover.net/1/messages.json',
+        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        data={
+            'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
+            'user': '983hfe0of902lkjfa2amanfgui',
+            'message': 'fail',
+        },
+    ).and_return(push_response).once()
+
+    flexmock(module.logger).should_receive('warning').once()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitor.State.FAIL,
+        monitoring_log_level=1,
+        dry_run=False,
+    )