# -*- coding: utf-8 -*-
# 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 unittest import mock
import os
import requests
import pytest
from apprise import (
    Apprise, AppriseAsset, AppriseAttachment, NotifyType, PersistentStoreMode)
from json import dumps, loads

from apprise.plugins.matrix import NotifyMatrix
from apprise.plugins.matrix import MatrixDiscoveryException
from helpers import AppriseURLTester

# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)

MATRIX_GOOD_RESPONSE = dumps({
    'room_id': '!abc123:localhost',
    'room_alias': '#abc123:localhost',
    'joined_rooms': ['!abc123:localhost', '!def456:localhost'],
    'access_token': 'abcd1234',
    'home_server': 'localhost',

    # Simulate .well-known
    "m.homeserver": {
        "base_url": "https://matrix.example.com"
    },
    "m.identity_server": {
        "base_url": "https://vector.im"
    },
})

# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')

# Our Testing URLs
apprise_url_tests = (
    ##################################
    # NotifyMatrix
    ##################################
    ('matrix://', {
        'instance': None,
    }),
    ('matrixs://', {
        'instance': None,
    }),
    ('matrix://localhost?mode=off', {
        # treats it as a anonymous user to register
        'instance': NotifyMatrix,
        # response is false because we have nothing to notify
        'response': False,
    }),
    ('matrix://localhost', {
        # response is TypeError because we'll try to initialize as
        # a t2bot and fail (localhost is too short of a api key)
        'instance': TypeError
    }),
    ('matrix://user:pass@localhost/#room1/#room2/#room3', {
        'instance': NotifyMatrix,
        'response': False,
        'requests_response_code': requests.codes.internal_server_error,
    }),
    ('matrix://user:pass@localhost/#room1/#room2/!room1', {
        'instance': NotifyMatrix,
        # throw a bizzare code forcing us to fail to look it up
        'response': False,
        'requests_response_code': 999,
    }),
    ('matrix://user:pass@localhost:1234/#room', {
        'instance': NotifyMatrix,
        # Throws a series of connection and transfer exceptions when this flag
        # is set and tests that we gracfully handle them
        'test_requests_exceptions': True,

        # Our expected url(privacy=True) startswith() response:
        'privacy_url': 'matrix://user:****@localhost:1234/',
    }),

    # Matrix supports webhooks too; the following tests this now:
    ('matrix://user:token@localhost?mode=matrix&format=text', {
        # user and token correctly specified with webhook
        'instance': NotifyMatrix,
        'response': False,
    }),
    ('matrix://user:token@localhost?mode=matrix&format=html', {
        # user and token correctly specified with webhook
        'instance': NotifyMatrix,
    }),
    ('matrix://user:token@localhost:123/#general/?version=3', {
        # Provide version over-ride (using version=)
        'instance': NotifyMatrix,
        # Our response expected server response
        'requests_response_text': MATRIX_GOOD_RESPONSE,
        'privacy_url': 'matrix://user:****@localhost:123',
    }),
    ('matrixs://user:token@localhost/#general?v=2', {
        # Provide version over-ride (using v=)
        'instance': NotifyMatrix,
        # Our response expected server response
        'requests_response_text': MATRIX_GOOD_RESPONSE,
        'privacy_url': 'matrixs://user:****@localhost',
    }),
    ('matrix://user:token@localhost:123/#general/?v=invalid', {
        # Invalid version specified
        'instance': TypeError
    }),
    ('matrix://user:token@localhost?mode=slack&format=text', {
        # user and token correctly specified with webhook
        'instance': NotifyMatrix,
    }),
    ('matrixs://user:token@localhost?mode=SLACK&format=markdown', {
        # user and token specified; slack webhook still detected
        # despite uppercase characters
        'instance': NotifyMatrix,
    }),
    ('matrix://user@localhost?mode=SLACK&format=markdown&token=mytoken', {
        # user and token specified; slack webhook still detected
        # despite uppercase characters; token also set on URL as arg
        'instance': NotifyMatrix,
    }),
    ('matrix://_?mode=t2bot&token={}'.format('b' * 64), {
        # Testing t2bot initialization and setting the password using the
        # token directive
        'instance': NotifyMatrix,
        # Our expected url(privacy=True) startswith() response:
        'privacy_url': 'matrix://b...b/',
    }),
    # Image Reference
    ('matrixs://user:token@localhost?mode=slack&format=markdown&image=True', {
        # user and token specified; image set to True
        'instance': NotifyMatrix,
    }),
    ('matrixs://user:token@localhost?mode=slack&format=markdown&image=False', {
        # user and token specified; image set to True
        'instance': NotifyMatrix,
    }),
    # A Bunch of bad ports
    ('matrixs://user:pass@hostname:port/#room_alias', {
        # Invalid Port specified (was a string)
        'instance': TypeError,
    }),
    ('matrixs://user:pass@hostname:0/#room_alias', {
        # Invalid Port specified (was a string)
        'instance': TypeError,
    }),
    ('matrixs://user:pass@hostname:65536/#room_alias', {
        # Invalid Port specified (was a string)
        'instance': TypeError,
    }),
    # More general testing...
    ('matrixs://user@{}?mode=t2bot&format=markdown&image=True'
     .format('a' * 64), {
         # user and token specified; image set to True
         'instance': NotifyMatrix}),
    ('matrix://user@{}?mode=t2bot&format=html&image=False'
     .format('z' * 64), {
         # user and token specified; image set to True
         'instance': NotifyMatrix}),
    # This will default to t2bot because no targets were specified and no
    # password
    ('matrixs://{}'.format('c' * 64), {
        'instance': NotifyMatrix,
        # Throws a series of connection and transfer exceptions when this flag
        # is set and tests that we gracfully handle them
        'test_requests_exceptions': True,
    }),
    # Test Native URL
    ('https://webhooks.t2bot.io/api/v1/matrix/hook/{}/'.format('d' * 64), {
        # user and token specified; image set to True
        'instance': NotifyMatrix,
    }),
    ('matrix://user:token@localhost?mode=On', {
        # invalid webhook specified (unexpected boolean)
        'instance': TypeError,
    }),
    ('matrix://token@localhost/?mode=Matrix', {
        'instance': NotifyMatrix,
        'response': False,
        'requests_response_code': requests.codes.internal_server_error,
    }),
    ('matrix://user:token@localhost/mode=matrix', {
        'instance': NotifyMatrix,
        # throw a bizzare code forcing us to fail to look it up
        'response': False,
        'requests_response_code': 999,
    }),
    ('matrix://token@localhost:8080/?mode=slack', {
        'instance': NotifyMatrix,
        # Throws a series of connection and transfer exceptions when this flag
        # is set and tests that we gracfully handle them
        'test_requests_exceptions': True,
    }),
    ('matrix://{}/?mode=t2bot'.format('b' * 64), {
        'instance': NotifyMatrix,
        # 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_matrix_urls():
    """
    NotifyMatrix() Apprise URLs

    """

    # Run our general tests
    AppriseURLTester(tests=apprise_url_tests).run_all()


@mock.patch('requests.put')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_matrix_general(mock_post, mock_get, mock_put):
    """
    NotifyMatrix() General Tests

    """

    response_obj = {
        'room_id': '!abc123:localhost',
        'room_alias': '#abc123:localhost',
        'joined_rooms': ['!abc123:localhost', '!def456:localhost'],
        'access_token': 'abcd1234',
        'home_server': 'localhost',
    }
    request = mock.Mock()
    request.content = dumps(response_obj)
    request.status_code = requests.codes.ok

    # Prepare Mock
    mock_get.return_value = request
    mock_post.return_value = request
    mock_put.return_value = request

    # Variation Initializations
    obj = NotifyMatrix(host='host', targets='#abcd')
    assert isinstance(obj, NotifyMatrix)
    assert isinstance(obj.url(), str)
    # Registration successful
    assert obj.send(body="test") is True
    del obj

    obj = NotifyMatrix(host='host', user='user', targets='#abcd')
    assert isinstance(obj, NotifyMatrix)
    assert isinstance(obj.url(), str)
    # Registration successful
    assert obj.send(body="test") is True
    del obj

    obj = NotifyMatrix(host='host', password='passwd', targets='#abcd')
    assert isinstance(obj, NotifyMatrix)
    assert isinstance(obj.url(), str)
    # A username gets automatically generated in these cases
    assert obj.send(body="test") is True
    del obj

    obj = NotifyMatrix(
        host='host', user='user', password='passwd', targets='#abcd')
    assert isinstance(obj.url(), str)
    assert isinstance(obj, NotifyMatrix)
    # Registration Successful
    assert obj.send(body="test") is True
    del obj

    # Test sending other format types
    kwargs = NotifyMatrix.parse_url(
        'matrix://user:passwd@hostname/#abcd?format=html')
    obj = NotifyMatrix(**kwargs)
    assert isinstance(obj.url(), str)
    assert isinstance(obj, NotifyMatrix)
    assert obj.send(body="test") is True
    assert obj.send(title="title", body="test") is True
    del obj

    kwargs = NotifyMatrix.parse_url(
        'matrix://user:passwd@hostname/#abcd/#abcd:localhost?format=markdown')
    obj = NotifyMatrix(**kwargs)
    assert isinstance(obj.url(), str)
    assert isinstance(obj, NotifyMatrix)
    assert obj.send(body="test") is True
    assert obj.send(title="title", body="test") is True
    del obj

    kwargs = NotifyMatrix.parse_url(
        'matrix://user:passwd@hostname/#abcd/!abcd:localhost?format=text')
    obj = NotifyMatrix(**kwargs)
    assert isinstance(obj.url(), str)
    assert isinstance(obj, NotifyMatrix) is True
    obj.send(body="test") is True
    obj.send(title="title", body="test") is True
    del obj

    # Test notice type notifications
    kwargs = NotifyMatrix.parse_url(
        'matrix://user:passwd@hostname/#abcd?msgtype=notice')
    obj = NotifyMatrix(**kwargs)
    assert isinstance(obj.url(), str) is True
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.send(body="test") is True
    assert obj.send(title="title", body="test") is True

    with pytest.raises(TypeError):
        # invalid message type specified
        kwargs = NotifyMatrix.parse_url(
            'matrix://user:passwd@hostname/#abcd?msgtype=invalid')
        NotifyMatrix(**kwargs)

    # Force a failed login
    ro = response_obj.copy()
    del ro['access_token']
    request.content = dumps(ro)
    request.status_code = 404

    # Fails because we couldn't register because of 404 errors
    assert obj.send(body="test") is False
    del obj

    obj = NotifyMatrix(host='host', user='test', targets='#abcd')
    assert isinstance(obj, NotifyMatrix) is True
    # Fails because we still couldn't register
    assert obj.send(user='test', password='passwd', body="test") is False
    del obj

    obj = NotifyMatrix(
        host='host', user='test', password='passwd', targets='#abcd')
    assert isinstance(obj, NotifyMatrix) is True
    # Fails because we still couldn't register
    assert obj.send(body="test") is False
    del obj

    obj = NotifyMatrix(host='host', password='passwd', targets='#abcd')
    # Fails because we still couldn't register
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.send(body="test") is False

    # Force a empty joined list response
    ro = response_obj.copy()
    ro['joined_rooms'] = []
    request.content = dumps(ro)
    assert obj.send(user='test', password='passwd', body="test") is False

    # Fall back to original template
    request.content = dumps(response_obj)
    request.status_code = requests.codes.ok

    # update our response object so logins now succeed
    response_obj['user_id'] = '@apprise:localhost'

    # Login was successful but not get a room_id
    ro = response_obj.copy()
    del ro['room_id']
    request.content = dumps(ro)
    assert obj.send(user='test', password='passwd', body="test") is False

    # Fall back to original template
    request.content = dumps(response_obj)
    request.status_code = requests.codes.ok
    del obj

    obj = NotifyMatrix(host='host', targets=None)
    assert isinstance(obj, NotifyMatrix) is True

    # Force a empty joined list response
    ro = response_obj.copy()
    ro['joined_rooms'] = []
    request.content = dumps(ro)
    assert obj.send(user='test', password='passwd', body="test") is False

    # Fall back to original template
    request.content = dumps(response_obj)
    request.status_code = requests.codes.ok

    # our room list is empty so we'll have retrieved the joined_list
    # as our backup
    assert obj.send(user='test', password='passwd', body="test") is True
    del obj


@mock.patch('requests.put')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_matrix_fetch(mock_post, mock_get, mock_put):
    """
    NotifyMatrix() Server Fetch/API Tests

    """

    response_obj = {
        'room_id': '!abc123:localhost',
        'room_alias': '#abc123:localhost',
        'joined_rooms': ['!abc123:localhost', '!def456:localhost'],

        # Login details
        'access_token': 'abcd1234',
        'user_id': '@apprise:localhost',
        'home_server': 'localhost',
    }

    def fetch_failed(url, *args, **kwargs):

        # Default configuration
        request = mock.Mock()
        request.status_code = requests.codes.ok
        request.content = dumps(response_obj)

        if url.find('/rooms/') > -1:
            # over-ride on room query
            request.status_code = 403
            request.content = dumps({
                u'errcode': u'M_UNKNOWN',
                u'error': u'Internal server error',
            })

        return request

    mock_put.side_effect = fetch_failed
    mock_get.side_effect = fetch_failed
    mock_post.side_effect = fetch_failed

    obj = NotifyMatrix(
        host='host', user='user', password='passwd', include_image=True)
    assert isinstance(obj, NotifyMatrix) is True
    # We would hve failed to send our image notification
    assert obj.send(user='test', password='passwd', body="test") is False
    del obj

    # Do the same query with no images to fetch
    asset = AppriseAsset(image_path_mask=False, image_url_mask=False)
    obj = NotifyMatrix(
        host='host', user='user', password='passwd', asset=asset)
    assert isinstance(obj, NotifyMatrix) is True
    # We would hve failed to send our notification
    assert obj.send(user='test', password='passwd', body="test") is False
    del obj

    response_obj = {
        # Registration
        'access_token': 'abcd1234',
        'user_id': '@apprise:localhost',
        'home_server': 'localhost',

        # For room joining
        'room_id': '!abc123:localhost',
    }

    # Default configuration
    mock_get.side_effect = None
    mock_post.side_effect = None
    mock_put.side_effect = None

    request = mock.Mock()
    request.status_code = requests.codes.ok
    request.content = dumps(response_obj)
    mock_post.return_value = request
    mock_get.return_value = request
    mock_put.return_value = request

    obj = NotifyMatrix(host='host', include_image=True)
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.access_token is None
    assert obj._register() is True
    assert obj.access_token is not None

    # Cause retries
    request.status_code = 429
    request.content = dumps({
        'retry_after_ms': 1,
    })

    code, response = obj._fetch('/retry/apprise/unit/test')
    assert code is False

    request.content = dumps({
        'error': {
            'retry_after_ms': 1,
        }
    })
    code, response = obj._fetch('/retry/apprise/unit/test')
    assert code is False

    request.content = dumps({
        'error': {}
    })
    code, response = obj._fetch('/retry/apprise/unit/test')
    assert code is False
    del obj


@mock.patch('requests.put')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_matrix_auth(mock_post, mock_get, mock_put):
    """
    NotifyMatrix() Server Authentication

    """

    response_obj = {
        # Registration
        'access_token': 'abcd1234',
        'user_id': '@apprise:localhost',
        'home_server': 'localhost',
    }

    # Default configuration
    request = mock.Mock()
    request.status_code = requests.codes.ok
    request.content = dumps(response_obj)
    mock_post.return_value = request
    mock_get.return_value = request
    mock_put.return_value = request

    obj = NotifyMatrix(host='localhost')
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.access_token is None
    # logging out without an access_token is silently a success
    assert obj._logout() is True
    assert obj.access_token is None

    assert obj._register() is True
    assert obj.access_token is not None

    # Logging in is silently treated as a success because we
    # already had success registering
    assert obj._login() is True
    assert obj.access_token is not None

    # However if we log out
    assert obj._logout() is True
    assert obj.access_token is None

    # And set ourselves up for failure
    request.status_code = 403
    assert obj._login() is False
    assert obj.access_token is None

    # Reset our token
    obj.access_token = None

    # Adjust our response to be invalid - missing access_token in response
    request.status_code = requests.codes.ok
    ro = response_obj.copy()
    del ro['access_token']
    request.content = dumps(ro)
    # Our registration will fail now
    assert obj._register() is False
    assert obj.access_token is None
    del obj

    # So will login
    obj = NotifyMatrix(host='host', user='user', password='password')
    assert isinstance(obj, NotifyMatrix) is True
    assert obj._login() is False
    assert obj.access_token is None

    # Adjust our response to be invalid - invalid json response
    request.content = "{"
    # Our registration will fail now
    assert obj._register() is False
    assert obj.access_token is None

    request.status_code = requests.codes.ok
    request.content = dumps(response_obj)
    assert obj._register() is True
    assert obj.access_token is not None
    # Test logoff when getting a 403 error
    request.status_code = 403
    assert obj._logout() is False
    assert obj.access_token is not None

    request.status_code = requests.codes.ok
    request.content = dumps(response_obj)
    assert obj._register() is True
    assert obj.access_token is not None
    request.status_code = 403
    request.content = dumps({
        u'errcode': u'M_UNKNOWN_TOKEN',
        u'error': u'Access Token unknown or expired',
    })
    # Test logoff when getting a 403 error; but if we have the right error
    # code in the response, then we return a True
    assert obj._logout() is True
    assert obj.access_token is None
    del obj


@mock.patch('requests.put')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_matrix_rooms(mock_post, mock_get, mock_put):
    """
    NotifyMatrix() Room Testing

    """

    response_obj = {
        # Registration
        'access_token': 'abcd1234',
        'user_id': '@apprise:localhost',
        'home_server': 'localhost',

        # For joined_room response
        'joined_rooms': ['!abc123:localhost', '!def456:localhost'],

        # For room joining
        'room_id': '!abc123:localhost',
    }

    # Default configuration
    request = mock.Mock()
    request.status_code = requests.codes.ok
    request.content = dumps(response_obj)
    mock_post.return_value = request
    mock_get.return_value = request
    mock_put.return_value = request

    obj = NotifyMatrix(host='host')
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.access_token is None

    # Can't get room listing if we're not connnected
    assert obj._room_join('#abc123') is None

    assert obj._register() is True
    assert obj.access_token is not None

    assert obj._room_join('!abc123') == response_obj['room_id']
    # Use cache to get same results
    assert obj.store.get('!abc123') is None
    # However this is how the cache entry gets stored
    assert obj.store.get('!abc123:localhost') is not None
    assert obj.store.get('!abc123:localhost')['id'] == response_obj['room_id']
    assert obj._room_join('!abc123') == response_obj['room_id']

    obj.store.clear()
    assert obj._room_join('!abc123:localhost') == response_obj['room_id']
    assert obj.store.get('!abc123:localhost') is not None
    assert obj.store.get('!abc123:localhost')['id'] == response_obj['room_id']
    # Use cache to get same results
    assert obj._room_join('!abc123:localhost') == response_obj['room_id']

    obj.store.clear()
    assert obj._room_join('abc123') == response_obj['room_id']
    # Use cache to get same results
    assert obj.store.get('#abc123:localhost') is not None
    assert obj.store.get('#abc123:localhost')['id'] == response_obj['room_id']
    assert obj._room_join('abc123') == response_obj['room_id']

    obj.store.clear()
    assert obj._room_join('abc123:localhost') == response_obj['room_id']
    # Use cache to get same results
    assert obj.store.get('#abc123:localhost') is not None
    assert obj.store.get('#abc123:localhost')['id'] == response_obj['room_id']
    assert obj._room_join('abc123:localhost') == response_obj['room_id']

    obj.store.clear()
    assert obj._room_join('#abc123:localhost') == response_obj['room_id']
    # Use cache to get same results
    assert obj.store.get('#abc123:localhost') is not None
    assert obj.store.get('#abc123:localhost')['id'] == response_obj['room_id']
    assert obj._room_join('#abc123:localhost') == response_obj['room_id']

    obj.store.clear()
    assert obj._room_join('%') is None
    assert obj._room_join(None) is None

    # 403 response; this will push for a room creation for alias based rooms
    # and these will fail
    request.status_code = 403
    obj.store.clear()
    assert obj._room_join('!abc123') is None
    obj.store.clear()
    assert obj._room_join('!abc123:localhost') is None
    obj.store.clear()
    assert obj._room_join('abc123') is None
    obj.store.clear()
    assert obj._room_join('abc123:localhost') is None
    obj.store.clear()
    assert obj._room_join('#abc123:localhost') is None
    del obj

    # Room creation
    request.status_code = requests.codes.ok
    obj = NotifyMatrix(host='host')
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.access_token is None

    # Can't get room listing if we're not connnected
    assert obj._room_create('#abc123') is None

    assert obj._register() is True
    assert obj.access_token is not None

    # You can't add room_id's, they must be aliases
    assert obj._room_create('!abc123') is None
    assert obj._room_create('!abc123:localhost') is None
    obj.store.clear()
    assert obj._room_create('abc123') == response_obj['room_id']
    obj.store.clear()
    assert obj._room_create('abc123:localhost') == response_obj['room_id']
    obj.store.clear()
    assert obj._room_create('#abc123:localhost') == response_obj['room_id']
    obj.store.clear()
    assert obj._room_create('%') is None
    assert obj._room_create(None) is None

    # 403 response; this will push for a room creation for alias based rooms
    # and these will fail
    request.status_code = 403
    obj.store.clear()
    assert obj._room_create('abc123') is None
    obj.store.clear()
    assert obj._room_create('abc123:localhost') is None
    obj.store.clear()
    assert obj._room_create('#abc123:localhost') is None

    request.status_code = 403
    request.content = dumps({
        u'errcode': u'M_ROOM_IN_USE',
        u'error': u'Room alias already taken',
    })
    obj.store.clear()
    # This causes us to look up a channel ID if we get a ROOM_IN_USE response
    assert obj._room_create('#abc123:localhost') is None
    del obj

    # Room detection
    request.status_code = requests.codes.ok
    request.content = dumps(response_obj)
    obj = NotifyMatrix(host='localhost')
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.access_token is None

    # No rooms if we're not connected
    response = obj._joined_rooms()
    assert isinstance(response, list) is True
    assert len(response) == 0

    # register our account
    assert obj._register() is True
    assert obj.access_token is not None

    response = obj._joined_rooms()
    assert isinstance(response, list) is True
    assert len(response) == len(response_obj['joined_rooms'])
    for r in response:
        assert r in response_obj['joined_rooms']

    request.status_code = 403
    response = obj._joined_rooms()
    assert isinstance(response, list) is True
    assert len(response) == 0
    del obj

    # Room id lookup
    request.status_code = requests.codes.ok
    obj = NotifyMatrix(host='localhost')
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.access_token is None

    # Can't get room listing if we're not connnected
    assert obj._room_id('#abc123') is None

    assert obj._register() is True
    assert obj.access_token is not None

    # You can't add room_id's, they must be aliases
    assert obj._room_id('!abc123') is None
    assert obj._room_id('!abc123:localhost') is None
    obj.store.clear()
    assert obj._room_id('abc123') == response_obj['room_id']
    obj.store.clear()
    assert obj._room_id('abc123:localhost') == response_obj['room_id']
    obj.store.clear()
    assert obj._room_id('#abc123:localhost') == response_obj['room_id']
    obj.store.clear()
    assert obj._room_id('%') is None
    assert obj._room_id(None) is None

    # If we can't look the code up, we return None
    request.status_code = 403
    obj.store.clear()
    assert obj._room_id('#abc123:localhost') is None

    # Force a object removal (thus a logout call)
    del obj


def test_plugin_matrix_url_parsing():
    """
    NotifyMatrix() URL Testing

    """
    result = NotifyMatrix.parse_url(
        'matrix://user:token@localhost?to=#room')
    assert isinstance(result, dict) is True
    assert len(result['targets']) == 1
    assert '#room' in result['targets']

    result = NotifyMatrix.parse_url(
        'matrix://user:token@localhost?to=#room1,#room2,#room3')
    assert isinstance(result, dict) is True
    assert len(result['targets']) == 3
    assert '#room1' in result['targets']
    assert '#room2' in result['targets']
    assert '#room3' in result['targets']


@mock.patch('requests.put')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_matrix_image_errors(mock_post, mock_get, mock_put):
    """
    NotifyMatrix() Image Error Handling

    """

    def mock_function_handing(url, data, **kwargs):
        """
        dummy function for handling image posts (as a failure)
        """
        response_obj = {
            'room_id': '!abc123:localhost',
            'room_alias': '#abc123:localhost',
            'joined_rooms': ['!abc123:localhost', '!def456:localhost'],
            'access_token': 'abcd1234',
            'home_server': 'localhost',
        }

        request = mock.Mock()
        request.content = dumps(response_obj)
        request.status_code = requests.codes.ok

        if 'm.image' in data:
            # Fail for images
            request.status_code = 400

        return request

    # Prepare Mock
    mock_get.side_effect = mock_function_handing
    mock_post.side_effect = mock_function_handing
    mock_put.side_effect = mock_function_handing

    obj = NotifyMatrix(host='host', include_image=True, version='2')
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.access_token is None

    # Notification was successful, however we could not post image and since
    # we had post errors (of any kind) we still report a failure.
    assert obj.notify('test', 'test') is False
    del obj

    obj = NotifyMatrix(host='host', include_image=False, version='2')
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.access_token is None

    # We didn't post an image (which was set to fail) and therefore our
    # post was okay
    assert obj.notify('test', 'test') is True

    # Force a object removal (thus a logout call)
    del obj

    def mock_function_handing(url, data, **kwargs):
        """
        dummy function for handling image posts (successfully)
        """
        response_obj = {
            'room_id': '!abc123:localhost',
            'room_alias': '#abc123:localhost',
            'joined_rooms': ['!abc123:localhost', '!def456:localhost'],
            'access_token': 'abcd1234',
            'home_server': 'localhost',
        }

        request = mock.Mock()
        request.content = dumps(response_obj)
        request.status_code = requests.codes.ok

        return request

    # Prepare Mock
    mock_get.side_effect = mock_function_handing
    mock_put.side_effect = mock_function_handing
    mock_post.side_effect = mock_function_handing
    obj = NotifyMatrix(host='host', include_image=True)
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.access_token is None

    assert obj.notify('test', 'test') is True
    del obj

    obj = NotifyMatrix(host='host', include_image=False)
    assert isinstance(obj, NotifyMatrix) is True
    assert obj.access_token is None

    assert obj.notify('test', 'test') is True

    # Force a object removal (thus a logout call)
    del obj


@mock.patch('requests.put')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_matrix_attachments_api_v3(mock_post, mock_get, mock_put):
    """
    NotifyMatrix() Attachment Checks (v3)

    """

    # Prepare a good response
    response = mock.Mock()
    response.status_code = requests.codes.ok
    response.content = MATRIX_GOOD_RESPONSE.encode('utf-8')

    # Prepare a bad response
    bad_response = mock.Mock()
    bad_response.status_code = requests.codes.internal_server_error

    # Prepare Mock return object
    mock_post.return_value = response
    mock_get.return_value = response
    mock_put.return_value = response

    # Instantiate our object
    obj = Apprise.instantiate('matrix://user:pass@localhost/#general?v=3')

    # attach our content
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # Test our call count
    assert mock_put.call_count == 1
    assert mock_post.call_count == 2
    assert mock_post.call_args_list[0][0][0] == \
        'http://localhost/_matrix/client/v3/login'
    assert mock_post.call_args_list[1][0][0] == \
        'http://localhost/_matrix/client/v3/join/%23general%3Alocalhost'
    assert mock_put.call_args_list[0][0][0] == \
        'http://localhost/_matrix/client/v3/rooms/%21abc123%3Alocalhost/' \
        'send/m.room.message/0'

    # Attach an unsupported file type (it's just skipped)
    attach = AppriseAttachment(
        os.path.join(TEST_VAR_DIR, 'apprise-archive.zip'))
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    # An invalid attachment will cause a failure
    path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
    attach = AppriseAttachment(path)
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=path) is False

    # update our attachment to be valid
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    mock_post.return_value = None
    # Throw an exception on the first call to requests.post()
    for side_effect in (requests.RequestException(), OSError(), bad_response):
        mock_post.side_effect = [side_effect]

        # We'll never fail because files are not attached
        assert obj.send(body="test", attach=attach) is True

    # Throw an exception on the second call to requests.post()
    for side_effect in (requests.RequestException(), OSError(), bad_response):
        mock_post.side_effect = [response, side_effect]

        # Attachment support does not exist vor v3 at time, so this will
        # work nicely
        assert obj.send(body="test", attach=attach) is True

    # handle a bad response
    mock_post.side_effect = [response, bad_response, response]

    # Attachment support does not exist vor v3 at time, so this will
    # work nicely
    assert obj.send(body="test", attach=attach) is True

    # Force a object removal (thus a logout call)
    del obj


@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_matrix_discovery_service(mock_post, mock_get):
    """
    NotifyMatrix() Discovery Service

    """

    # Prepare a good response
    response = mock.Mock()
    response.status_code = requests.codes.ok
    response.content = MATRIX_GOOD_RESPONSE.encode('utf-8')

    # Prepare a good response
    bad_response = mock.Mock()
    bad_response.status_code = requests.codes.unauthorized
    bad_response.content = MATRIX_GOOD_RESPONSE.encode('utf-8')

    # Prepare Mock return object
    mock_post.return_value = response
    mock_get.return_value = response

    # Instantiate our object
    obj = Apprise.instantiate(
        'matrixs://user:pass@example.com/#general?v=2&discovery=yes')
    assert obj.notify('body') is True

    response = mock.Mock()
    response.status_code = requests.codes.unavailable
    _resp = loads(MATRIX_GOOD_RESPONSE)

    mock_get.return_value = response
    mock_post.return_value = response
    obj = Apprise.instantiate(
        'matrixs://user:pass@example.com/#general?v=2&discovery=yes')
    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)

    # Invalid host / fallback is to resolve our own host
    with pytest.raises(MatrixDiscoveryException):
        obj.base_url

    # Verify cache is not saved
    assert NotifyMatrix.discovery_base_key not in obj.store
    assert NotifyMatrix.discovery_identity_key not in obj.store

    response.status_code = requests.codes.ok
    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)

    # bad data
    _resp['m.homeserver'] = '!garbage!:303'
    response.content = dumps(_resp).encode('utf-8')
    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)

    with pytest.raises(MatrixDiscoveryException):
        obj.base_url

    # Verify cache is not saved
    assert NotifyMatrix.discovery_base_key not in obj.store
    assert NotifyMatrix.discovery_identity_key not in obj.store

    # We fail our discovery and therefore can't send our notification
    assert obj.notify('hello world') is False

    # bad key
    _resp['m.homeserver'] = {}
    response.content = dumps(_resp).encode('utf-8')
    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)
    with pytest.raises(MatrixDiscoveryException):
        obj.base_url

    # Verify cache is not saved
    assert NotifyMatrix.discovery_base_key not in obj.store
    assert NotifyMatrix.discovery_identity_key not in obj.store

    _resp['m.homeserver'] = {'base_url': 'https://nuxref.com/base'}
    response.content = dumps(_resp).encode('utf-8')
    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)
    assert obj.base_url == 'https://nuxref.com/base'
    assert obj.identity_url == "https://vector.im"

    # Verify cache saved
    assert NotifyMatrix.discovery_base_key in obj.store
    assert NotifyMatrix.discovery_identity_key in obj.store

    # Discovery passes so notifications work too
    assert obj.notify('hello world') is True

    # bad data
    _resp['m.identity_server'] = '!garbage!:303'
    response.content = dumps(_resp).encode('utf-8')
    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)

    with pytest.raises(MatrixDiscoveryException):
        obj.base_url

    # Verify cache is not saved
    assert NotifyMatrix.discovery_base_key not in obj.store
    assert NotifyMatrix.discovery_identity_key not in obj.store

    # no key
    _resp['m.identity_server'] = {}
    response.content = dumps(_resp).encode('utf-8')
    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)

    with pytest.raises(MatrixDiscoveryException):
        obj.base_url

    # Verify cache is not saved
    assert NotifyMatrix.discovery_base_key not in obj.store
    assert NotifyMatrix.discovery_identity_key not in obj.store

    # remove
    del _resp['m.identity_server']
    response.content = dumps(_resp).encode('utf-8')

    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)
    assert obj.base_url == 'https://nuxref.com/base'
    assert obj.identity_url == 'https://nuxref.com/base'

    # restore
    _resp['m.identity_server'] = {'base_url': '"https://vector.im'}
    response.content = dumps(_resp).encode('utf-8')

    # Not found is an acceptable response (no exceptions thrown)
    response.status_code = requests.codes.not_found
    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)
    assert obj.base_url == 'https://example.com'
    assert obj.identity_url == 'https://example.com'

    # Verify cache saved
    assert NotifyMatrix.discovery_base_key in obj.store
    assert NotifyMatrix.discovery_identity_key in obj.store

    # Discovery passes so notifications work too
    response.status_code = requests.codes.ok
    assert obj.notify('hello world') is True

    response.status_code = requests.codes.ok
    mock_get.return_value = None
    mock_get.side_effect = (response, bad_response)
    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)

    with pytest.raises(MatrixDiscoveryException):
        obj.base_url

    # Verify cache is not saved
    assert NotifyMatrix.discovery_base_key not in obj.store
    assert NotifyMatrix.discovery_identity_key not in obj.store

    # Test case where ourIdentity URI fails to do it's check
    mock_get.side_effect = (response, response, bad_response)
    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)

    with pytest.raises(MatrixDiscoveryException):
        obj.base_url

    # Verify cache is not saved
    assert NotifyMatrix.discovery_base_key not in obj.store
    assert NotifyMatrix.discovery_identity_key not in obj.store

    # Test an empty block response
    response.status_code = requests.codes.ok
    response.content = ''
    mock_get.return_value = response
    mock_get.side_effect = None
    mock_post.return_value = response
    mock_post.side_effect = None
    obj.store.clear(
        NotifyMatrix.discovery_base_key, NotifyMatrix.discovery_identity_key)

    assert obj.base_url == 'https://example.com'
    assert obj.identity_url == 'https://example.com'

    # Verify cache saved
    assert NotifyMatrix.discovery_base_key in obj.store
    assert NotifyMatrix.discovery_identity_key in obj.store

    del obj


