Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
05e8501
Migrate couch config assertion test to check.http.options
mwdd146980 Feb 25, 2026
2ec53f4
Migrate gitlab_runner config assertion test to check.http.options
mwdd146980 Feb 25, 2026
80340b8
Migrate couch config assertion test to http_client_session
mwdd146980 Feb 25, 2026
a6191a0
Migrate gitlab_runner timeout test to http_client_session
mwdd146980 Feb 25, 2026
f66df55
Reformat with ddev test --fmt
mwdd146980 Feb 25, 2026
a0500cd
Migrate marathon config assertion test to http_client_session
mwdd146980 Feb 25, 2026
8a79d6d
Migrate rabbitmq session-patch tests to http_client_session
mwdd146980 Feb 25, 2026
c5ca4d6
Migrate consul session-patch tests to http_client_session
mwdd146980 Feb 25, 2026
c03244e
Migrate torchserve session-patch test to http_client_session
mwdd146980 Feb 25, 2026
977cc84
Migrate etcd, ecs_fargate, mesos_master session-patch tests to http_c…
mwdd146980 Feb 25, 2026
2eb59c6
Reformat with ddev test --fmt
mwdd146980 Feb 25, 2026
337c330
Scrap http_client_session; migrate Step 2 tests to check.http.options…
mwdd146980 Feb 25, 2026
c01be5e
Migrate airflow session-patch tests to mock_http
mwdd146980 Feb 25, 2026
5acf749
Migrate druid session-patch tests to mock_http
mwdd146980 Feb 25, 2026
0ea892d
Migrate envoy config test to check.http.options
mwdd146980 Feb 25, 2026
9a3b4f8
Migrate mesos_slave session-patch tests to mock_http
mwdd146980 Feb 25, 2026
0c2c556
Migrate nginx session-patch tests to check.http.options and mock_http
mwdd146980 Feb 25, 2026
5c64d03
Migrate php_fpm session-patch tests to check.http.options and mock_http
mwdd146980 Feb 25, 2026
d3325c3
Add options to HTTPClientProtocol; initialize in mock_http fixture
mwdd146980 Feb 25, 2026
c4f8d3c
Address code review suggestions: fix imports, complete protocol, move…
mwdd146980 Feb 25, 2026
b8efb28
Reformat with ddev test --fmt
mwdd146980 Feb 25, 2026
ffda81b
Remove mock_http from http_testing.py; use datadog_checks_dev plugin …
mwdd146980 Feb 27, 2026
eb9fdbc
Fix mock_http fixture: initialize client.options as MagicMock
mwdd146980 Feb 27, 2026
87da77b
Add get_header/set_header to RequestsWrapper and migrate all callers
mwdd146980 Mar 3, 2026
51ec5a1
Step 2 addendum: migrate teamcity and squid config assertion tests
mwdd146980 Mar 4, 2026
0bfd31b
Fix license header year in etcd/tests/test_unit.py
mwdd146980 Mar 5, 2026
b62276f
Add docstring to get_header; use set_header in vault __init__
mwdd146980 Mar 5, 2026
e32477e
Fix mock_http fixture: use real dict for client.options
mwdd146980 Mar 5, 2026
e99947d
Fix Cursor review findings: vault _set_header and mock_http side_effects
mwdd146980 Mar 10, 2026
27e530d
Trim redundant tests in test_headers.py and test_http_testing.py
mwdd146980 Mar 10, 2026
ba7cdbe
Flatten test classes to top-level functions in test_headers.py and te…
mwdd146980 Mar 10, 2026
5a3eeb5
Make get_header/set_header case-insensitive
mwdd146980 Mar 16, 2026
e51c89a
Remove dead assertions in rabbitmq test__get_data (PR #22722 review)
mwdd146980 Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 50 additions & 59 deletions airflow/tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,16 @@ def test_service_checks_cannot_connect(aggregator):
'json_resp, expected_healthy_status, expected_healthy_value',
[({'status': 'OK'}, AgentCheck.OK, 1), ({'status': 'KO'}, AgentCheck.CRITICAL, 0), ({}, AgentCheck.CRITICAL, 0)],
)
def test_service_checks_healthy_exp(aggregator, json_resp, expected_healthy_status, expected_healthy_value):
def test_service_checks_healthy_exp(aggregator, mock_http, json_resp, expected_healthy_status, expected_healthy_value):
instance = common.FULL_CONFIG['instances'][0]
check = AirflowCheck('airflow', common.FULL_CONFIG, [instance])

