Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
22 changes: 15 additions & 7 deletions codecov-cli/codecov_cli/helpers/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,33 @@

USER_AGENT = f"codecov-cli/{__version__}"

_extra_headers: dict = {}

def _set_user_agent(headers: Optional[dict] = None) -> dict:

def set_extra_headers(headers: dict):
global _extra_headers
_extra_headers = dict(headers)


def _prepare_headers(headers: Optional[dict] = None) -> dict:
headers = headers or {}
headers.setdefault("User-Agent", USER_AGENT)
return headers
merged = {**_extra_headers, **headers}
merged["User-Agent"] = USER_AGENT
return merged


def patch(url: str, headers: dict = None, json: dict = None) -> requests.Response:
headers = _set_user_agent(headers)
headers = _prepare_headers(headers)
return requests.patch(url, json=json, headers=headers)


def get(url: str, headers: dict = None, params: dict = None) -> requests.Response:
headers = _set_user_agent(headers)
headers = _prepare_headers(headers)
return requests.get(url, params=params, headers=headers)


def put(url: str, data: dict = None, headers: dict = None) -> requests.Response:
headers = _set_user_agent(headers)
headers = _prepare_headers(headers)
return requests.put(url, data=data, headers=headers)


Expand All @@ -44,7 +52,7 @@ def post(
headers: Optional[dict] = None,
params: Optional[dict] = None,
) -> requests.Response:
headers = _set_user_agent(headers)
headers = _prepare_headers(headers)
return requests.post(url, json=data, headers=headers, params=params)


Expand Down
18 changes: 18 additions & 0 deletions codecov-cli/codecov_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from codecov_cli.helpers.ci_adapters import get_ci_adapter, get_ci_providers_list
from codecov_cli.helpers.config import load_cli_config
from codecov_cli.helpers.logging_utils import configure_logger
from codecov_cli.helpers.request import set_extra_headers
from codecov_cli.helpers.versioning_systems import get_versioning_system
from codecov_cli.opentelemetry import init_telem

Expand All @@ -48,6 +49,11 @@
@click.option(
"--disable-telem", help="Disable sending telemetry data to Codecov", is_flag=True
)
@click.option(
"--http-header",
multiple=True,
help="Extra HTTP header to send with every request (format: Header-Name:Value). Can be specified multiple times.",
)
@click.pass_context
@click.version_option(__version__, prog_name="codecovcli")
def cli(
Expand All @@ -57,6 +63,7 @@ def cli(
enterprise_url: str,
verbose: bool = False,
disable_telem: bool = False,
http_header: typing.Tuple[str, ...] = (),
Comment thread
cursor[bot] marked this conversation as resolved.
):
ctx.obj["cli_args"] = ctx.params
ctx.obj["cli_args"]["version"] = f"cli-{__version__}"
Expand All @@ -72,6 +79,17 @@ def cli(
ctx.obj["enterprise_url"] = enterprise_url
ctx.obj["disable_telem"] = disable_telem
ctx.obj["branding"] = [Branding.CODECOV]
if http_header:
extra = {}
for h in http_header:
if ":" not in h:
raise click.BadParameter(
f"Invalid header format: '{h}'. Expected 'Header-Name:Value'.",
param_hint="'--http-header'",
)
name, value = h.split(":", 1)
extra[name.strip()] = value.strip()
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
set_extra_headers(extra)
Comment thread
Vignesh-285 marked this conversation as resolved.
init_telem(ctx.obj)


Expand Down
65 changes: 65 additions & 0 deletions codecov-cli/tests/helpers/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
from requests import Response

from codecov_cli import __version__
from codecov_cli.helpers import request as request_module
from codecov_cli.helpers.request import (
_prepare_headers,
get,
get_token_header,
get_token_header_or_fail,
log_warnings_and_errors_if_any,
set_extra_headers,
)
from codecov_cli.helpers.request import logger as req_log
from codecov_cli.helpers.request import (
Expand Down Expand Up @@ -186,3 +189,65 @@ def mock_request(*args, headers={}, **kwargs):
side_effect=mock_request,
)
patch("my_url")


class TestExtraHeaders:
@pytest.fixture(autouse=True)
def reset_extra_headers(self):
set_extra_headers({})
yield
set_extra_headers({})

def test_prepare_headers_without_extra(self):
headers = _prepare_headers()
assert headers == {"User-Agent": f"codecov-cli/{__version__}"}

def test_prepare_headers_with_extra(self):
set_extra_headers({"CF-Access-Client-Id": "abc123"})
headers = _prepare_headers()
assert headers["CF-Access-Client-Id"] == "abc123"
assert headers["User-Agent"] == f"codecov-cli/{__version__}"

def test_extra_headers_dont_overwrite_authorization(self):
set_extra_headers({"Authorization": "evil"})
headers = _prepare_headers({"Authorization": "token real-token"})
assert headers["Authorization"] == "token real-token"

def test_extra_headers_dont_overwrite_user_agent(self):
set_extra_headers({"User-Agent": "custom-agent"})
headers = _prepare_headers()
assert headers["User-Agent"] == f"codecov-cli/{__version__}"

def test_extra_headers_merged_into_post(self, mocker):
set_extra_headers({"X-Custom": "value"})

def mock_post(*args, headers=None, **kwargs):
assert headers["X-Custom"] == "value"
assert headers["User-Agent"] == f"codecov-cli/{__version__}"
resp = Response()
resp.status_code = 200
resp._content = b"ok"
return resp

mocker.patch.object(requests, "post", side_effect=mock_post)
send_post_request("my_url")

def test_extra_headers_merged_into_get(self, mocker):
set_extra_headers({"X-Custom": "value"})

def mock_get(*args, headers=None, **kwargs):
assert headers["X-Custom"] == "value"
resp = Response()
resp.status_code = 200
resp._content = b"ok"
return resp

mocker.patch.object(requests, "get", side_effect=mock_get)
get("my_url")

def test_set_extra_headers_replaces_previous(self):
set_extra_headers({"A": "1"})
set_extra_headers({"B": "2"})
headers = _prepare_headers()
assert "A" not in headers
assert headers["B"] == "2"
46 changes: 46 additions & 0 deletions codecov-cli/tests/test_codecov_cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import pytest
from click.testing import CliRunner

from codecov_cli import main
from codecov_cli.helpers import request as request_module


def test_existing_commands():
Expand All @@ -17,3 +21,45 @@ def test_existing_commands():
"upload-coverage",
"upload-process",
]


class TestHttpHeaderOption:
@pytest.fixture(autouse=True)
def reset_extra_headers(self):
request_module._extra_headers = {}
yield
request_module._extra_headers = {}

def test_http_header_valid(self):
runner = CliRunner()
result = runner.invoke(
main.cli,
[
"--http-header",
"CF-Access-Client-Id:abc123",
"--http-header",
"CF-Access-Client-Secret:xyz789",
"--help",
],
obj={},
)
assert result.exit_code == 0
Comment thread
cursor[bot] marked this conversation as resolved.

def test_http_header_invalid_format(self):
runner = CliRunner()
result = runner.invoke(
main.cli,
["--http-header", "InvalidHeader", "do-upload", "--help"],
obj={},
)
assert result.exit_code != 0
assert "Invalid header format" in result.output

def test_http_header_value_with_colon(self):
runner = CliRunner()
result = runner.invoke(
main.cli,
["--http-header", "X-Test:value:with:colons", "--help"],
obj={},
)
assert result.exit_code == 0