@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_matrix_attachments_api_v2(mock_post, mock_get):
    """
    NotifyMatrix() Attachment Checks (v2)

    """

    # Prepare a good response
    response = mock.Mock()
    response.status_code = requests.codes.ok
    response.content = MATRIX_GOOD_RESPONSE.encode('utf-8')

    # Prepare a bad response
    bad_response = mock.Mock()
    bad_response.status_code = requests.codes.internal_server_error

    # Prepare Mock return object
    mock_post.return_value = response
    mock_get.return_value = response

    # Instantiate our object
    obj = Apprise.instantiate('matrix://user:pass@localhost/#general?v=2')

    # attach our content
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    # Attach an unsupported file
    mock_post.return_value = response
    mock_get.return_value = response
    mock_post.side_effect = None
    mock_get.side_effect = None

    # Force a object removal (thus a logout call)
    del obj

    # Instantiate our object
    obj = Apprise.instantiate('matrixs://user:pass@localhost/#general?v=2')

    # Reset our object
    mock_post.reset_mock()
    mock_get.reset_mock()

    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    # Test our call count
    assert mock_post.call_count == 5
    assert mock_post.call_args_list[0][0][0] == \
        'https://matrix.example.com/_matrix/client/r0/login'
    assert mock_post.call_args_list[1][0][0] == \
        'https://matrix.example.com/_matrix/media/r0/upload'
    assert mock_post.call_args_list[2][0][0] == \
        'https://matrix.example.com/_matrix/client/r0/' \
        'join/%23general%3Alocalhost'
    assert mock_post.call_args_list[3][0][0] == \
        'https://matrix.example.com/_matrix/client/r0' \
        '/rooms/%21abc123%3Alocalhost/send/m.room.message'
    assert mock_post.call_args_list[4][0][0] == \
        'https://matrix.example.com/_matrix/client/r0/' \
        'rooms/%21abc123%3Alocalhost/send/m.room.message'

    # Attach an unsupported file type; these are skipped
    attach = AppriseAttachment(
        os.path.join(TEST_VAR_DIR, 'apprise-archive.zip'))
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=attach) is True

    # An invalid attachment will cause a failure
    path = os.path.join(TEST_VAR_DIR, '/invalid/path/to/an/invalid/file.jpg')
    attach = AppriseAttachment(path)
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO,
        attach=path) is False

    # update our attachment to be valid
    attach = AppriseAttachment(os.path.join(TEST_VAR_DIR, 'apprise-test.gif'))

    mock_post.return_value = None
    mock_get.return_value = None

    # Throw an exception on the first call to requests.post()
    for side_effect in (requests.RequestException(), OSError(), bad_response):
        # Reset our value
        mock_post.reset_mock()
        mock_get.reset_mock()

        mock_post.side_effect = [side_effect, response]
        mock_get.side_effect = [side_effect, response]

        assert obj.send(body="test", attach=attach) is False

    # Throw an exception on the second call to requests.post()
    for side_effect in (requests.RequestException(), OSError(), bad_response):
        # Reset our value
        mock_post.reset_mock()
        mock_get.reset_mock()

        mock_post.side_effect = [response, side_effect, side_effect, response]
        mock_get.side_effect = [side_effect, side_effect, response]

        # We'll fail now because of our error handling
        assert obj.send(body="test", attach=attach) is False

    # handle a bad response
    mock_post.side_effect = \
        [response, bad_response, response, response, response, response]
    mock_get.side_effect = \
        [response, bad_response, response, response, response, response]

    # We'll fail now because of an internal exception
    assert obj.send(body="test", attach=attach) is False

    # Force a object removal (thus a logout call)
    del obj

    # Instantiate our object (no discovery required)
    obj = Apprise.instantiate(
        'matrixs://user:pass@localhost/#general?v=2&discovery=no&image=y')

    # Reset our object
    mock_post.reset_mock()
    mock_get.reset_mock()

    mock_post.return_value = None
    mock_get.return_value = None
    mock_post.side_effect = \
        [response, response, bad_response, response, response, response,
         response]
    mock_get.side_effect = \
        [response, response, bad_response, response, response, response,
         response]

    # image attachment didn't succeed
    assert obj.notify(
        body='body', title='title', notify_type=NotifyType.INFO) is False

    # Error during image post
    mock_post.return_value = response
    mock_get.return_value = response
    mock_post.side_effect = None
    mock_get.side_effect = None

    # We'll fail now because of an internal exception
    assert obj.send(body="test", attach=attach) is True

    # Force __del__() call
    del obj