with mock.patch('datadog_checks.airflow.airflow.AirflowCheck._get_version', return_value=None):
mock_session = mock.MagicMock()
with mock.patch('datadog_checks.base.utils.http.requests.Session', return_value=mock_session):
mock_resp = mock.MagicMock(status_code=200)
mock_resp.json.side_effect = [json_resp]
mock_session.get.return_value = mock_resp
mock_resp = mock.MagicMock(status_code=200)
mock_resp.json.side_effect = [json_resp]
mock_http.get.return_value = mock_resp

check.check(None)
with mock.patch('datadog_checks.airflow.airflow.AirflowCheck._get_version', return_value=None):
check.check(None)

tags = ['key:my-tag', 'url:http://localhost:8080']

Expand All @@ -54,65 +52,60 @@ def test_service_checks_healthy_exp(aggregator, json_resp, expected_healthy_stat
],
)
def test_service_checks_healthy_stable(
aggregator, metadb_status, scheduler_status, expected_healthy_status, expected_healthy_value
aggregator, mock_http, metadb_status, scheduler_status, expected_healthy_status, expected_healthy_value
): # Stable is only defined in the context of Airflow 2
instance = common.FULL_CONFIG['instances'][0]
check = AirflowCheck('airflow', common.FULL_CONFIG, [instance])

with mock.patch('datadog_checks.airflow.airflow.AirflowCheck._get_version', return_value='2.6.2'):
mock_session = mock.MagicMock()
with mock.patch('datadog_checks.base.utils.http.requests.Session', return_value=mock_session):
mock_resp = mock.MagicMock(status_code=200)
mock_resp.json.side_effect = [
{'metadatabase': {'status': metadb_status}, 'scheduler': {'status': scheduler_status}},
{'status': 'OK'},
]
mock_session.get.return_value = mock_resp
mock_resp = mock.MagicMock(status_code=200)
mock_resp.json.side_effect = [
{'metadatabase': {'status': metadb_status}, 'scheduler': {'status': scheduler_status}},
{'status': 'OK'},
]
mock_http.get.return_value = mock_resp

check.check(None)
with mock.patch('datadog_checks.airflow.airflow.AirflowCheck._get_version', return_value='2.6.2'):
check.check(None)

tags = ['key:my-tag', 'url:http://localhost:8080']

aggregator.assert_service_check('airflow.healthy', expected_healthy_status, tags=tags, count=1)
aggregator.assert_metric('airflow.healthy', expected_healthy_value, tags=tags, count=1)


def test_dag_total_tasks(aggregator, task_instance):
def test_dag_total_tasks(aggregator, mock_http, task_instance):
instance = common.FULL_CONFIG['instances'][0]
check = AirflowCheck('airflow', common.FULL_CONFIG, [instance])

with mock.patch('datadog_checks.airflow.airflow.AirflowCheck._get_version', return_value='2.6.2'):
req = mock.MagicMock()
with mock.patch('datadog_checks.base.utils.http.requests.Session', return_value=req):
mock_resp = mock.MagicMock(status_code=200)
mock_resp.json.side_effect = [
{'metadatabase': {'status': 'healthy'}, 'scheduler': {'status': 'healthy'}},
task_instance,
]
req.get.return_value = mock_resp
mock_resp = mock.MagicMock(status_code=200)
mock_resp.json.side_effect = [
{'metadatabase': {'status': 'healthy'}, 'scheduler': {'status': 'healthy'}},
task_instance,
]
mock_http.get.return_value = mock_resp

