def test_valid_settings(): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['VALIDATE_GUID'] = False mocked_settings['GUID_HEADER_NAME'] = 'Correlation-ID-TEST' mocked_settings['RETURN_HEADER'] = False mocked_settings['EXPOSE_HEADER'] = False with override_settings(DJANGO_GUID=mocked_settings): assert not Settings().validate_guid assert Settings().guid_header_name == 'Correlation-ID-TEST' assert not Settings().return_header
def test_valid_settings(monkeypatch): monkeypatch.setattr( django_settings, 'DJANGO_GUID', { 'VALIDATE_GUID': False, 'GUID_HEADER_NAME': 'Correlation-ID-TEST', 'RETURN_HEADER': False, 'EXPOSE_HEADER': False, }, ) assert not Settings().VALIDATE_GUID assert Settings().GUID_HEADER_NAME == 'Correlation-ID-TEST' assert not Settings().RETURN_HEADER
def test_sentry_integration(client, caplog, mocker, monkeypatch): """ Tests the sentry integration """ mock_scope = mocker.patch.object(Scope, 'set_tag') with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.middleware.settings', settings) client.get( '/api', **{'HTTP_Correlation-ID': '97c304252fd14b25b72d6aee31565842'}) expected = [ (None, 'sync middleware called'), (None, 'Correlation-ID found in the header: 97c304252fd14b25b72d6aee31565842' ), (None, '97c304252fd14b25b72d6aee31565842 is a valid GUID'), ('97c304252fd14b25b72d6aee31565842', 'Running integration: `SentryIntegration`'), ('97c304252fd14b25b72d6aee31565842', 'Setting Sentry transaction_id to 97c304252fd14b25b72d6aee31565842' ), ('97c304252fd14b25b72d6aee31565842', 'This is a DRF view log, and should have a GUID.'), ('97c304252fd14b25b72d6aee31565842', 'Some warning in a function'), ('97c304252fd14b25b72d6aee31565842', 'Running tear down for integration: `SentryIntegration`'), ('97c304252fd14b25b72d6aee31565842', 'Received signal `request_finished`, clearing guid'), ] mock_scope.assert_called_with('transaction_id', '97c304252fd14b25b72d6aee31565842') assert [(x.correlation_id, x.message) for x in caplog.records] == expected
def test_no_return_header_and_drf_url(client, caplog, mock_uuid, monkeypatch): """ Tests that it does not return the GUID if RETURN_HEADER is false. This test also tests a DRF response, just to confirm everything works in both worlds. """ mocked_settings = { 'GUID_HEADER_NAME': 'Correlation-ID', 'VALIDATE_GUID': True, 'INTEGRATIONS': [], 'IGNORE_URLS': ['no-guid'], 'RETURN_HEADER': False, } with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.middleware.settings', settings) response = client.get('/api') expected = [ ('sync middleware called', None), ( 'Header `Correlation-ID` was not found in the incoming request. Generated new GUID: 704ae5472cae4f8daa8f2cc5a5a8mock', None, ), ('This is a DRF view log, and should have a GUID.', '704ae5472cae4f8daa8f2cc5a5a8mock'), ('Some warning in a function', '704ae5472cae4f8daa8f2cc5a5a8mock'), ('Received signal `request_finished`, clearing guid', '704ae5472cae4f8daa8f2cc5a5a8mock'), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected assert not response.get('Correlation-ID')
def test_invalid_setting(monkeypatch): monkeypatch.setattr(django_settings, 'DJANGO_GUID', {'invalid_setting': 'some_value'}) with pytest.raises( ImproperlyConfigured, match='invalid_setting is not a valid setting for django_guid'): Settings()
def test_invalid_guid(): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['VALIDATE_GUID'] = 'string' with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='VALIDATE_GUID must be a boolean'): Settings().validate()
def test_invalid_expose_header_setting(): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['EXPOSE_HEADER'] = 'string' with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='EXPOSE_HEADER must be a boolean'): Settings().validate()
def test_cleanup_signal(client, caplog, monkeypatch): """ Tests that a request cleans up a request after finishing. :param client: Django client :param caplog: Caplog fixture """ from django.conf import settings as django_settings mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['VALIDATE_GUID'] = False with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.utils.settings', settings) client.get('/', **{'HTTP_Correlation-ID': 'bad-guid'}) client.get('/', **{'HTTP_Correlation-ID': 'another-bad-guid'}) expected = [ # First request ('sync middleware called', None), ('Correlation-ID found in the header: bad-guid', None), ('Returning ID from header without validating it as a GUID', None), ('This log message should have a GUID', 'bad-guid'), ('Some warning in a function', 'bad-guid'), ('Received signal `request_finished`, clearing guid', 'bad-guid'), # Second request ('sync middleware called', None), ('Correlation-ID found in the header: another-bad-guid', None), ('Returning ID from header without validating it as a GUID', None), ('This log message should have a GUID', 'another-bad-guid'), ('Some warning in a function', 'another-bad-guid'), ('Received signal `request_finished`, clearing guid', 'another-bad-guid'), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected
def test_invalid_header_name(): mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['GUID_HEADER_NAME'] = True with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='GUID_HEADER_NAME must be a string'): Settings().validate()
def test_bad_integrations_type(monkeypatch): for item in [{}, '', 2, None, -2]: monkeypatch.setattr(django_settings, 'DJANGO_GUID', {'INTEGRATIONS': item}) with pytest.raises(ImproperlyConfigured, match='INTEGRATIONS must be an array'): Settings()
def test_invalid_skip_guid_setting(monkeypatch): """ Assert that a deprecation warning is called when settings are instantiated with SKIP_CLEANUP == True or False """ monkeypatch.setattr(django_settings, 'DJANGO_GUID', {'SKIP_CLEANUP': True}) with pytest.deprecated_call(): Settings()
def test_request_with_invalid_correlation_id_without_validation( client, caplog, monkeypatch): """ Tests that a request with an invalid GUID is replaced when VALIDATE_GUID is False. :param client: Django client :param caplog: Caplog fixture """ mocked_settings = { 'GUID_HEADER_NAME': 'Correlation-ID', 'VALIDATE_GUID': False, 'INTEGRATIONS': [], 'IGNORE_URLS': ['no-guid'], } with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.utils.settings', settings) client.get('/', **{'HTTP_Correlation-ID': 'bad-guid'}) expected = [ ('sync middleware called', None), ('Correlation-ID found in the header: bad-guid', None), ('Returning ID from header without validating it as a GUID', None), ('This log message should have a GUID', 'bad-guid'), ('Some warning in a function', 'bad-guid'), ('Received signal `request_finished`, clearing guid', 'bad-guid'), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected
def test_non_callable_methods(monkeypatch, subtests): """ Tests that an exception is raised when any of the integration base methods are non-callable. """ from django_guid.integrations import SentryIntegration from django.conf import settings from django_guid.config import Settings mock_integration = SentryIntegration() to_test = [ { 'function_name': 'cleanup', 'error': 'Integration method `cleanup` needs to be made callable for `SentryIntegration`.', }, { 'function_name': 'run', 'error': 'Integration method `run` needs to be made callable for `SentryIntegration`.', }, { 'function_name': 'setup', 'error': 'Integration method `setup` needs to be made callable for `SentryIntegration`.', }, ] for test in to_test: setattr(mock_integration, test.get('function_name'), 'test') monkeypatch.setattr(settings, 'DJANGO_GUID', {'INTEGRATIONS': [mock_integration]}) with subtests.test(msg=f'Testing function {test.get("function_name")}'): with pytest.raises(ImproperlyConfigured, match=test.get('error')): Settings()
def test_not_array_ignore_urls(): for setting in [{}, '', 2, None, -2]: mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['IGNORE_URLS'] = setting with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='IGNORE_URLS must be an array'): Settings().validate()
def test_bad_integrations_type(): for setting in [{}, '', 2, None, -2]: mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = setting with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises(ImproperlyConfigured, match='INTEGRATIONS must be an array'): Settings().validate()
def test_not_string_in_igore_urls(): for setting in ['api/v1/test', 'api/v1/othertest', True], [1, 2, 'yup']: mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['IGNORE_URLS'] = setting with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises( ImproperlyConfigured, match='IGNORE_URLS must be an array of strings'): Settings().validate()
def test_uuid_len_fail(): for setting in [True, False, {}, [], 'asd', -1, 0, 33]: mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['UUID_LENGTH'] = setting with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises( ImproperlyConfigured, match='UUID_LENGTH must be an integer and be between 1-32' ): Settings().validate()
def test_expose_header_return_header_true(client, monkeypatch): """ Tests that it does return the Access-Control-Allow-Origin when EXPOSE_HEADER is set to True and RETURN_HEADER is True """ from django.conf import settings as django_settings mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['EXPOSE_HEADER'] = True with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.middleware.settings', settings) response = client.get('/api') assert response.get('Access-Control-Expose-Headers')
def test_worker_prerun_guid_does_not_exist(monkeypatch, mocker: MockerFixture, mock_uuid): """ Tests that a GUID is set if it does not exist """ mock_task = mocker.Mock() mock_task.request = {'Correlation-ID': None} mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=False)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) worker_prerun(mock_task) assert get_guid() == '704ae5472cae4f8daa8f2cc5a5a8mock'
def test_worker_prerun_guid_exists(monkeypatch, mocker: MockerFixture, two_unique_uuid4): """ Tests that GUID is set to the GUID if a GUID exists in the task object. """ mock_task = mocker.Mock() mock_task.request = {'Correlation-ID': '704ae5472cae4f8daa8f2cc5a5a8mock'} mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=False)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) worker_prerun(mock_task) assert get_guid() == '704ae5472cae4f8daa8f2cc5a5a8mock'
def test_cleanup_method_not_accepting_kwargs(monkeypatch, client): """ Tests that an exception is raised when the run method doesn't accept kwargs. """ from django_guid.integrations import SentryIntegration from django.conf import settings from django_guid.config import Settings class BadIntegration(SentryIntegration): def cleanup(self, guid): pass monkeypatch.setattr(settings, 'DJANGO_GUID', {'INTEGRATIONS': [BadIntegration()]}) with pytest.raises(ImproperlyConfigured, match='Integration method `cleanup` must accept keyword arguments '): Settings()
def test_cleanup(monkeypatch, mocker: MockerFixture): """ Test that cleanup works as expected """ set_guid('123') celery_current.set('123') celery_parent.set('123') mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=True)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) clean_up(task=mocker.Mock()) assert [get_guid(), celery_current.get(), celery_parent.get()] == [None, None, None]
def test_sentry_validation(client, monkeypatch): """ Tests that the package handles multiple header values by defaulting to one and logging a warning. """ import sys from django_guid.integrations import SentryIntegration from django_guid.config import Settings from django.conf import settings # Mock away the sentry_sdk dependency sys.modules['sentry_sdk'] = None monkeypatch.setattr(settings, 'DJANGO_GUID', {'INTEGRATIONS': [SentryIntegration()]}) with pytest.raises( ImproperlyConfigured, match='The package `sentry-sdk` is required for extending your tracing IDs to Sentry. ' 'Please run `pip install sentry-sdk` if you wish to include this integration.', ): Settings()
def test_missing_run_method(monkeypatch, client): """ Tests that an exception is raised when the run method has not been defined. """ from django_guid.integrations import SentryIntegration monkeypatch.delattr(SentryIntegration, 'run') mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [SentryIntegration()] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.middleware.settings', settings) with pytest.raises( ImproperlyConfigured, match= 'The integration `SentryIntegration` is missing a `run` method' ): client.get('/api')
def test_cleanup_method_not_accepting_kwargs(client): """ Tests that an exception is raised when the run method doesn't accept kwargs. """ from django_guid.config import Settings from django_guid.integrations import SentryIntegration class BadIntegration(SentryIntegration): def cleanup(self, guid): pass mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [BadIntegration()] with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises( ImproperlyConfigured, match= 'Integration method `cleanup` must accept keyword arguments '): Settings().validate()
def test_dont_set_transaction_id(monkeypatch, caplog): """ Tests that the `configure_scope()` is not executed, given `sentry_integration=False` in CeleryIntegration """ logger = logging.getLogger('django_guid.celery') logger.addHandler(caplog.handler) mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [ CeleryIntegration(sentry_integration=False) ] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) guid = generate_guid() set_transaction_id(guid) logger.removeHandler(caplog.handler) assert f'Setting Sentry transaction_id to {guid}' not in [ record.message for record in caplog.records ]
def test_worker_prerun_guid_log_parent_with_origin(monkeypatch, mocker: MockerFixture, mock_uuid_two_unique): """ Tests that depth works when there is an origin """ from django_guid.integrations.celery.signals import parent_header mock_task = mocker.Mock() mock_task.request = { 'Correlation-ID': None, parent_header: '1234' } # No origin mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=True)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) worker_prerun(mock_task) assert get_guid() == '704ae5472cae4f8daa8f2cc5a5a8mock' assert celery_current.get() == 'c494886651cd4baaa8654e4d24a8mock' assert celery_parent.get() == '1234'
def test_sentry_validation(client): """ Tests that the package handles multiple header values by defaulting to one and logging a warning. """ # Mock away the sentry_sdk dependency backup = None if 'sentry_sdk' in sys.modules: backup = sys.modules['sentry_sdk'] sys.modules['sentry_sdk'] = None with override_settings(DJANGO_GUID=mocked_settings): with pytest.raises( ImproperlyConfigured, match= 'The package `sentry-sdk` is required for extending your tracing IDs to Sentry. ' 'Please run `pip install sentry-sdk` if you wish to include this integration.', ): Settings().validate() # Put it back in - otherwise a bunch of downstream tests break if backup: sys.modules['sentry_sdk'] = backup
def test_task_publish_includes_correct_depth_headers(monkeypatch): """ Test log_parent True """ mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=True)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) headers = {} publish_task_from_worker_or_request(headers=headers) # The parent header should not be in headers, because # There should be no celery_current context assert parent_header not in headers for correlation_id in ['test', 123, -1]: headers = {} celery_current.set(correlation_id) publish_task_from_worker_or_request(headers=headers) # Here the celery-parent-id header should exist assert headers[parent_header] == correlation_id
def test_task_publish_includes_correct_headers(monkeypatch): """ It's important that we include the correct headers when publishing a task to the celery worker pool, otherwise there's no transfer of state. """ # Mocking overhead mocked_settings = deepcopy(django_settings.DJANGO_GUID) mocked_settings['INTEGRATIONS'] = [CeleryIntegration(log_parent=False)] with override_settings(DJANGO_GUID=mocked_settings): settings = Settings() monkeypatch.setattr('django_guid.integrations.celery.signals.settings', settings) # Actual testing for correlation_id in [None, 'test', 123, -1]: # Set the id in our context var set_guid(correlation_id) # Run signal with empty headers headers = {} publish_task_from_worker_or_request(headers=headers) # Makes sure the returned headers contain the correct result assert headers[settings.guid_header_name] == correlation_id