diff --git a/cilium/tests/conftest.py b/cilium/tests/conftest.py index caa56b731ba88..e20a4f6f0760b 100644 --- a/cilium/tests/conftest.py +++ b/cilium/tests/conftest.py @@ -3,10 +3,10 @@ # Licensed under a 3-clause BSD style license (see LICENSE) import os -import mock import pytest from datadog_checks.base.utils.common import get_docker_hostname +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.cilium import CiliumCheck from datadog_checks.dev import run_command from datadog_checks.dev.kind import kind_run @@ -198,28 +198,18 @@ def operator_instance_use_openmetrics(): @pytest.fixture() -def mock_agent_data(): +def mock_agent_data(mock_openmetrics_http): f_name = os.path.join(os.path.dirname(__file__), "fixtures", "agent_metrics.txt") with open(f_name, "r") as f: text_data = f.read() - with mock.patch( - 'requests.Session.get', - return_value=mock.MagicMock( - status_code=200, iter_lines=lambda **kwargs: text_data.split("\n"), headers={"Content-Type": "text/plain"} - ), - ): - yield + mock_openmetrics_http.get.return_value = MockHTTPResponse(content=text_data, headers={"Content-Type": "text/plain"}) + yield @pytest.fixture() -def mock_operator_data(): +def mock_operator_data(mock_openmetrics_http): f_name = os.path.join(os.path.dirname(__file__), "fixtures", "operator_metrics.txt") with open(f_name, "r") as f: text_data = f.read() - with mock.patch( - 'requests.Session.get', - return_value=mock.MagicMock( - status_code=200, iter_lines=lambda **kwargs: text_data.split("\n"), headers={"Content-Type": "text/plain"} - ), - ): - yield + mock_openmetrics_http.get.return_value = MockHTTPResponse(content=text_data, headers={"Content-Type": "text/plain"}) + yield diff --git a/datadog_checks_dev/changelog.d/22676.added b/datadog_checks_dev/changelog.d/22676.added index a7037be61a947..45a1c1308315f 100644 --- a/datadog_checks_dev/changelog.d/22676.added +++ b/datadog_checks_dev/changelog.d/22676.added @@ -1 +1 @@ -Add mock_http fixture for library-agnostic HTTP client mocking in integration tests. \ No newline at end of file +Add mock_http, mock_openmetrics_http, and mock_prometheus_http fixtures for library-agnostic HTTP client mocking in integration tests. \ No newline at end of file diff --git a/datadog_checks_dev/datadog_checks/dev/plugin/pytest.py b/datadog_checks_dev/datadog_checks/dev/plugin/pytest.py index 7588d0cdfb38c..9e474ba6a2ef0 100644 --- a/datadog_checks_dev/datadog_checks/dev/plugin/pytest.py +++ b/datadog_checks_dev/datadog_checks/dev/plugin/pytest.py @@ -341,6 +341,32 @@ def _set_header(name, value): return client +@pytest.fixture +def mock_openmetrics_http(mock_http, mocker): + """OpenMetrics HTTP mock with dual interception: + + - v1 checks (OpenMetricsBaseCheck): patches OpenMetricsScraperMixin.get_http_handler to return mock_http. + - v2 checks (OpenMetricsBaseCheckV2): inherited via mock_http's AgentCheck.http PropertyMock; the + get_http_handler patch is unused on this path because v2 calls self.http.get(...) directly. + """ + mocker.patch( + 'datadog_checks.base.checks.openmetrics.mixins.OpenMetricsScraperMixin.get_http_handler', + return_value=mock_http, + ) + return mock_http + + +@pytest.fixture +def mock_prometheus_http(mock_http, mocker): + """mock_http with PrometheusScraperMixin.get_http_handler patched to return it.""" + mock_http.ignore_tls_warning = False + mocker.patch( + 'datadog_checks.base.checks.prometheus.mixins.PrometheusScraperMixin.get_http_handler', + return_value=mock_http, + ) + return mock_http + + @pytest.fixture def mock_http_response_per_endpoint(mocker, mock_response): @overload diff --git a/datadog_cluster_agent/tests/conftest.py b/datadog_cluster_agent/tests/conftest.py index 4ccf2c30a4710..b6d28217c9d9a 100644 --- a/datadog_cluster_agent/tests/conftest.py +++ b/datadog_cluster_agent/tests/conftest.py @@ -4,9 +4,10 @@ import os from copy import deepcopy -import mock import pytest +from datadog_checks.base.utils.http_testing import MockHTTPResponse + INSTANCE = {'prometheus_url': 'http://localhost:5000/metrics'} @@ -21,14 +22,9 @@ def instance(): @pytest.fixture() -def mock_metrics_endpoint(): +def mock_metrics_endpoint(mock_openmetrics_http): f_name = os.path.join(os.path.dirname(__file__), 'fixtures', 'metrics.txt') with open(f_name, 'r') as f: text_data = f.read() - with mock.patch( - 'requests.Session.get', - return_value=mock.MagicMock( - status_code=200, iter_lines=lambda **kwargs: text_data.split("\n"), headers={'Content-Type': "text/plain"} - ), - ): - yield + mock_openmetrics_http.get.return_value = MockHTTPResponse(content=text_data, headers={'Content-Type': 'text/plain'}) + yield diff --git a/dcgm/tests/conftest.py b/dcgm/tests/conftest.py index ab5c6d305432e..628d5c03875b9 100644 --- a/dcgm/tests/conftest.py +++ b/dcgm/tests/conftest.py @@ -4,10 +4,10 @@ import copy import os -from unittest import mock import pytest +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.dcgm import DcgmCheck from datadog_checks.dev import docker_run from datadog_checks.dev.conditions import CheckDockerLogs, CheckEndpoints @@ -40,28 +40,18 @@ def check(instance): @pytest.fixture() -def mock_metrics(): +def mock_metrics(mock_http): f_name = os.path.join(os.path.dirname(__file__), 'fixtures', 'metrics.txt') with open(f_name, 'r') as f: text_data = f.read() - with mock.patch( - 'requests.Session.get', - return_value=mock.MagicMock( - status_code=200, iter_lines=lambda **kwargs: text_data.split("\n"), headers={'Content-Type': "text/plain"} - ), - ): - yield + mock_http.get.return_value = MockHTTPResponse(content=text_data, headers={'Content-Type': 'text/plain'}) + yield @pytest.fixture() -def mock_label_remap(): +def mock_label_remap(mock_http): f_name = os.path.join(os.path.dirname(__file__), 'fixtures', 'label_remap.txt') with open(f_name, 'r') as f: text_data = f.read() - with mock.patch( - 'requests.Session.get', - return_value=mock.MagicMock( - status_code=200, iter_lines=lambda **kwargs: text_data.split("\n"), headers={'Content-Type': "text/plain"} - ), - ): - yield + mock_http.get.return_value = MockHTTPResponse(content=text_data, headers={'Content-Type': 'text/plain'}) + yield diff --git a/external_dns/tests/conftest.py b/external_dns/tests/conftest.py index 659cb475e7f29..f7fa24463b583 100644 --- a/external_dns/tests/conftest.py +++ b/external_dns/tests/conftest.py @@ -4,27 +4,23 @@ import os from copy import deepcopy -import mock import pytest +from datadog_checks.base.utils.http_testing import MockHTTPResponse + from .common import FIXTURE_DIR INSTANCE = {'prometheus_url': 'http://localhost:7979/metrics', 'tags': ['custom:tag']} @pytest.fixture -def mock_external_dns(): +def mock_external_dns(mock_openmetrics_http): f_name = os.path.join(FIXTURE_DIR, 'metrics.txt') with open(f_name, 'r') as f: text_data = f.read() - with mock.patch( - 'requests.Session.get', - return_value=mock.MagicMock( - status_code=200, iter_lines=lambda **kwargs: text_data.split('\n'), headers={'Content-Type': 'text/plain'} - ), - ): - yield + mock_openmetrics_http.get.return_value = MockHTTPResponse(content=text_data, headers={'Content-Type': 'text/plain'}) + yield @pytest.fixture(scope='session') diff --git a/fluxcd/tests/conftest.py b/fluxcd/tests/conftest.py index 55a944c2d90bd..f4f4a6875f515 100644 --- a/fluxcd/tests/conftest.py +++ b/fluxcd/tests/conftest.py @@ -3,10 +3,10 @@ # Licensed under a 3-clause BSD style license (see LICENSE) import os from contextlib import ExitStack -from unittest import mock import pytest +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.dev import get_here from datadog_checks.dev.kind import kind_run from datadog_checks.dev.kube_port_forward import port_forward @@ -69,36 +69,22 @@ def check(instance): @pytest.fixture() -def mock_metrics_v1(): +def mock_metrics_v1(mock_http): fixture_file = os.path.join(os.path.dirname(__file__), "fixtures", "metrics-v1.txt") with open(fixture_file, "r") as f: content = f.read() - with mock.patch( - "requests.Session.get", - return_value=mock.MagicMock( - status_code=200, - iter_lines=lambda **kwargs: content.split("\n"), - headers={"Content-Type": "text/plain"}, - ), - ): - yield + mock_http.get.return_value = MockHTTPResponse(content=content, headers={"Content-Type": "text/plain"}) + yield @pytest.fixture() -def mock_metrics_v2(): +def mock_metrics_v2(mock_http): fixture_file = os.path.join(os.path.dirname(__file__), "fixtures", "metrics-v2.txt") with open(fixture_file, "r") as f: content = f.read() - with mock.patch( - "requests.Session.get", - return_value=mock.MagicMock( - status_code=200, - iter_lines=lambda **kwargs: content.split("\n"), - headers={"Content-Type": "text/plain"}, - ), - ): - yield + mock_http.get.return_value = MockHTTPResponse(content=content, headers={"Content-Type": "text/plain"}) + yield diff --git a/gitlab/tests/conftest.py b/gitlab/tests/conftest.py index b7d24e38eadae..a7148e0d9c07d 100644 --- a/gitlab/tests/conftest.py +++ b/gitlab/tests/conftest.py @@ -7,10 +7,10 @@ from contextlib import contextmanager from time import sleep -import mock import pytest import requests +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.dev import EnvVars, TempDir, docker_run from datadog_checks.dev._env import get_state, save_state from datadog_checks.dev.conditions import CheckEndpoints @@ -102,12 +102,9 @@ def dd_environment(): @pytest.fixture() -def mock_data(): - with mock.patch( - 'requests.Session.get', - side_effect=mocked_requests_get, - ): - yield +def mock_data(mock_openmetrics_http): + mock_openmetrics_http.get.side_effect = mocked_requests_get + yield def mocked_requests_get(*args, **kwargs): @@ -117,45 +114,29 @@ def mocked_requests_get(*args, **kwargs): f_name = os.path.join(os.path.dirname(__file__), 'fixtures', 'readiness_check.json') with open(f_name, 'r') as f: text_data = f.read() - response = mock.MagicMock() - response.status_code = 200 - response.json.return_value = json.loads(text_data) - return response + return MockHTTPResponse(json_data=json.loads(text_data)) elif url == "http://{}:{}/-/liveness".format(HOST, GITLAB_LOCAL_PORT) or url == "http://{}:{}/-/health".format( HOST, GITLAB_LOCAL_PORT ): - response = mock.MagicMock() - response.status_code = 200 - return response + return MockHTTPResponse() elif url == "http://{}:{}/-/metrics".format(HOST, GITLAB_LOCAL_PORT): f_name = os.path.join(os.path.dirname(__file__), 'fixtures', 'metrics.txt') with open(f_name, 'r') as f: text_data = f.read() - return mock.MagicMock( - status_code=200, - iter_lines=lambda **kwargs: text_data.split("\n"), - headers={'Content-Type': "text/plain"}, - ) + return MockHTTPResponse(content=text_data, headers={'Content-Type': 'text/plain'}) elif url == "http://{}:{}/metrics".format(HOST, GITLAB_LOCAL_GITALY_PROMETHEUS_PORT): f_name = os.path.join(os.path.dirname(__file__), 'fixtures', 'gitaly.txt') with open(f_name, 'r') as f: text_data = f.read() - return mock.MagicMock( - status_code=200, - iter_lines=lambda **kwargs: text_data.split("\n"), - headers={'Content-Type': "text/plain"}, - ) + return MockHTTPResponse(content=text_data, headers={'Content-Type': 'text/plain'}) elif url == "http://{}:{}/api/v4/version".format(HOST, GITLAB_LOCAL_PORT): f_name = os.path.join(os.path.dirname(__file__), 'fixtures', 'version.json') with open(f_name, 'r') as f: text_data = f.read() - response = mock.MagicMock() - response.status_code = 200 - response.json.return_value = json.loads(text_data) - return response + return MockHTTPResponse(json_data=json.loads(text_data)) pytest.fail("url `{}` not registered".format(args[0])) diff --git a/gitlab_runner/tests/conftest.py b/gitlab_runner/tests/conftest.py index ec93ffbd16807..8391bf121ee9f 100644 --- a/gitlab_runner/tests/conftest.py +++ b/gitlab_runner/tests/conftest.py @@ -4,9 +4,9 @@ import os -import mock import pytest +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.dev import docker_run from datadog_checks.dev.conditions import CheckDockerLogs, CheckEndpoints @@ -68,21 +68,14 @@ def _mocked_requests_get(*args, **kwargs): fixtures_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'metrics.txt') with open(fixtures_path, 'r') as f: text_data = f.read() - return mock.MagicMock( - status_code=200, - iter_lines=lambda **kwargs: text_data.split("\n"), - headers={'Content-Type': "text/plain"}, - ) + return MockHTTPResponse(content=text_data, headers={'Content-Type': 'text/plain'}) elif url == 'http://{}:{}/ci'.format(HOST, GITLAB_LOCAL_MASTER_PORT): - return mock.MagicMock(status_code=200) + return MockHTTPResponse() - return mock.MagicMock(status_code=404) + return MockHTTPResponse(status_code=404) @pytest.fixture() -def mock_data(): - with mock.patch( - 'requests.Session.get', - side_effect=_mocked_requests_get, - ): - yield +def mock_data(mock_openmetrics_http): + mock_openmetrics_http.get.side_effect = _mocked_requests_get + yield diff --git a/haproxy/tests/conftest.py b/haproxy/tests/conftest.py index b32c6c12f585d..c114af9ec6c39 100644 --- a/haproxy/tests/conftest.py +++ b/haproxy/tests/conftest.py @@ -14,6 +14,7 @@ import requests from packaging import version +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.dev import TempDir, WaitFor, docker_run from datadog_checks.haproxy import HAProxyCheck from datadog_checks.haproxy.metrics import METRIC_MAP @@ -204,14 +205,13 @@ def instancev2(): return instance -@pytest.fixture(scope="module") -def haproxy_mock(): +@pytest.fixture +def haproxy_mock(mock_http): filepath = os.path.join(HERE, 'fixtures', 'mock_data') with open(filepath, 'rb') as f: data = f.read() - p = mock.patch('requests.Session.get', return_value=mock.Mock(content=data)) - yield p.start() - p.stop() + mock_http.get.return_value = MockHTTPResponse(content=data) + yield @pytest.fixture(scope="module") @@ -222,23 +222,22 @@ def mock_data(): return data.split('\n') -@pytest.fixture(scope="module") -def haproxy_mock_evil(): +@pytest.fixture +def haproxy_mock_evil(mock_http): filepath = os.path.join(HERE, 'fixtures', 'mock_data_evil') with open(filepath, 'rb') as f: data = f.read() - p = mock.patch('requests.Session.get', return_value=mock.Mock(content=data)) - yield p.start() - p.stop() + mock_http.get.return_value = MockHTTPResponse(content=data) + yield -@pytest.fixture(scope="module") -def haproxy_mock_enterprise_version_info(): +@pytest.fixture +def haproxy_mock_enterprise_version_info(mock_http): filepath = os.path.join(HERE, 'fixtures', 'enterprise_version_info.html') with open(filepath, 'rb') as f: data = f.read() - with mock.patch('requests.Session.get', return_value=mock.Mock(content=data)) as p: - yield p + mock_http.get.return_value = MockHTTPResponse(content=data) + yield @pytest.fixture(scope="session") diff --git a/impala/tests/conftest.py b/impala/tests/conftest.py index fed60c3fed3bf..86f818e299bfa 100644 --- a/impala/tests/conftest.py +++ b/impala/tests/conftest.py @@ -3,10 +3,10 @@ # Licensed under a 3-clause BSD style license (see LICENSE) import os from contextlib import ExitStack, contextmanager -from unittest import mock import pytest +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.dev import EnvVars, TempDir, docker_run, get_docker_hostname, get_here from datadog_checks.dev._env import get_state, save_state from datadog_checks.dev.conditions import CheckEndpoints @@ -92,22 +92,15 @@ def catalog_check(catalog_instance): @pytest.fixture() -def mock_metrics(request): +def mock_metrics(request, mock_http): metrics_file = request.node.get_closest_marker("metrics_file") with open( os.path.join(os.path.dirname(__file__), "fixtures", metrics_file.args[0], metrics_file.args[1]), "r" ) as fixture_file: content = fixture_file.read() - with mock.patch( - "requests.Session.get", - return_value=mock.MagicMock( - status_code=200, - iter_lines=lambda **kwargs: content.split("\n"), - headers={"Content-Type": "text/plain"}, - ), - ): - yield + mock_http.get.return_value = MockHTTPResponse(content=content, headers={"Content-Type": "text/plain"}) + yield @contextmanager diff --git a/nutanix/tests/conftest.py b/nutanix/tests/conftest.py index 9dbee745235ec..b1911496c2945 100644 --- a/nutanix/tests/conftest.py +++ b/nutanix/tests/conftest.py @@ -8,6 +8,7 @@ import pytest +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.dev import docker_run, get_docker_hostname, get_here from datadog_checks.dev.conditions import CheckEndpoints @@ -121,12 +122,8 @@ def mock_instance(): @pytest.fixture -def mock_http_get(mocker): +def mock_http_get(mock_http): def mock_response(url, params=None, *args, **kwargs): - mock_resp = mocker.Mock() - mock_resp.status_code = 200 - mock_resp.raise_for_status = mocker.Mock() - page = None if params: @@ -144,62 +141,43 @@ def mock_response(url, params=None, *args, **kwargs): page = 0 if '/console' in url: - return mock_resp + return MockHTTPResponse() if ( "/api/clustermgmt/v4.0/stats/clusters/00064715-c043-5d8f-ee4b-176ec875554d/hosts/d8787814-4fe8-4ba5-931f-e1ee31c294a6" in url ): - response_data = load_fixture("host_stats_00064715_d8787814.json") - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=load_fixture("host_stats_00064715_d8787814.json")) if ( "/api/clustermgmt/v4.0/stats/clusters/aabbccdd-1111-2222-3333-444455556666/hosts/eeee1111-2222-3333-4444-555566667777" in url ): - response_data = load_fixture("host_stats_aabbccdd_eeee1111.json") - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=load_fixture("host_stats_aabbccdd_eeee1111.json")) if "/api/clustermgmt/v4.0/stats/clusters/00064715-c043-5d8f-ee4b-176ec875554d" in url: - response_data = load_fixture("cluster_stats_00064715.json") - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=load_fixture("cluster_stats_00064715.json")) if "/api/clustermgmt/v4.0/stats/clusters/aabbccdd-1111-2222-3333-444455556666" in url: - response_data = load_fixture("cluster_stats_aabbccdd.json") - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=load_fixture("cluster_stats_aabbccdd.json")) if '/api/clustermgmt/v4.0/config/clusters/d07db284-6df6-4ca2-88cd-9dd5ed71ac08/hosts' in url: - mock_resp.status_code = 400 - return mock_resp + return MockHTTPResponse(status_code=400) if '/api/clustermgmt/v4.0/config/clusters/00064715-c043-5d8f-ee4b-176ec875554d/hosts' in url: - response_data = load_fixture_page("hosts_00064715.json", page) - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=load_fixture_page("hosts_00064715.json", page)) if '/api/clustermgmt/v4.0/config/clusters/aabbccdd-1111-2222-3333-444455556666/hosts' in url: - response_data = load_fixture_page("hosts_aabbccdd.json", page) - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=load_fixture_page("hosts_aabbccdd.json", page)) if '/api/clustermgmt/v4.0/config/clusters' in url: - response_data = load_fixture_page("clusters.json", page) - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=load_fixture_page("clusters.json", page)) if '/api/prism/v4.0/config/categories' in url: - response_data = load_fixture_page("categories.json", page) - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=load_fixture_page("categories.json", page)) if 'api/vmm/v4.0/ahv/stats/vms' in url: - response_data = load_fixture_page("vms_stats.json", page) - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=load_fixture_page("vms_stats.json", page)) if 'api/vmm/v4.0/ahv/config/vms' in url: response_data = load_fixture_page("vms.json", page) @@ -218,8 +196,7 @@ def mock_response(url, params=None, *args, **kwargs): response_data = dict(response_data) response_data['data'] = filtered - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=response_data) # Events endpoint - paginated if 'api/monitoring/v4.0/serviceability/events' in url: @@ -247,8 +224,7 @@ def mock_response(url, params=None, *args, **kwargs): response_data = dict(response_data) response_data['data'] = filtered_data - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=response_data) if 'api/monitoring/v4.0/serviceability/audits' in url: response_data = load_fixture_page("audits.json", page) @@ -275,8 +251,7 @@ def mock_response(url, params=None, *args, **kwargs): response_data = dict(response_data) response_data['data'] = filtered_data - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=response_data) # Individual alert fetch by ID (e.g. /alerts/{uuid}) import re @@ -287,11 +262,8 @@ def mock_response(url, params=None, *args, **kwargs): all_alerts = load_fixture_page("alerts.json", 0).get('data', []) alert_data = next((a for a in all_alerts if a.get('extId') == alert_ext_id), None) if alert_data: - mock_resp.json = mocker.Mock(return_value={"data": alert_data}) - else: - mock_resp.status_code = 404 - mock_resp.raise_for_status = mocker.Mock(side_effect=Exception("404 Not Found")) - return mock_resp + return MockHTTPResponse(json_data={"data": alert_data}) + return MockHTTPResponse(status_code=404) if 'api/monitoring/v4.0/serviceability/alerts' in url or 'api/monitoring/v4.2/serviceability/alerts' in url: response_data = load_fixture_page("alerts.json", page) @@ -318,8 +290,8 @@ def mock_response(url, params=None, *args, **kwargs): response_data = dict(response_data) response_data['data'] = filtered_data - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=response_data) + if 'api/prism/v4.0/config/tasks' in url: response_data = load_fixture_page("tasks.json", page) @@ -345,12 +317,10 @@ def mock_response(url, params=None, *args, **kwargs): response_data = dict(response_data) response_data['data'] = filtered_data - mock_resp.json = mocker.Mock(return_value=response_data) - return mock_resp + return MockHTTPResponse(json_data=response_data) print(f"[MOCK ERROR] No matching endpoint for URL: {url}") - mock_resp.status_code = 404 - mock_resp.raise_for_status = mocker.Mock(side_effect=Exception("404 Not Found")) - return mock_resp + return MockHTTPResponse(status_code=404) - return mocker.patch('requests.Session.get', side_effect=mock_response) + mock_http.get.side_effect = mock_response + return mock_http.get diff --git a/nutanix/tests/test_retry.py b/nutanix/tests/test_retry.py index c9aed0a766c1e..369000ed40951 100644 --- a/nutanix/tests/test_retry.py +++ b/nutanix/tests/test_retry.py @@ -3,11 +3,10 @@ # Licensed under a 3-clause BSD style license (see LICENSE) -from unittest.mock import Mock - import pytest -from requests import HTTPError, Response +from datadog_checks.base.utils.http_exceptions import HTTPStatusError +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.nutanix import NutanixCheck pytestmark = [pytest.mark.unit] @@ -18,18 +17,14 @@ def test_retry_on_rate_limit_success_no_retry(dd_run_check, aggregator, mock_ins check = NutanixCheck('nutanix', {}, [mock_instance]) dd_run_check(check) - mock_response = Mock(spec=Response) - mock_response.status_code = 200 - mock_response.json.return_value = {"data": {"test": "data"}} - mock_response.raise_for_status = Mock() - mock_response.content = b'{"data": {"test": "data"}}' - - mock_get = mocker.patch('requests.Session.get', return_value=mock_response) + mock_http_get.reset_mock() + mock_http_get.side_effect = None + mock_http_get.return_value = MockHTTPResponse(json_data={"data": {"test": "data"}}) result = check._get_request_data("api/test") assert result == {"test": "data"} - assert mock_get.call_count == 1 + assert mock_http_get.call_count == 1 aggregator.assert_metric("nutanix.api.rate_limited", count=0) @@ -38,26 +33,17 @@ def test_retry_on_rate_limit_429_then_success(dd_run_check, aggregator, mock_ins check = NutanixCheck('nutanix', {}, [mock_instance]) dd_run_check(check) - # First response: 429 rate limited - mock_response_429 = Mock(spec=Response) - mock_response_429.status_code = 429 - mock_response_429.raise_for_status.side_effect = HTTPError(response=mock_response_429) - mock_response_429.content = b'' - - # Second response: success - mock_response_200 = Mock(spec=Response) - mock_response_200.status_code = 200 - mock_response_200.json.return_value = {"data": {"test": "data"}} - mock_response_200.raise_for_status = Mock() - mock_response_200.content = b'{"data": {"test": "data"}}' - - mock_get = mocker.patch('requests.Session.get', side_effect=[mock_response_429, mock_response_200]) + mock_http_get.reset_mock() + mock_http_get.side_effect = [ + MockHTTPResponse(status_code=429), + MockHTTPResponse(json_data={"data": {"test": "data"}}), + ] mock_sleep = mocker.patch('time.sleep') result = check._get_request_data("api/test") assert result == {"test": "data"} - assert mock_get.call_count == 2 + assert mock_http_get.call_count == 2 assert mock_sleep.call_count == 1 # First retry: base * 2^1 + jitter = 2 to 3 @@ -74,19 +60,16 @@ def test_retry_on_rate_limit_max_retries_exceeded(dd_run_check, aggregator, mock check = NutanixCheck('nutanix', {}, [mock_instance]) dd_run_check(check) - mock_response_429 = Mock(spec=Response) - mock_response_429.status_code = 429 - mock_response_429.raise_for_status.side_effect = HTTPError(response=mock_response_429) - mock_response_429.content = b'' - - mock_get = mocker.patch('requests.Session.get', return_value=mock_response_429) + mock_http_get.reset_mock() + mock_http_get.side_effect = None + mock_http_get.return_value = MockHTTPResponse(status_code=429) mock_sleep = mocker.patch('time.sleep') - with pytest.raises(HTTPError): + with pytest.raises(HTTPStatusError): check._get_request_data("api/test") # Initial request + 1 retry (range(1, 2)) = 2 total - assert mock_get.call_count == 2 + assert mock_http_get.call_count == 2 assert mock_sleep.call_count == 1 # Sleep between retries (not after final failure) aggregator.assert_metric("nutanix.api.rate_limited", tags=['nutanix', 'prism_central:10.0.0.197']) @@ -98,19 +81,16 @@ def test_retry_on_non_429_error_no_retry(dd_run_check, aggregator, mock_instance dd_run_check(check) # 500 Internal Server Error - mock_response_500 = Mock(spec=Response) - mock_response_500.status_code = 500 - mock_response_500.raise_for_status.side_effect = HTTPError(response=mock_response_500) - mock_response_500.content = b'' - - mock_get = mocker.patch('requests.Session.get', return_value=mock_response_500) + mock_http_get.reset_mock() + mock_http_get.side_effect = None + mock_http_get.return_value = MockHTTPResponse(status_code=500) mock_sleep = mocker.patch('time.sleep') - with pytest.raises(HTTPError): + with pytest.raises(HTTPStatusError): check._get_request_data("api/test") # Should only try once, no retries for non-429 errors - assert mock_get.call_count == 1 + assert mock_http_get.call_count == 1 assert mock_sleep.call_count == 0 aggregator.assert_metric("nutanix.api.rate_limited", count=0) @@ -126,24 +106,17 @@ def test_retry_with_custom_config(dd_run_check, aggregator, mock_instance, mock_ dd_run_check(check) # First two responses: 429, then success - mock_response_429 = Mock(spec=Response) - mock_response_429.status_code = 429 - mock_response_429.raise_for_status.side_effect = HTTPError(response=mock_response_429) - mock_response_429.content = b'' - - mock_response_200 = Mock(spec=Response) - mock_response_200.status_code = 200 - mock_response_200.json.return_value = {"data": {"test": "data"}} - mock_response_200.raise_for_status = Mock() - mock_response_200.content = b'{"data": {"test": "data"}}' - - mock_get = mocker.patch('requests.Session.get', side_effect=[mock_response_429, mock_response_200]) + mock_http_get.reset_mock() + mock_http_get.side_effect = [ + MockHTTPResponse(status_code=429), + MockHTTPResponse(json_data={"data": {"test": "data"}}), + ] mock_sleep = mocker.patch('time.sleep') result = check._get_request_data("api/test") assert result == {"test": "data"} - assert mock_get.call_count == 2 + assert mock_http_get.call_count == 2 # First retry: base * 2^1 + jitter = 4 to 5 (base=2) sleep_time = mock_sleep.call_args[0][0] @@ -160,26 +133,19 @@ def test_retry_exponential_backoff(dd_run_check, aggregator, mock_instance, mock dd_run_check(check) # Four 429 responses, then success - mock_response_429 = Mock(spec=Response) - mock_response_429.status_code = 429 - mock_response_429.raise_for_status.side_effect = HTTPError(response=mock_response_429) - mock_response_429.content = b'' - - mock_response_200 = Mock(spec=Response) - mock_response_200.status_code = 200 - mock_response_200.json.return_value = {"data": {"test": "data"}} - mock_response_200.raise_for_status = Mock() - mock_response_200.content = b'{"data": {"test": "data"}}' - - mock_get = mocker.patch( - 'requests.Session.get', side_effect=[mock_response_429, mock_response_429, mock_response_429, mock_response_200] - ) + mock_http_get.reset_mock() + mock_http_get.side_effect = [ + MockHTTPResponse(status_code=429), + MockHTTPResponse(status_code=429), + MockHTTPResponse(status_code=429), + MockHTTPResponse(json_data={"data": {"test": "data"}}), + ] mock_sleep = mocker.patch('time.sleep') result = check._get_request_data("api/test") assert result == {"test": "data"} - assert mock_get.call_count == 4 + assert mock_http_get.call_count == 4 assert mock_sleep.call_count == 3 # Check exponential backoff pattern: base * 2^attempt + jitter (attempt starts at 1) @@ -202,19 +168,16 @@ def test_retry_disabled_with_zero_max_retries(dd_run_check, aggregator, mock_ins check = NutanixCheck('nutanix', {}, [mock_instance]) dd_run_check(check) - mock_response_429 = Mock(spec=Response) - mock_response_429.status_code = 429 - mock_response_429.raise_for_status.side_effect = HTTPError(response=mock_response_429) - mock_response_429.content = b'' - - mock_get = mocker.patch('requests.Session.get', return_value=mock_response_429) + mock_http_get.reset_mock() + mock_http_get.side_effect = None + mock_http_get.return_value = MockHTTPResponse(status_code=429) mock_sleep = mocker.patch('time.sleep') - with pytest.raises(HTTPError): + with pytest.raises(HTTPStatusError): check._get_request_data("api/test") # Should only try once when max_retries is 0 - assert mock_get.call_count == 1 + assert mock_http_get.call_count == 1 assert mock_sleep.call_count == 0 # Loop never runs with max_retries=0 (clamped to 1), so no rate_limited metric is emitted @@ -226,25 +189,18 @@ def test_health_check_with_retry(dd_run_check, aggregator, mock_instance, mock_h check = NutanixCheck('nutanix', {}, [mock_instance]) dd_run_check(check) - # First response: 429 rate limited - mock_response_429 = Mock(spec=Response) - mock_response_429.status_code = 429 - mock_response_429.raise_for_status.side_effect = HTTPError(response=mock_response_429) - mock_response_429.content = b'' - - # Second response: success - mock_response_200 = Mock(spec=Response) - mock_response_200.status_code = 200 - mock_response_200.raise_for_status = Mock() - mock_response_200.content = b'' - - mock_get = mocker.patch('requests.Session.get', side_effect=[mock_response_429, mock_response_200]) + # First response: 429 rate limited, second: success + mock_http_get.reset_mock() + mock_http_get.side_effect = [ + MockHTTPResponse(status_code=429), + MockHTTPResponse(), + ] mock_sleep = mocker.patch('time.sleep') result = check._check_health() assert result is True - assert mock_get.call_count == 2 + assert mock_http_get.call_count == 2 assert mock_sleep.call_count == 1 # Health check should report up after successful retry diff --git a/prometheus/tests/conftest.py b/prometheus/tests/conftest.py index 853e8f5e99efd..de8ddb13c8174 100644 --- a/prometheus/tests/conftest.py +++ b/prometheus/tests/conftest.py @@ -4,11 +4,11 @@ import os -import mock import pytest from prometheus_client import CollectorRegistry, Counter, Gauge, generate_latest from datadog_checks.base import ensure_unicode +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.dev import docker_run, get_docker_hostname HERE = os.path.dirname(os.path.abspath(__file__)) @@ -50,7 +50,7 @@ def dd_environment(e2e_instance): @pytest.fixture -def poll_mock(): +def poll_mock(mock_prometheus_http): registry = CollectorRegistry() # pylint: disable=E1123,E1101 g1 = Gauge('metric1', 'processor usage', ['matched_label', 'node', 'flavor'], registry=registry) @@ -62,13 +62,6 @@ def poll_mock(): g3 = Gauge('metric3', 'memory usage', ['matched_label', 'node', 'timestamp'], registry=registry) g3.labels(matched_label="foobar", node="host2", timestamp="456").set(float('inf')) - poll_mock_patch = mock.patch( - 'requests.Session.get', - return_value=mock.MagicMock( - status_code=200, - iter_lines=lambda **kwargs: ensure_unicode(generate_latest(registry)).split("\n"), - headers={'Content-Type': "text/plain"}, - ), - ) - with poll_mock_patch: - yield + content = ensure_unicode(generate_latest(registry)) + mock_prometheus_http.get.return_value = MockHTTPResponse(content=content, headers={'Content-Type': 'text/plain'}) + yield diff --git a/scylla/tests/conftest.py b/scylla/tests/conftest.py index 5b28e13609d02..52338e368df3f 100644 --- a/scylla/tests/conftest.py +++ b/scylla/tests/conftest.py @@ -3,9 +3,9 @@ # Licensed under a 3-clause BSD style license (see LICENSE) import os -import mock import pytest +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.dev import docker_run, get_docker_hostname, get_here HERE = get_here() @@ -34,7 +34,7 @@ def instance(): @pytest.fixture() -def mock_db_data(): +def mock_db_data(mock_openmetrics_http): if os.environ['SCYLLA_VERSION'].startswith('5.'): f_name = os.path.join(os.path.dirname(__file__), 'fixtures', 'scylla_5_metrics.txt') elif os.environ['SCYLLA_VERSION'].startswith('3.3'): @@ -44,12 +44,5 @@ def mock_db_data(): with open(f_name, 'r') as f: text_data = f.read() - with mock.patch( - 'requests.Session.get', - return_value=mock.MagicMock( - status_code=200, - iter_lines=lambda **kwargs: text_data.split("\n"), - headers={'Content-Type': "text/plain"}, - ), - ): - yield + mock_openmetrics_http.get.return_value = MockHTTPResponse(content=text_data, headers={'Content-Type': 'text/plain'}) + yield diff --git a/temporal/tests/conftest.py b/temporal/tests/conftest.py index 2ed04281aa8c5..68117b017db27 100644 --- a/temporal/tests/conftest.py +++ b/temporal/tests/conftest.py @@ -5,10 +5,10 @@ import os import time from contextlib import contextmanager -from unittest import mock import pytest +from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.dev import EnvVars, TempDir, docker_run, get_docker_hostname, get_here, run_command from datadog_checks.dev._env import get_state, save_state from datadog_checks.dev.conditions import CheckEndpoints @@ -82,14 +82,9 @@ def check(instance): @pytest.fixture() -def mock_metrics(): +def mock_metrics(mock_http): f_name = os.path.join(os.path.dirname(__file__), 'fixtures', 'metrics.txt') with open(f_name, 'r') as f: text_data = f.read() - with mock.patch( - 'requests.Session.get', - return_value=mock.MagicMock( - status_code=200, iter_lines=lambda **kwargs: text_data.split("\n"), headers={'Content-Type': "text/plain"} - ), - ): - yield + mock_http.get.return_value = MockHTTPResponse(content=text_data, headers={'Content-Type': 'text/plain'}) + yield diff --git a/yarn/tests/conftest.py b/yarn/tests/conftest.py index b435fcf773a73..e98c47abdae54 100644 --- a/yarn/tests/conftest.py +++ b/yarn/tests/conftest.py @@ -7,8 +7,6 @@ from urllib.parse import urljoin import pytest -from mock import patch -from requests.exceptions import SSLError from datadog_checks.base.utils.http_testing import MockHTTPResponse from datadog_checks.dev import docker_run @@ -65,19 +63,6 @@ def mocked_auth_request(mock_http): yield -@pytest.fixture -def mocked_bad_cert_request(): - """Keep requests.Session.get patch — tests verify=True vs verify=False which requires the real HTTP wrapper.""" - - def requests_bad_cert_get(session, *args, **kwargs): - if kwargs.get('verify', True): - raise SSLError("certificate verification failed for {}".format(args[0])) - return requests_get_mock(args[0], *args[1:], **kwargs) - - with patch("requests.Session.get", new=requests_bad_cert_get): - yield - - def requests_get_mock(url, *args, **kwargs): if url == YARN_CLUSTER_METRICS_URL: return MockHTTPResponse(file_path=os.path.join(FIXTURE_DIR, 'cluster_metrics')) diff --git a/yarn/tests/test_yarn.py b/yarn/tests/test_yarn.py index 0ff1a36ccc18c..c98c76e3bc055 100644 --- a/yarn/tests/test_yarn.py +++ b/yarn/tests/test_yarn.py @@ -5,7 +5,6 @@ import re import pytest -from requests.exceptions import SSLError from datadog_checks.yarn import YarnCheck from datadog_checks.yarn.yarn import ( @@ -267,7 +266,12 @@ def test_auth(aggregator, mocked_auth_request): ) -def test_ssl_verification(aggregator, mocked_bad_cert_request): +def test_ssl_verification(aggregator, mock_http): + from requests.exceptions import SSLError + + from .conftest import requests_get_mock + + mock_http.get.side_effect = SSLError("certificate verification failed") instance = YARN_SSL_VERIFY_TRUE_CONFIG['instances'][0] # Instantiate YarnCheck @@ -288,6 +292,7 @@ def test_ssl_verification(aggregator, mocked_bad_cert_request): raise AssertionError('Should have thrown an SSLError due to a badly configured certificate') # Run the check on the same configuration, but with verify=False. We shouldn't get an exception. + mock_http.get.side_effect = requests_get_mock instance = YARN_SSL_VERIFY_FALSE_CONFIG['instances'][0] yarn = YarnCheck('yarn', {}, [instance]) yarn.check(instance)