check.check(None)
with mock.patch('datadog_checks.airflow.airflow.AirflowCheck._get_version', return_value='2.6.2'):
check.check(None)

aggregator.assert_metric('airflow.dag.task.total_running', value=1, count=1)


def test_dag_task_ongoing_duration(aggregator, task_instance):
def test_dag_task_ongoing_duration(aggregator, mock_http, task_instance):
instance = common.FULL_CONFIG['instances'][0]
check = AirflowCheck('airflow', common.FULL_CONFIG, [instance])

mock_resp = mock.MagicMock(status_code=200)
mock_resp.json.side_effect = [
{'metadatabase': {'status': 'healthy'}, 'scheduler': {'status': 'healthy'}},
]
mock_http.get.return_value = mock_resp

with mock.patch('datadog_checks.airflow.airflow.AirflowCheck._get_version', return_value='2.6.2'):
req = mock.MagicMock()
with mock.patch('datadog_checks.base.utils.http.requests.Session', return_value=req):
mock_resp = mock.MagicMock(status_code=200)
mock_resp.json.side_effect = [
{'metadatabase': {'status': 'healthy'}, 'scheduler': {'status': 'healthy'}},
]
req.get.return_value = mock_resp
with mock.patch(
'datadog_checks.airflow.airflow.AirflowCheck._get_all_task_instances',
return_value=task_instance.get('task_instances'),
):
check.check(None)
with mock.patch(
'datadog_checks.airflow.airflow.AirflowCheck._get_all_task_instances',
return_value=task_instance.get('task_instances'),
):
check.check(None)

aggregator.assert_metric(
'airflow.dag.task.ongoing_duration',
Expand Down Expand Up @@ -141,23 +134,21 @@ def test_dag_task_ongoing_duration(aggregator, task_instance):
),
],
)
def test_config_collect_ongoing_duration(collect_ongoing_duration, should_call_method):
def test_config_collect_ongoing_duration(mock_http, collect_ongoing_duration, should_call_method):
instance = {**common.FULL_CONFIG['instances'][0], 'collect_ongoing_duration': collect_ongoing_duration}
check = AirflowCheck('airflow', common.FULL_CONFIG, [instance])

mock_resp = mock.MagicMock(status_code=200)
mock_resp.json.side_effect = [
{'metadatabase': {'status': 'healthy'}, 'scheduler': {'status': 'healthy'}},
]
mock_http.get.return_value = mock_resp

with mock.patch('datadog_checks.airflow.airflow.AirflowCheck._get_version', return_value='2.6.2'):
req = mock.MagicMock()
with mock.patch('datadog_checks.base.utils.http.requests.Session', return_value=req):
mock_resp = mock.MagicMock(status_code=200)
mock_resp.json.side_effect = [
{'metadatabase': {'status': 'healthy'}, 'scheduler': {'status': 'healthy'}},
]
req.get.return_value = mock_resp

with mock.patch(
'datadog_checks.airflow.airflow.AirflowCheck._get_all_task_instances'
) as mock_get_all_task_instances:
check.check(None)

# Assert method calls
mock_get_all_task_instances.assert_has_calls(should_call_method, any_order=False)
with mock.patch(
'datadog_checks.airflow.airflow.AirflowCheck._get_all_task_instances'
) as mock_get_all_task_instances:
check.check(None)

# Assert method calls
mock_get_all_task_instances.assert_has_calls(should_call_method, any_order=False)
60 changes: 23 additions & 37 deletions consul/tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Licensed under a 3-clause BSD style license (see LICENSE)
import logging

import mock
import pytest

