Skip to content

Commit 8d57811

Browse files
committed
test: update unit tests so pydantic-settings reads existing files
patching pydantic-settings directly was hiding the files' parsing in tests
1 parent abeaf2c commit 8d57811

File tree

4 files changed

+177
-91
lines changed

4 files changed

+177
-91
lines changed
Lines changed: 19 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import sys
2+
from pathlib import Path
23
from types import SimpleNamespace
4+
35
import pytest
46

57

@@ -11,8 +13,11 @@ def mock_main_path(monkeypatch):
1113
sys.modules, "__main__", SimpleNamespace(__file__="/app/src/main.py")
1214
)
1315

16+
1417
@pytest.fixture
15-
def mock_basic_environment(monkeypatch):
18+
def mock_environment(monkeypatch):
19+
"""Mock `os.environ` for `_SettingsLoader` and `BaseConnectorSettings` calls."""
20+
1621
monkeypatch.setenv("OPENCTI_URL", "http://localhost:8080")
1722
monkeypatch.setenv("OPENCTI_TOKEN", "changeme")
1823
monkeypatch.setenv("CONNECTOR_ID", "connector-poc--uid")
@@ -23,60 +28,26 @@ def mock_basic_environment(monkeypatch):
2328

2429

2530
@pytest.fixture
26-
def mock_yaml_file_presence(monkeypatch):
27-
def is_file(self):
28-
if self.name == "config.yml":
29-
return True
30-
return False
31+
def mock_config_yml_file_presence(monkeypatch):
32+
"""Mock the path of `config.yml` for `_SettingsLoader` and `BaseConnectorSettings` calls."""
3133

32-
monkeypatch.setattr("pathlib.Path.is_file", is_file)
33-
34-
35-
@pytest.fixture
36-
def mock_env_file_presence(monkeypatch):
37-
def is_file(self):
38-
if self.name == ".env":
39-
return True
40-
return False
41-
42-
monkeypatch.setattr("pathlib.Path.is_file", is_file)
43-
44-
45-
@pytest.fixture
46-
def mock_yaml_config_settings_read_files(monkeypatch):
47-
def read_files(_, __):
48-
return {
49-
"connector": {
50-
"duration_period": "PT5M",
51-
"id": "connector-poc--uid",
52-
"log_level": "error",
53-
"name": "Test Connector",
54-
"scope": "test",
55-
},
56-
"opencti": {"token": "changeme", "url": "http://localhost:8080"},
57-
}
34+
def get_config_yml_file_path():
35+
return Path(__file__).parent / "data" / "config.test.yml"
5836

5937
monkeypatch.setattr(
60-
"pydantic_settings.YamlConfigSettingsSource._read_files", read_files
38+
"connectors_sdk.settings.base_settings._SettingsLoader._get_config_yml_file_path",
39+
get_config_yml_file_path,
6140
)
6241

6342

6443
@pytest.fixture
65-
def mock_env_config_settings_read_env_files(monkeypatch):
66-
def _read_env_files(self):
67-
if self.settings_cls.__name__ == "SettingsLoader":
68-
return {
69-
"opencti": {"url": "http://localhost:8080", "token": "changeme"},
70-
"connector": {
71-
"id": "connector-poc--uid",
72-
"name": "Test Connector",
73-
"duration_period": "PT5M",
74-
"log_level": "error",
75-
"scope": "test",
76-
},
77-
}
78-
return {}
44+
def mock_dot_env_file_presence(monkeypatch):
45+
"""Mock the path of `.env` for `_SettingsLoader` and `BaseConnectorSettings` calls."""
46+
47+
def get_dot_env_file_path():
48+
return Path(__file__).parent / "data" / ".env.test"
7949

