diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 00000000..70036ab7 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,59 @@ +--- +# This workflow will install Python dependencies +# and run unit tests for given OSes + +name: Unit tests + +on: [push, pull_request] + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: 'ubuntu-latest' + python-version: '3.7' + rf-version: '3.2.2' + - os: 'windows' + python-version: '3.8' + rf-version: '4.1.3' + - os: 'ubuntu-latest' + python-version: '3.9' + rf-version: '5.0.1' + - os: 'ubuntu-latest' + python-version: '3.10' + rf-version: '6.1.1' + - os: 'ubuntu-latest' + python-version: '3.11' + rf-version: '6.1.1' + - os: 'ubuntu-latest' + python-version: '3.12' + rf-version: '6.1.1' + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ${{ matrix.rf-version }} + pip install . + + - name: Run unit tests with coverage + run: + coverage run -m pytest + + - name: Codecov + uses: codecov/codecov-action@v3 + with: + name: ${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.rf-version }} diff --git a/src/DatabaseLibrary/connection_manager.py b/src/DatabaseLibrary/connection_manager.py index 09a42793..2a30e3cf 100644 --- a/src/DatabaseLibrary/connection_manager.py +++ b/src/DatabaseLibrary/connection_manager.py @@ -13,14 +13,11 @@ # limitations under the License. import importlib +from configparser import ConfigParser, NoOptionError, NoSectionError from dataclasses import dataclass +from pathlib import Path from typing import Any, Dict, Optional -try: - import ConfigParser -except: - import configparser as ConfigParser - from robot.api import logger @@ -81,6 +78,30 @@ def __iter__(self): return iter(self._connections.values()) +class ConfigReader: + def __init__(self, config_file: Optional[str], alias: str): + if config_file is None: + config_file = "./resources/db.cfg" + config_path = Path(config_file) + if config_path.exists(): + config = ConfigParser() + config.read([config_file]) + else: + config = None + self.alias = alias + self.config: Optional[ConfigParser] = config + + def get(self, param: str) -> str: + if self.config is None: + raise ValueError(f"Required '{param}' parameter was not provided in keyword arguments.") from None + try: + return self.config.get(self.alias, param) + except NoSectionError: + raise ValueError(f"Configuration file does not have [{self.alias}] section.") from None + except NoOptionError: + raise ValueError(f"Required '{param}' parameter missing in both keyword arguments and configuration file.") + + class ConnectionManager: """ Connection Manager handles the connection & disconnection to the database. @@ -153,18 +174,14 @@ def connect_to_database( | # uses explicit `dbapiModuleName` and `dbName` but uses the `dbUsername` and `dbPassword` in './resources/db.cfg' | | Connect To Database | psycopg2 | my_db_test | """ - - if dbConfigFile is None: - dbConfigFile = "./resources/db.cfg" - config = ConfigParser.ConfigParser() - config.read([dbConfigFile]) - - dbapiModuleName = dbapiModuleName or config.get(alias, "dbapiModuleName") - dbName = dbName or config.get(alias, "dbName") - dbUsername = dbUsername or config.get(alias, "dbUsername") - dbPassword = dbPassword if dbPassword is not None else config.get(alias, "dbPassword") - dbHost = dbHost or config.get(alias, "dbHost") or "localhost" - dbPort = int(dbPort or config.get(alias, "dbPort")) + config = ConfigReader(dbConfigFile, alias) + + dbapiModuleName = dbapiModuleName or config.get("dbapiModuleName") + dbName = dbName or config.get("dbName") + dbUsername = dbUsername or config.get("dbUsername") + dbPassword = dbPassword if dbPassword is not None else config.get("dbPassword") + dbHost = dbHost or config.get("dbHost") or "localhost" + dbPort = int(dbPort if dbPort is not None else config.get("dbPort")) if dbapiModuleName == "excel" or dbapiModuleName == "excelrw": db_api_module_name = "pyodbc" diff --git a/test/tests/__init__.py b/test/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/tests/utests/__init__.py b/test/tests/utests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/tests/utests/test_connection_manager.py b/test/tests/utests/test_connection_manager.py new file mode 100644 index 00000000..bbc9ea26 --- /dev/null +++ b/test/tests/utests/test_connection_manager.py @@ -0,0 +1,48 @@ +import re +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from DatabaseLibrary.connection_manager import ConnectionManager + +TEST_DATA = Path(__file__).parent / "test_data" + + +class TestConnectWithConfigFile: + def test_connect_with_empty_config(self): + conn_manager = ConnectionManager() + config_path = str(TEST_DATA / "empty.cfg") + with pytest.raises(ValueError, match=re.escape("Configuration file does not have [default] section.")): + conn_manager.connect_to_database("my_client", dbConfigFile=config_path) + + def test_connect_no_params_no_config(self): + conn_manager = ConnectionManager() + with pytest.raises(ValueError, match="Required 'dbName' parameter was not provided in keyword arguments."): + conn_manager.connect_to_database("my_client") + + def test_connect_missing_option(self): + conn_manager = ConnectionManager() + config_path = str(TEST_DATA / "no_option.cfg") + with pytest.raises( + ValueError, + match="Required 'dbPassword' parameter missing in both keyword arguments and configuration file.", + ): + conn_manager.connect_to_database("my_client", dbConfigFile=config_path) + + def test_aliased_section(self): + conn_manager = ConnectionManager() + config_path = str(TEST_DATA / "alias.cfg") + with patch("importlib.import_module", new=MagicMock()) as client: + conn_manager.connect_to_database( + "my_client", + dbUsername="name", + dbPassword="password", + dbHost="host", + dbPort=0, + dbConfigFile=config_path, + alias="alias2", + ) + client.return_value.connect.assert_called_with( + database="example", user="name", password="password", host="host", port=0 + ) diff --git a/test/tests/utests/test_data/alias.cfg b/test/tests/utests/test_data/alias.cfg new file mode 100644 index 00000000..06d0431c --- /dev/null +++ b/test/tests/utests/test_data/alias.cfg @@ -0,0 +1,2 @@ +[alias2] +dbName = example diff --git a/test/tests/utests/test_data/empty.cfg b/test/tests/utests/test_data/empty.cfg new file mode 100644 index 00000000..e69de29b diff --git a/test/tests/utests/test_data/no_option.cfg b/test/tests/utests/test_data/no_option.cfg new file mode 100644 index 00000000..4e0db9e8 --- /dev/null +++ b/test/tests/utests/test_data/no_option.cfg @@ -0,0 +1,3 @@ +[default] +dbName = example +dbUsername = example \ No newline at end of file