from datadog_checks.consul import ConsulCheck
Expand Down Expand Up @@ -155,25 +154,25 @@ def test_get_nodes_with_service_critical(aggregator):
aggregator.assert_metric('consul.catalog.services_count', value=1, tags=expected_tags)


def test_consul_request(aggregator, instance, mocker):
def test_consul_request(aggregator, instance, mocker, mock_http):
consul_check = ConsulCheck(common.CHECK_NAME, {}, [consul_mocks.MOCK_CONFIG])
mocker.patch("datadog_checks.base.utils.serialization.json.loads")
with mock.patch("datadog_checks.consul.consul.requests.Session.get") as mock_requests_get:

consul_check.consul_request("foo")
url = "{}/{}".format(instance["url"], "foo")
aggregator.assert_service_check("consul.can_connect", ConsulCheck.OK, tags=["url:{}".format(url)], count=1)

aggregator.reset()
mock_http.get.side_effect = Exception("message")
with pytest.raises(Exception):
consul_check.consul_request("foo")
url = "{}/{}".format(instance["url"], "foo")
aggregator.assert_service_check("consul.can_connect", ConsulCheck.OK, tags=["url:{}".format(url)], count=1)

aggregator.reset()
mock_requests_get.side_effect = Exception("message")
with pytest.raises(Exception):
consul_check.consul_request("foo")
aggregator.assert_service_check(
"consul.can_connect",
ConsulCheck.CRITICAL,
tags=["url:{}".format(url)],
count=1,
message="Consul request to {} failed: message".format(url),
)
aggregator.assert_service_check(
"consul.can_connect",
ConsulCheck.CRITICAL,
tags=["url:{}".format(url)],
count=1,
message="Consul request to {} failed: message".format(url),
)


def test_service_checks(aggregator):
Expand Down Expand Up @@ -648,26 +647,13 @@ def test_network_latency_node_name(
),
],
)
def test_config(test_case, extra_config, expected_http_kwargs, mocker):
def test_config(test_case, extra_config, expected_http_kwargs):
instance = extra_config
check = ConsulCheck(common.CHECK_NAME, {}, instances=[instance])
mocker.patch("datadog_checks.base.utils.serialization.json.loads")

with mock.patch('datadog_checks.base.utils.http.requests.Session') as session:
mock_session = mock.MagicMock()
session.return_value = mock_session
mock_session.get.return_value = mock.MagicMock(status_code=200)

check.check(None)

http_wargs = {
'auth': mock.ANY,
'cert': mock.ANY,
'headers': mock.ANY,
'proxies': mock.ANY,
'timeout': mock.ANY,
'verify': mock.ANY,
'allow_redirects': mock.ANY,
}
http_wargs.update(expected_http_kwargs)
mock_session.get.assert_called_with('/v1/status/leader', **http_wargs)
for key, value in expected_http_kwargs.items():
if key == 'headers':
for h_key, h_value in value.items():
assert check.http.get_header(h_key) == h_value
else:
assert check.http.options[key] == value
21 changes: 2 additions & 19 deletions couch/tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from copy import deepcopy
from unittest.mock import MagicMock

import mock
import pytest

from datadog_checks.couch import CouchDb
Expand All @@ -30,24 +29,8 @@ def test_config(test_case, extra_config, expected_http_kwargs):
instance.update(extra_config)
check = CouchDb(common.CHECK_NAME, {}, instances=[instance])

r = mock.MagicMock()
with mock.patch('datadog_checks.base.utils.http.requests.Session', return_value=r):
r.get.return_value = mock.MagicMock(status_code=200, content='{}')

check.check(instance)

http_wargs = {
'auth': mock.ANY,
'cert': mock.ANY,
'headers': mock.ANY,
'proxies': mock.ANY,
'timeout': mock.ANY,
'verify': mock.ANY,
'allow_redirects': mock.ANY,
}
http_wargs.update(expected_http_kwargs)

