Skip to content

Commit d562452

Browse files
authored
Merge pull request #197 from MarketSquare/fix/config_file_without_section
Fix and improve error handling for missing or invalid configuration file
2 parents f368032 + 7df2cca commit d562452

8 files changed

Lines changed: 151 additions & 17 deletions

File tree

.github/workflows/unit_tests.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
# This workflow will install Python dependencies
3+
# and run unit tests for given OSes
4+
5+
name: Unit tests
6+
7+
on: [push, pull_request]
8+
9+
jobs:
10+
build:
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
include:
15+
- os: 'ubuntu-latest'
16+
python-version: '3.7'
17+
rf-version: '3.2.2'
18+
- os: 'ubuntu-latest'
19+
python-version: '3.8'
20+
rf-version: '4.1.3'
21+
- os: 'ubuntu-latest'
22+
python-version: '3.9'
23+
rf-version: '5.0.1'
24+
- os: 'ubuntu-latest'
25+
python-version: '3.10'
26+
rf-version: '6.1.1'
27+
- os: 'ubuntu-latest'
28+
python-version: '3.11'
29+
rf-version: '6.1.1'
30+
- os: 'ubuntu-latest'
31+
python-version: '3.12'
32+
rf-version: '7.0a1'
33+
runs-on: ${{ matrix.os }}
34+
35+
steps:
36+
- name: Checkout
37+
uses: actions/checkout@v4
38+
with:
39+
fetch-depth: 2
40+
41+
- name: Set up Python ${{ matrix.python-version }}
42+
uses: actions/setup-python@v4
43+
with:
44+
python-version: ${{ matrix.python-version }}
45+
46+
- name: Install dependencies
47+
run: |
48+
python -m pip install --upgrade pip
49+
pip install robotframework==${{ matrix.rf-version }} coverage pytest
50+
pip install .
51+
52+
- name: Run unit tests with coverage
53+
run:
54+
coverage run -m pytest
55+
56+
- name: Codecov
57+
uses: codecov/codecov-action@v3
58+
with:
59+
name: ${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.rf-version }}

src/DatabaseLibrary/connection_manager.py

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,11 @@
1313
# limitations under the License.
1414

1515
import importlib
16+
from configparser import ConfigParser, NoOptionError, NoSectionError
1617
from dataclasses import dataclass
18+
from pathlib import Path
1719
from typing import Any, Dict, Optional
1820

19-
try:
20-
import ConfigParser
21-
except:
22-
import configparser as ConfigParser
23-
2421
from robot.api import logger
2522

2623

@@ -81,6 +78,35 @@ def __iter__(self):
8178
return iter(self._connections.values())
8279

8380

81+
class ConfigReader:
82+
def __init__(self, config_file: Optional[str], alias: str):
83+
if config_file is None:
84+
config_file = "./resources/db.cfg"
85+
self.alias = alias
86+
self.config = self._load_config(config_file)
87+
88+
@staticmethod
89+
def _load_config(config_file: str) -> Optional[ConfigParser]:
90+
config_path = Path(config_file)
91+
if not config_path.exists():
92+
return None
93+
config = ConfigParser()
94+
config.read([config_path])
95+
return config
96+
97+
def get(self, param: str) -> str:
98+
if self.config is None:
99+
raise ValueError(f"Required '{param}' parameter was not provided in keyword arguments.") from None
100+
try:
101+
return self.config.get(self.alias, param)
102+
except NoSectionError:
103+
raise ValueError(f"Configuration file does not have [{self.alias}] section.") from None
104+
except NoOptionError:
105+
raise ValueError(
106+
f"Required '{param}' parameter missing in both keyword arguments and configuration file."
107+
) from None
108+
109+
84110
class ConnectionManager:
85111
"""
86112
Connection Manager handles the connection & disconnection to the database.
@@ -153,18 +179,14 @@ def connect_to_database(
153179
| # uses explicit `dbapiModuleName` and `dbName` but uses the `dbUsername` and `dbPassword` in './resources/db.cfg' |
154180
| Connect To Database | psycopg2 | my_db_test |
155181
"""
156-
157-
if dbConfigFile is None:
158-
dbConfigFile = "./resources/db.cfg"
159-
config = ConfigParser.ConfigParser()
160-
config.read([dbConfigFile])
161-
162-
dbapiModuleName = dbapiModuleName or config.get(alias, "dbapiModuleName")
163-
dbName = dbName or config.get(alias, "dbName")
164-
dbUsername = dbUsername or config.get(alias, "dbUsername")
165-
dbPassword = dbPassword if dbPassword is not None else config.get(alias, "dbPassword")
166-
dbHost = dbHost or config.get(alias, "dbHost") or "localhost"
167-
dbPort = int(dbPort or config.get(alias, "dbPort"))
182+
config = ConfigReader(dbConfigFile, alias)
183+
184+
dbapiModuleName = dbapiModuleName or config.get("dbapiModuleName")
185+
dbName = dbName or config.get("dbName")
186+
dbUsername = dbUsername or config.get("dbUsername")
187+
dbPassword = dbPassword if dbPassword is not None else config.get("dbPassword")
188+
dbHost = dbHost or config.get("dbHost") or "localhost"
189+
dbPort = int(dbPort if dbPort is not None else config.get("dbPort"))
168190

169191
if dbapiModuleName == "excel" or dbapiModuleName == "excelrw":
170192
db_api_module_name = "pyodbc"

test/tests/__init__.py

Whitespace-only changes.

test/tests/utests/__init__.py

Whitespace-only changes.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import re
2+
from pathlib import Path
3+
from unittest.mock import MagicMock, patch
4+
5+
import pytest
6+
7+
from DatabaseLibrary.connection_manager import ConnectionManager
8+
9+
TEST_DATA = Path(__file__).parent / "test_data"
10+
11+
12+
class TestConnectWithConfigFile:
13+
def test_connect_with_empty_config(self):
14+
conn_manager = ConnectionManager()
15+
config_path = str(TEST_DATA / "empty.cfg")
16+
with pytest.raises(ValueError, match=re.escape("Configuration file does not have [default] section.")):
17+
conn_manager.connect_to_database("my_client", dbConfigFile=config_path)
18+
19+
def test_connect_no_params_no_config(self):
20+
conn_manager = ConnectionManager()
21+
with pytest.raises(ValueError, match="Required 'dbName' parameter was not provided in keyword arguments."):
22+
conn_manager.connect_to_database("my_client")
23+
24+
def test_connect_missing_option(self):
25+
conn_manager = ConnectionManager()
26+
config_path = str(TEST_DATA / "no_option.cfg")
27+
with pytest.raises(
28+
ValueError,
29+
match="Required 'dbPassword' parameter missing in both keyword arguments and configuration file.",
30+
):
31+
conn_manager.connect_to_database("my_client", dbConfigFile=config_path)
32+
33+
def test_aliased_section(self):
34+
conn_manager = ConnectionManager()
35+
config_path = str(TEST_DATA / "alias.cfg")
36+
with patch("importlib.import_module", new=MagicMock()) as client:
37+
conn_manager.connect_to_database(
38+
"my_client",
39+
dbUsername="name",
40+
dbPassword="password",
41+
dbHost="host",
42+
dbPort=0,
43+
dbConfigFile=config_path,
44+
alias="alias2",
45+
)
46+
client.return_value.connect.assert_called_with(
47+
database="example", user="name", password="password", host="host", port=0
48+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[alias2]
2+
dbName = example

test/tests/utests/test_data/empty.cfg

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[default]
2+
dbName = example
3+
dbUsername = example

0 commit comments

Comments
 (0)