Skip to content

Commit 692b604

Browse files
beniwohlibasepi
andauthored
fix handling of required keys in configuration (#927)
* fix handling of required keys in configuration If a key wasn't provided, we didn't check if it was required or not. This led to a problem that if no `service_name` was configured, we'd send invalid data to APM Server. fixes #921 * when copying configs, discard errors of the fresh copy * raise exception explicitly in test we seem to have relied on buggy behaviour without noticing here before * raise a proper exception in the test instead of relying on buggy behavior * Add changelog Co-authored-by: Colton Myers <[email protected]>
1 parent 08b0641 commit 692b604

File tree

4 files changed

+36
-10
lines changed

4 files changed

+36
-10
lines changed

CHANGELOG.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ endif::[]
4343
[float]
4444
===== Bug fixes
4545
46+
* Fix validation of config to properly require `required` config items. {pull}927[#927]
47+
4648
4749
[[release-notes-5.x]]
4850
=== Python Agent version 5.x

elasticapm/conf/__init__.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ def __get__(self, instance, owner):
6969
else:
7070
return self.default
7171

72-
def __set__(self, instance, value):
73-
value = self._validate(instance, value)
74-
instance._values[self.dict_key] = value
72+
def __set__(self, config_instance, value):
73+
value = self._validate(config_instance, value)
74+
config_instance._values[self.dict_key] = value
7575

7676
def _validate(self, instance, value):
7777
if value is None and self.required:
@@ -237,6 +237,11 @@ def update(self, config_dict=None, env_dict=None, inline_dict=None):
237237
setattr(self, field, new_value)
238238
except ConfigurationError as e:
239239
self._errors[e.field_name] = str(e)
240+
# if a field has not been provided by any config source, we have to check separately if it is required
241+
if config_value.required and getattr(self, field) is None:
242+
self._errors[config_value.dict_key] = "Configuration error: value for {} is required.".format(
243+
config_value.dict_key
244+
)
240245

241246
@property
242247
def values(self):
@@ -250,6 +255,12 @@ def values(self, values):
250255
def errors(self):
251256
return self._errors
252257

258+
def copy(self):
259+
c = self.__class__()
260+
c._errors = {}
261+
c.values = self.values.copy()
262+
return c
263+
253264

254265
class Config(_ConfigBase):
255266
service_name = _ConfigValue("SERVICE_NAME", validators=[RegexValidator("^[a-zA-Z0-9 _-]+$")], required=True)
@@ -393,8 +404,7 @@ def update(self, version, **config):
393404
:param config: a key/value map of new configuration
394405
:return: configuration errors, if any
395406
"""
396-
new_config = Config()
397-
new_config.values = self._config.values.copy()
407+
new_config = self._config.copy()
398408

399409
# pass an empty env dict to ensure the environment doesn't get precedence
400410
new_config.update(inline_dict=config, env_dict={})

tests/config/tests.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,17 @@ def test_capture_body_mapping(val, expected):
301301
def test_is_recording(enabled, recording, is_recording):
302302
c = Config(inline_dict={"enabled": enabled, "recording": recording, "service_name": "foo"})
303303
assert c.is_recording is is_recording
304+
305+
306+
def test_required_is_checked_if_field_not_provided():
307+
class MyConfig(_ConfigBase):
308+
this_one_is_required = _ConfigValue("this_one_is_required", type=int, required=True)
309+
this_one_isnt = _ConfigValue("this_one_isnt", type=int, required=False)
310+
311+
assert MyConfig({"this_one_is_required": None}).errors
312+
assert MyConfig({}).errors
313+
assert MyConfig({"this_one_isnt": 1}).errors
314+
315+
c = MyConfig({"this_one_is_required": 1})
316+
c.update({"this_one_isnt": 0})
317+
assert not c.errors

tests/contrib/django/django_tests.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,18 +1313,18 @@ def test_test_exception(urlopen_mock):
13131313
assert "Success! We tracked the error successfully!" in output
13141314

13151315

1316-
@pytest.mark.parametrize(
1317-
"django_elasticapm_client", [{"transport_class": "elasticapm.transport.http.Transport"}], indirect=True
1318-
)
1319-
def test_test_exception_fails(django_elasticapm_client):
1316+
@mock.patch("elasticapm.transport.http.Transport.send")
1317+
def test_test_exception_fails(mock_send):
13201318
stdout = compat.StringIO()
1319+
mock_send.side_effect = Exception("boom")
13211320
with override_settings(
1322-
ELASTIC_APM={"TRANSPORT_CLASS": "elasticapm.transport.http.Transport"},
1321+
ELASTIC_APM={"TRANSPORT_CLASS": "elasticapm.transport.http.Transport", "SERVICE_NAME": "testapp"},
13231322
**middleware_setting(django.VERSION, ["foo", "elasticapm.contrib.django.middleware.TracingMiddleware"])
13241323
):
13251324
call_command("elasticapm", "test", stdout=stdout, stderr=stdout)
13261325
output = stdout.getvalue()
13271326
assert "Oops" in output
1327+
assert "boom" in output
13281328

13291329

13301330
def test_tracing_middleware_uses_test_client(client, django_elasticapm_client):

0 commit comments

Comments
 (0)