def test_DatabaseWrapper_connect_successfully_connects(mock_autocommit_methods,
                                                       db_config):
    db = DatabaseWrapper(db_config)
    assert db.connection is None
    db.connect()
    assert db.connection is not None
    assert db.is_usable() is True
def test_DatabaseWrapper_is_usable_returns_false_if_creating_cursor_fails(
        caplog, mocker, mock_connection, mock_autocommit_methods, db_config):
    mock_connection.cursor.side_effect = pyodbc.Error("", "error message 1")
    db = DatabaseWrapper(db_config)
    db.connect()
    assert db.is_usable() is False
    assert "error message 1" in caplog.text
def test_sleeps_between_connection_attempts(mocker, mock_sleep, mock_connect,
                                            db_config):
    mock_uniform = mocker.patch(
        "random.uniform",
        autospec=True,
        side_effect=[1000, 2000, 3000, 4000, 5000],
    )
    mock_connect.side_effect = READ_ERROR
    db = DatabaseWrapper({
        **db_config,
        "CONNECTION_RETRY": {
            "MAX_ATTEMPTS": 6,
            "WAIT_MIN": 15,
            "WAIT_MAX": 100,
            "WAIT_MULTIPLIER": 10,
            "WAIT_EXP_BASE": 2,
        },
    })
    with pytest.raises(pyodbc.Error):
        db.get_new_connection(db.get_connection_params())
    assert mock_uniform.call_args_list == [
        call(15, 15),
        call(15, 20),
        call(15, 40),
        call(15, 80),
        call(15, 100),
    ]
    assert mock_sleep.call_args_list == [
        call(1), call(2), call(3), call(4),
        call(5)
    ]
def test_connection_is_not_revalidated_within_validation_interval(
        mock_is_usable, mock_autocommit_methods, db_config):
    with freeze_time() as frozen_time:
        db = DatabaseWrapper({
            **db_config,
            "OPTIONS": {
                "VALIDATE_CONNECTION": True,
                "VALIDATION_INTERVAL": 10,
            },
        })
        db.connect()

        frozen_time.tick(delta=timedelta(seconds=10))
        db.validate_connection()
        assert mock_is_usable.called is True

        mock_is_usable.reset_mock()

        frozen_time.tick(delta=timedelta(seconds=5))
        db.validate_connection()
        assert mock_is_usable.called is False

        frozen_time.tick(delta=timedelta(seconds=5))
        db.validate_connection()
        assert mock_is_usable.called is True
def test_get_new_connection_breaks_early_if_connection_succeeds(
        mock_sleep, mock_connect, db_config):
    mock_connect.side_effect = [READ_ERROR, Mock()]
    db = DatabaseWrapper({
        **db_config, "CONNECTION_RETRY": {
            "MAX_ATTEMPTS": 3
        }
    })
    db.get_new_connection(db.get_connection_params())
    assert mock_connect.call_count == 2
    assert mock_sleep.call_count == 1
def test_only_retries_certain_errors(mock_sleep, mock_connect, db_config,
                                     error, expected_retries):
    mock_connect.side_effect = error
    db = DatabaseWrapper({
        **db_config, "CONNECTION_RETRY": {
            "MAX_ATTEMPTS": 2
        }
    })
    with pytest.raises(pyodbc.Error):
        db.get_new_connection(db.get_connection_params())
    assert mock_connect.call_count == 1 + expected_retries  # attempts = 1 + retries
def test_errors_to_retry_can_be_overridden(mock_sleep, mock_connect, db_config,
                                           error, expected_retries):
    mock_connect.side_effect = error
    db = DatabaseWrapper({
        **db_config,
        "CONNECTION_RETRY": {
            "MAX_ATTEMPTS": 2,
            "ERRORS": ["-12345"],
        },
    })
    with pytest.raises(pyodbc.Error):
        db.get_new_connection(db.get_connection_params())
    assert mock_connect.call_count == 1 + expected_retries
def test_get_new_connection_retries_up_to_MAX_ATTEMPTS(mock_sleep,
                                                       mock_connect,
                                                       db_config):
    mock_connect.side_effect = READ_ERROR
    db = DatabaseWrapper({
        **db_config, "CONNECTION_RETRY": {
            "MAX_ATTEMPTS": 3
        }
    })
    with pytest.raises(pyodbc.Error):
        db.get_new_connection(db.get_connection_params())
    assert mock_connect.call_count == 3
    assert mock_sleep.call_count == 2
def test_DatabaseWrapper_get_new_connection_calls_pyodbc_connect(
        mock_connect, db_config):
    db = DatabaseWrapper({
        "SERVER": "server1",
        "NAME": "db1",
        "USER": "******",
        "PASSWORD": "******",
        "OPTIONS": {
            "CONN_TIMEOUT": 120
        },
    })
    db.get_new_connection(db.get_connection_params())
    assert mock_connect.called is True
    connection_string = mock_connect.call_args[0][0]
    parts = connection_string.split(";")
    assert "Server=server1" in parts
    assert "Database=db1" in parts
    assert "Uid=user1" in parts
    assert "Pwd=password1" in parts
    assert mock_connect.call_args[1]['timeout'] == 120
def test_DatabaseWrapper_validate_connection_does_not_close_connection_if_not_enabled(
        mock_autocommit_methods, mock_is_usable, db_config):
    mock_is_usable.return_value = False
    db = DatabaseWrapper({
        **db_config,
        "OPTIONS": {
            "VALIDATE_CONNECTION": False,
            "VALIDATION_INTERVAL": 0
        },
    })
    db.connect()
    db.validate_connection()
    assert db.connection is not None
def test_DatabaseWrapper_validate_connection_closes_connections_that_are_not_usable(
        mock_autocommit_methods, mock_is_usable, db_config):
    mock_is_usable.return_value = False
    db = DatabaseWrapper({
        **db_config,
        "OPTIONS": {
            "VALIDATE_CONNECTION": True,
            "VALIDATION_INTERVAL": 0
        },
    })
    db.connect()
    db.validate_connection()
    assert db.connection is None
def test_DatabaseWrapper_validate_connection_handles_closed_connections(
        mock_autocommit_methods, db_config):
    db = DatabaseWrapper({
        **db_config,
        "OPTIONS": {
            "VALIDATE_CONNECTION": True,
            "VALIDATION_INTERVAL": 0
        },
        "CONN_MAX_AGE":
        0,  # this will make close_if_unusable_or_obsolete close the connection
    })
    db.connect()
    db.validate_connection()
    assert db.connection is None
def test_get_new_connection_doesnt_retry_by_default(mock_connect, db_config):
    mock_connect.side_effect = READ_ERROR
    db = DatabaseWrapper(db_config)
    with pytest.raises(pyodbc.Error):
        db.get_new_connection(db.get_connection_params())
    assert mock_connect.call_count == 1
def test_DatabaseWrapper_is_usable(mocker, mock_autocommit_methods, db_config):
    mocker.patch("pyodbc.connect", autospec=True)
    db = DatabaseWrapper(db_config)
    db.connect()
    assert db.is_usable() is True