8050
monkeypatch.setattr(
81-
"pydantic_settings.DotEnvSettingsSource._load_env_vars", _read_env_files
51+
"connectors_sdk.settings.base_settings._SettingsLoader._get_dot_env_file_path",
52+
get_dot_env_file_path,
8253
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
OPENCTI_URL=http://localhost:8080
2+
OPENCTI_TOKEN=changeme
3+
CONNECTOR_ID=connector-poc--uid
4+
CONNECTOR_NAME=Test Connector
5+
CONNECTOR_SCOPE=test
6+
CONNECTOR_LOG_LEVEL=error
7+
CONNECTOR_DURATION_PERIOD=PT5M
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
opencti:
2+
url: http://localhost:8080
3+
token: changeme
4+
5+
connector:
6+
id: connector-poc--uid
7+
name: Test Connector
8+
scope: test
9+
log_level: error
10+
duration_period: PT5M
11+

connectors-sdk/tests/test_settings/test_base_settings.py

Lines changed: 140 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,72 @@ def is_file(self: Path) -> bool:
7878
dot_env_file_path = _SettingsLoader._get_dot_env_file_path()
7979

8080
assert dot_env_file_path == Path("/app/.env").resolve()
81+
82+
83+
def test_settings_loader_should_parse_config_yml_file(mock_config_yml_file_presence):
84+
"""
85+
Test that `_SettingsLoader()` parses config vars in `config.yml`.
86+
For testing purpose, the path of `config.yml` file is `tests/test_settings/data/config.test.yml`.
87+
"""
88+
settings_loader = _SettingsLoader()
89+
settings_dict = settings_loader.model_dump()
90+
91+
assert settings_dict == {
92+
"opencti": {
93+
"url": "http://localhost:8080",
94+
"token": "changeme",
95+
},
96+
"connector": {
97+
"id": "connector-poc--uid",
98+
"name": "Test Connector",
99+
"duration_period": "PT5M",
100+
"log_level": "error",
101+
"scope": "test",
102+
},
103+
}
104+
105+
106+
def test_settings_loader_should_parse_dot_env_file(mock_dot_env_file_presence):
107+
"""
108+
Test that `_SettingsLoader()` parses env vars in `.env`.
109+
For testing purpose, the path of `.env` file is `tests/test_settings/data/.env.test`.
110+
"""
111+
112+
settings_loader = _SettingsLoader()
113+
settings_dict = settings_loader.model_dump()
114+
115+
assert settings_dict == {
116+
"opencti_url": "http://localhost:8080",
117+
"opencti_token": "changeme",
118+
"connector_id": "connector-poc--uid",
119+
"connector_name": "Test Connector",
120+
"connector_duration_period": "PT5M",
121+
"connector_log_level": "error",
122+
"connector_scope": "test",
123+
}
124+
125+
126+
def test_settings_loader_should_parse_os_environ(mock_environment):
127+
"""
128+
Test that `_SettingsLoader()` parses env vars from `os.environ`.
129+
For testing purpose, `os.environ` is patched.
130+
"""
131+
81132
settings_loader = _SettingsLoader()
82133
settings_dict = settings_loader.model_dump()
83134

84135
assert settings_dict == {}
85136

86137

87-
def test_should_create_settings_loader_from_model(mock_basic_environment):
138+
def test_settings_loader_should_parse_config_yml_from_model(
139+
mock_config_yml_file_presence,
140+
):
141+
"""
142+
Test that `_SettingsLoader.build_loader_from_model` returns a `BaseSettings` subclass
143+
capable of parsing `config.yml` according to the given `BaseModel`.
144+
For testing purpose, the path of `config.yml` file is `tests/test_settings/data/config.test.yml`.
145+
"""
146+
88147
settings_loader = _SettingsLoader.build_loader_from_model(BaseConnectorSettings)
89148
settings_dict = settings_loader().model_dump()
90149

@@ -96,39 +155,31 @@ def test_should_create_settings_loader_from_model(mock_basic_environment):
96155
assert settings_dict["connector"]["log_level"] == "error"
97156

98157

99-
def test_should_create_base_connector_settings(mock_basic_environment):
100-
settings = BaseConnectorSettings()
101-
assert settings.opencti.url == HttpUrl("http://localhost:8080/")
102-
assert settings.opencti.token == "changeme"
103-
assert settings.connector.id == "connector-poc--uid"
104-
assert settings.connector.name == "Test Connector"
105-
assert settings.connector.scope == ["test"]
106-
assert settings.connector.log_level == "error"
107-
158+
def test_settings_loader_should_parse_dot_env_from_model(mock_dot_env_file_presence):
159+
"""
160+
Test that `_SettingsLoader.build_loader_from_model` returns a `BaseSettings` subclass
161+
capable of parsing `.env` according to the given `BaseModel`.
162+
For testing purpose, the path of `.env` file is `tests/test_settings/data/.env.test`.
163+
"""
108164

109-
def test_should_fail_with_missing_mandatory_env_vars():
110-
with pytest.raises(ConfigValidationError):
111-
BaseConnectorSettings()
165+
settings_loader = _SettingsLoader.build_loader_from_model(BaseConnectorSettings)
166+
settings_dict = settings_loader().model_dump()
112167

168+
assert settings_dict["opencti"]["url"] == "http://localhost:8080"
169+
assert settings_dict["opencti"]["token"] == "changeme"
170+
assert settings_dict["connector"]["id"] == "connector-poc--uid"
171+
assert settings_dict["connector"]["name"] == "Test Connector"
172+
assert settings_dict["connector"]["scope"] == "test"
173+
assert settings_dict["connector"]["log_level"] == "error"
113174

114-
def test_should_dump_opencti_model(mock_basic_environment):
115-
settings = BaseConnectorSettings()
116-
opencti_dict = settings.to_helper_config()
117-
assert opencti_dict == {
118-
"connector": {
119-
"duration_period": "PT5M",
120-
"id": "connector-poc--uid",
121-
"log_level": "error",
122-
"name": "Test Connector",
123-
"scope": "test",
124-
},
125-
"opencti": {"token": "changeme", "url": "http://localhost:8080/"},
126-
}
127175

176+
def test_settings_loader_should_parse_os_environ_from_model(mock_environment):
177+
"""
178+
Test that `_SettingsLoader.build_loader_from_model` returns a `BaseSettings` subclass
179+
capable of parsing `os.environ` according to the given `BaseModel`.
180+
For testing purpose, `os.environ` is patched.
181+
"""
128182

129-
def test_should_create_yml_settings_loader_from_model(
130-
mock_yaml_file_presence, mock_yaml_config_settings_read_files
131-
):
132183
settings_loader = _SettingsLoader.build_loader_from_model(BaseConnectorSettings)
133184
settings_dict = settings_loader().model_dump()
134185

@@ -140,10 +191,16 @@ def test_should_create_yml_settings_loader_from_model(
140191
assert settings_dict["connector"]["log_level"] == "error"
141192

142193

143-
def test_should_load_settings_from_yaml_file(
144-
mock_yaml_file_presence, mock_yaml_config_settings_read_files
194+
def test_base_connector_settings_should_validate_settings_from_config_yaml_file(
195+
mock_config_yml_file_presence,
145196
):
197+
"""
198+
Test that `BaseConnectorSettings` casts and validates config vars in `config.yml`.
199+
For testing purpose, the path of `config.yml` file is `tests/test_settings/data/config.test.yml`.
200+
"""
201+
146202
settings = BaseConnectorSettings()
203+
147204
assert settings.opencti.url == HttpUrl("http://localhost:8080/")
148205
assert settings.opencti.token == "changeme"
149206
assert settings.connector.id == "connector-poc--uid"
@@ -152,27 +209,67 @@ def test_should_load_settings_from_yaml_file(
152209
assert settings.connector.log_level == "error"
153210

154211

155-
def test_should_create_dot_env_settings_loader_from_model(
156-
mock_env_file_presence, mock_env_config_settings_read_env_files
212+
def test_base_connector_settings_should_validate_settings_from_dot_env_file(
213+
mock_dot_env_file_presence,
157214
):
158-
settings_loader = _SettingsLoader.build_loader_from_model(BaseConnectorSettings)
159-
settings_dict = settings_loader().model_dump()
215+
"""
216+
Test that `BaseConnectorSettings` casts and validates env vars in `.env`.
217+
For testing purpose, the path of `.env` file is `tests/test_settings/data/.env.test`.
218+
"""
160219

161-
assert settings_dict["opencti"]["url"] == "http://localhost:8080"
162-
assert settings_dict["opencti"]["token"] == "changeme"
163-
assert settings_dict["connector"]["id"] == "connector-poc--uid"
164-
assert settings_dict["connector"]["name"] == "Test Connector"
165-
assert settings_dict["connector"]["scope"] == "test"
166-
assert settings_dict["connector"]["log_level"] == "error"
220+
settings = BaseConnectorSettings()
221+
222+
assert settings.opencti.url == HttpUrl("http://localhost:8080/")
223+
assert settings.opencti.token == "changeme"
224+
assert settings.connector.id == "connector-poc--uid"
225+
assert settings.connector.name == "Test Connector"
226+
assert settings.connector.scope == ["test"]
227+
assert settings.connector.log_level == "error"
167228

168229

169-
def test_should_load_settings_from_env_file(
170-
mock_env_file_presence, mock_env_config_settings_read_env_files
230+
def test_base_connector_settings_should_validate_settings_from_os_environ(
231+
mock_environment,
171232
):
233+
"""
234+
Test that `BaseConnectorSettings` casts and validates env vars in `os.environ`.
235+
For testing purpose, `os.environ` is patched.
236+
"""
237+
172238
settings = BaseConnectorSettings()
239+
173240
assert settings.opencti.url == HttpUrl("http://localhost:8080/")
174241
assert settings.opencti.token == "changeme"
175242
assert settings.connector.id == "connector-poc--uid"
176243
assert settings.connector.name == "Test Connector"
177244
assert settings.connector.scope == ["test"]
178245
assert settings.connector.log_level == "error"
246+
247+
248+
def test_base_connector_settings_should_raise_when_missing_mandatory_env_vars():
249+
"""Test that `BaseConnectorSettings` raises a `ValidationError` when no value is provided for required fields."""
250+
with pytest.raises(ConfigValidationError):
251+
BaseConnectorSettings()
252+
253+
254+
def test_base_connector_settings_should_provide_helper_config(mock_environment):
255+
"""
256+
Test that `BaseConnectorSettings().to_helper_config` returns a valid `config` dict for `pycti.OpenCTIConnectorHelper`.
257+
For testing purpose, `os.environ` is patched.
258+
"""
259+
260+
settings = BaseConnectorSettings()
261+
opencti_dict = settings.to_helper_config()
262+
263+
assert opencti_dict == {
264+
"connector": {
265+
"duration_period": "PT5M",
266+
"id": "connector-poc--uid",
267+
"log_level": "error",
268+
"name": "Test Connector",
269+
"scope": "test",
270+
},
271+
"opencti": {
272+
"token": "changeme",
273+
"url": "http://localhost:8080/",
274+
},
275+
}

0 commit comments

Comments
 (0)