r.get.assert_called_with('http://{}:5984/_all_dbs/'.format(common.HOST), **http_wargs)
for key, value in expected_http_kwargs.items():
assert check.http.options[key] == value


def test_new_version_system_metrics(load_test_data):
Expand Down
14 changes: 14 additions & 0 deletions datadog_checks_base/datadog_checks/base/utils/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,20 @@ def __init__(self, instance, init_config, remapper=None, logger=None, session=No
self.tls_config = {key: value for key, value in config.items() if key.startswith('tls_')}
self._https_adapters = {}

def get_header(self, name: str, default: str | None = None) -> str | None:
"""Look up a request header by name. Lookup is case-insensitive."""
for key, value in self.options['headers'].items():
if key.lower() == name.lower():
return value
return default

def set_header(self, name: str, value: str) -> None:
for key in self.options['headers']:
if key.lower() == name.lower():
self.options['headers'][key] = value
return
self.options['headers'][name] = value

def get(self, url, **options):
return self._request('get', url, options)

Expand Down
13 changes: 13 additions & 0 deletions datadog_checks_base/datadog_checks/base/utils/http_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@
# Licensed under a 3-clause BSD style license (see LICENSE)
from __future__ import annotations

from collections.abc import Mapping
from typing import Any, Iterator, Protocol


class HTTPResponseProtocol(Protocol):
status_code: int
content: bytes
text: str
headers: Mapping[str, str]

def json(self, **kwargs: Any) -> Any: ...
def raise_for_status(self) -> None: ...
def close(self) -> None: ...
def iter_content(self, chunk_size: int | None = None, decode_unicode: bool = False) -> Iterator[bytes | str]: ...
def iter_lines(
self,
Expand All @@ -19,10 +28,14 @@ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool | None: ...


class HTTPClientProtocol(Protocol):
options: dict[str, Any]

def get(self, url: str, **options: Any) -> HTTPResponseProtocol: ...
def post(self, url: str, **options: Any) -> HTTPResponseProtocol: ...
def head(self, url: str, **options: Any) -> HTTPResponseProtocol: ...
def put(self, url: str, **options: Any) -> HTTPResponseProtocol: ...
def patch(self, url: str, **options: Any) -> HTTPResponseProtocol: ...
def delete(self, url: str, **options: Any) -> HTTPResponseProtocol: ...
def options_method(self, url: str, **options: Any) -> HTTPResponseProtocol: ...
def get_header(self, name: str, default: str | None = None) -> str | None: ...
def set_header(self, name: str, value: str) -> None: ...
31 changes: 31 additions & 0 deletions datadog_checks_base/tests/base/utils/http/test_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,34 @@ def test_extra_headers_on_http_method_call():
# make sure the original headers are not modified
assert http.options['headers'] == complete_headers
assert extra_headers == {"foo": "bar"}


def test_get_header_default_for_missing():
http = RequestsWrapper({}, {})
assert http.get_header('X-Missing') is None
assert http.get_header('X-Missing', 'fallback') == 'fallback'


def test_get_header_case_insensitive():
http = RequestsWrapper({}, {})
assert http.get_header('accept') == '*/*'
assert http.get_header('Accept') == '*/*'
assert http.get_header('ACCEPT') == '*/*'


def test_set_header():
http = RequestsWrapper({}, {})
http.set_header('X-Token', 'abc123')
assert http.get_header('X-Token') == 'abc123'
http.set_header('Accept', 'application/json')
assert http.get_header('Accept') == 'application/json'


def test_set_header_case_insensitive():
http = RequestsWrapper({}, {})
http.set_header('accept', 'application/json')
# Overwrites the existing 'Accept' key (preserving original casing)
assert http.get_header('Accept') == 'application/json'
assert http.options['headers']['Accept'] == 'application/json'
# No duplicate key created
assert sum(1 for k in http.options['headers'] if k.lower() == 'accept') == 1
Loading
Loading