@mock.patch('requests.put')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_matrix_transaction_ids_api_v3_no_cache(
        mock_post, mock_get, mock_put):
    """
    NotifyMatrix() Transaction ID Checks (v3)

    """

    # Prepare a good response
    response = mock.Mock()
    response.status_code = requests.codes.ok
    response.content = MATRIX_GOOD_RESPONSE.encode('utf-8')

    # Prepare a bad response
    bad_response = mock.Mock()
    bad_response.status_code = requests.codes.internal_server_error

    # Prepare Mock return object
    mock_post.return_value = response
    mock_get.return_value = response
    mock_put.return_value = response

    # For each element is 1 batch that is ran
    # the number defined is the number of notifications to send
    batch = [10, 1, 5]

    for notifications in batch:
        # Instantiate our object
        obj = Apprise.instantiate('matrix://user:pass@localhost/#general?v=3')

        # Ensure mode is memory
        assert obj.store.mode == PersistentStoreMode.MEMORY

        # Performs a login
        assert obj.notify(
            body='body', title='title', notify_type=NotifyType.INFO
        ) is True
        assert mock_get.call_count == 0
        assert mock_post.call_count == 2
        assert mock_post.call_args_list[0][0][0] == \
            'http://localhost/_matrix/client/v3/login'
        assert mock_post.call_args_list[1][0][0] == \
            'http://localhost/_matrix/client/v3/join/%23general%3Alocalhost'
        assert mock_put.call_count == 1
        assert mock_put.call_args_list[0][0][0] == \
            'http://localhost/_matrix/client/v3/rooms/' + \
            '%21abc123%3Alocalhost/send/m.room.message/0'

        for no, _ in enumerate(range(notifications), start=1):
            # Clean our slate
            mock_post.reset_mock()
            mock_get.reset_mock()
            mock_put.reset_mock()

            assert obj.notify(
                body='body', title='title', notify_type=NotifyType.INFO
            ) is True

            assert mock_get.call_count == 0
            assert mock_post.call_count == 0
            assert mock_put.call_count == 1
            assert mock_put.call_args_list[0][0][0] == \
                'http://localhost/_matrix/client/v3/rooms/' + \
                f'%21abc123%3Alocalhost/send/m.room.message/{no}'

        mock_post.reset_mock()
        mock_get.reset_mock()
        mock_put.reset_mock()

        # Force a object removal (thus a logout call)
        del obj

        assert mock_get.call_count == 0
        assert mock_post.call_count == 1
        assert mock_post.call_args_list[0][0][0] == \
            'http://localhost/_matrix/client/v3/logout'
        mock_post.reset_mock()
        assert mock_put.call_count == 0


