from unittest import mock import pytest from test_fixtures.models import ExecuteResult from test_fixtures.mysql import MySQLConnection _SECRET = { 'host': 'db.example.com', 'user': 'test', 'password': 'pass', 'database': 'mydb', } def _make_connection(): # Patches exit after __init__, but conn._conn already holds the mock reference # so subsequent method calls on conn still hit the mock. with mock.patch('test_fixtures.mysql.get_secret', return_value=_SECRET): with mock.patch('pymysql.connect') as mock_connect: conn = MySQLConnection('my/db/secret') return conn, mock_connect.return_value def test_connects_with_secret_credentials(): with mock.patch('test_fixtures.mysql.get_secret', return_value=_SECRET): with mock.patch('pymysql.connect') as mock_connect: MySQLConnection('my/db/secret') mock_connect.assert_called_once_with( host='db.example.com', port=3306, user='test', password='pass', database='mydb', cursorclass=mock.ANY, autocommit=True, ) def test_raises_if_secret_is_not_dict(): with mock.patch('test_fixtures.mysql.get_secret', return_value='not-a-dict'): with pytest.raises(ValueError, match='must be a JSON dict'): MySQLConnection('my/db/secret') def test_raises_if_secret_is_missing_required_keys(): with mock.patch( 'test_fixtures.mysql.get_secret', return_value={'host': 'db.example.com'} ): with pytest.raises(ValueError, match='missing required keys'): MySQLConnection('my/db/secret') def test_fetchall(): conn, mock_pymysql_conn = _make_connection() mock_cursor = mock_pymysql_conn.cursor.return_value.__enter__.return_value mock_cursor.fetchall.return_value = [{'id': 1}, {'id': 2}] result = conn.fetchall('SELECT * FROM foo') assert result == [{'id': 1}, {'id': 2}] def test_fetchone(): conn, mock_pymysql_conn = _make_connection() mock_cursor = mock_pymysql_conn.cursor.return_value.__enter__.return_value mock_cursor.fetchone.return_value = {'id': 1} result = conn.fetchone('SELECT * FROM foo WHERE id = %s', (1,)) assert result == {'id': 1} def test_fetchone_returns_none_when_no_row(): conn, mock_pymysql_conn = _make_connection() mock_pymysql_conn.cursor.return_value.__enter__.return_value.fetchone.return_value = None assert conn.fetchone('SELECT * FROM foo WHERE id = %s', (999,)) is None def test_execute_returns_rowcount_and_rows_when_description_present(): conn, mock_pymysql_conn = _make_connection() mock_cursor = mock_pymysql_conn.cursor.return_value.__enter__.return_value mock_cursor.rowcount = 1 mock_cursor.description = [('id',)] mock_cursor.fetchall.return_value = [{'id': 42}] result = conn.execute('INSERT INTO foo (x) VALUES (%s) RETURNING id', (1,)) assert isinstance(result, ExecuteResult) assert result.rowcount == 1 assert result.rows == [{'id': 42}] def test_execute_rows_empty_when_no_result_set(): conn, mock_pymysql_conn = _make_connection() mock_cursor = mock_pymysql_conn.cursor.return_value.__enter__.return_value mock_cursor.rowcount = 3 mock_cursor.description = None result = conn.execute('DELETE FROM foo WHERE status = %s', ('old',)) assert result.rowcount == 3 assert result.rows == [] mock_cursor.fetchall.assert_not_called() def test_close(): conn, mock_pymysql_conn = _make_connection() conn.close() mock_pymysql_conn.close.assert_called_once() def test_context_manager_closes_on_exit(): with mock.patch('test_fixtures.mysql.get_secret', return_value=_SECRET): with mock.patch('pymysql.connect') as mock_connect: with MySQLConnection('my/db/secret') as conn: assert conn is not None mock_connect.return_value.close.assert_called_once()