@mock.patch('requests.put')
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_matrix_transaction_ids_api_v3_w_cache(
        mock_post, mock_get, mock_put, tmpdir):
    """
    NotifyMatrix() Transaction ID Checks (v3)

    """

    # Prepare a good response
    response = mock.Mock()
    response.status_code = requests.codes.ok
    response.content = MATRIX_GOOD_RESPONSE.encode('utf-8')

    # Prepare a bad response
    bad_response = mock.Mock()
    bad_response.status_code = requests.codes.internal_server_error

    # Prepare Mock return object
    mock_post.return_value = response
    mock_get.return_value = response
    mock_put.return_value = response

    # For each element is 1 batch that is ran
    # the number defined is the number of notifications to send
    batch = [10, 1, 5]

    mock_post.reset_mock()
    mock_get.reset_mock()
    mock_put.reset_mock()

    asset = AppriseAsset(
        storage_mode=PersistentStoreMode.FLUSH,
        storage_path=str(tmpdir),
    )

    # Message Counter
    transaction_id = 1

    for no, notifications in enumerate(batch):
        # Instantiate our object
        obj = Apprise.instantiate(
            'matrix://user:pass@localhost/#general?v=3', asset=asset)

        # Ensure mode is flush
        assert obj.store.mode == PersistentStoreMode.FLUSH

        # Performs a login
        assert obj.notify(
            body='body', title='title', notify_type=NotifyType.INFO
        ) is True
        assert mock_get.call_count == 0
        if no == 0:
            # first entry
            assert mock_post.call_count == 2
            assert mock_post.call_args_list[0][0][0] == \
                'http://localhost/_matrix/client/v3/login'
            assert mock_post.call_args_list[1][0][0] == \
                'http://localhost/_matrix/client/v3/' \
                'join/%23general%3Alocalhost'
            assert mock_put.call_count == 1
            assert mock_put.call_args_list[0][0][0] == \
                'http://localhost/_matrix/client/v3/rooms/' + \
                '%21abc123%3Alocalhost/send/m.room.message/0'

        for no, _ in enumerate(range(notifications), start=transaction_id):
            # Clean our slate
            mock_post.reset_mock()
            mock_get.reset_mock()
            mock_put.reset_mock()

            assert obj.notify(
                body='body', title='title', notify_type=NotifyType.INFO
            ) is True

            # Increment transaction counter
            transaction_id += 1

            assert mock_get.call_count == 0
            assert mock_post.call_count == 0
            assert mock_put.call_count == 1
            assert mock_put.call_args_list[0][0][0] == \
                'http://localhost/_matrix/client/v3/rooms/' + \
                f'%21abc123%3Alocalhost/send/m.room.message/{no}'

        # Increment transaction counter
        transaction_id += 1

        mock_post.reset_mock()
        mock_get.reset_mock()
        mock_put.reset_mock()

        # Force a object removal
        # Biggest takeaway is that a logout no longer happens
        del obj

        assert mock_get.call_count == 0
        assert mock_post.call_count == 0
        assert mock_put.call_count == 0
