Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions linodecli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def main(): # pylint: disable=too-many-branches,too-many-statements
cli.page = parsed.page
cli.page_size = parsed.page_size
cli.debug_request = parsed.debug
cli.raw_body = parsed.raw_body

if parsed.as_user and not skip_config:
cli.config.set_user(parsed.as_user)
Expand Down
38 changes: 33 additions & 5 deletions linodecli/api_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,21 +348,49 @@ def _build_request_body(

:return: A JSON string representing the request body, or None if not applicable.
"""
if operation.method == "get":
# Get operations don't have a body
if operation.method in ("get", "delete"):
# GET and DELETE operations don't have a body
if ctx.raw_body is not None:
print(
f"--raw-body cannot be specified for actions with method {operation.method}",
file=sys.stderr,
)
sys.exit(ExitCodes.ARGUMENT_ERROR)

return None

param_names = {param.name for param in operation.params}

# Returns whether the given argument should be included in the request body
def __should_include(key: str, value: Any) -> bool:
return value is not None and key not in param_names

# If the user has specified the --raw-body argument,
# return it.
if ctx.raw_body is not None:
specified_keys = [
k for k, v in vars(parsed_args).items() if __should_include(k, v)
]

if len(specified_keys) > 0:
print(
"--raw-body cannot be specified with action arguments: "
+ ", ".join(sorted(f"--{key}" for key in specified_keys)),
file=sys.stderr,
)
sys.exit(ExitCodes.ARGUMENT_ERROR)

return ctx.raw_body

# Merge defaults into body if applicable
if ctx.defaults:
parsed_args = ctx.config.update(parsed_args, operation.allowed_defaults)

param_names = {param.name for param in operation.params}

expanded_json = {}

# Expand dotted keys into nested dictionaries
for k, v in vars(parsed_args).items():
if v is None or k in param_names:
if not __should_include(k, v):
continue

path_segments = get_path_segments(k)
Expand Down
8 changes: 8 additions & 0 deletions linodecli/arg_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ def register_args(parser: ArgumentParser) -> ArgumentParser:
help="The alias to set or remove.",
)

parser.add_argument(
"--raw-body",
type=str,
help="The raw JSON to use as the request body of an action. "
+ "This argument cannot be used if action-specific arguments are specified. "
+ "Additionally, this argument can only be used with POST and PUT actions.",
)

# Register shared argument groups
register_output_args_shared(parser)
register_pagination_args_shared(parser)
Expand Down
1 change: 1 addition & 0 deletions linodecli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__(self, version, base_url, skip_config=False):
self.base_url = base_url
self.spec_version = "None"
self.suppress_warnings = False
self.raw_body = None

self.output_handler = OutputHandler()
self.config = CLIConfig(self.base_url, skip_config=skip_config)
Expand Down
85 changes: 85 additions & 0 deletions tests/integration/cli/test_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import json

from linodecli.exit_codes import ExitCodes
from tests.integration.helpers import (
exec_failing_test_command,
exec_test_command,
get_random_region_with_caps,
get_random_text,
)


def test_arg_raw_body():
label = get_random_text(12)
region = get_random_region_with_caps(["VPCs"])

res = json.loads(
exec_test_command(
[
"linode-cli",
"vpcs",
"create",
"--json",
"--raw-body",
json.dumps(
{
"label": label,
"region": region,
}
),
],
)
)

exec_test_command(["linode-cli", "vpcs", "delete", str(res[0]["id"])])

assert res[0]["id"] > 0
assert res[0]["label"] == label
assert res[0]["region"] == region


def test_arg_raw_body_conflict():
label = get_random_text(12)
region = get_random_region_with_caps(["VPCs"])

res = exec_failing_test_command(
[
"linode-cli",
"vpcs",
"create",
"--json",
"--label",
label,
"--region",
region,
"--raw-body",
json.dumps(
{
"label": label,
"region": region,
}
),
],
expected_code=ExitCodes.ARGUMENT_ERROR,
)

assert (
"--raw-body cannot be specified with action arguments: --label, --region"
in res
)


def test_arg_raw_body_get():
res = exec_failing_test_command(
[
"linode-cli",
"vpcs",
"list",
"--json",
"--raw-body",
json.dumps({"label": "test"}),
],
expected_code=ExitCodes.ARGUMENT_ERROR,
)

assert "--raw-body cannot be specified for actions with method get" in res
68 changes: 67 additions & 1 deletion tests/unit/test_api_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@

import pytest
import requests
from _pytest.capture import CaptureFixture

from linodecli import api_request
from linodecli import ExitCodes, api_request
from linodecli.baked.operation import (
ExplicitEmptyDictValue,
ExplicitEmptyListValue,
Expand Down Expand Up @@ -163,6 +164,71 @@ def test_build_request_body_non_null_field(
== result
)

def test_build_request_body_raw(self, mock_cli, create_operation):
body = {"foo": "bar"}

mock_cli.raw_body = json.dumps(body)

result = api_request._build_request_body(
mock_cli,
create_operation,
SimpleNamespace(),
)
assert json.loads(result) == body

def test_build_request_body_raw_with_defaults(
self, mock_cli, create_operation
):
body = {"foo": "bar"}
mock_cli.raw_body = json.dumps(body)

mock_cli.defaults = True
mock_cli.config.get = lambda user, key, **kwargs: {"foo": "baz"}
create_operation.allowed_defaults = ["foo"]

result = api_request._build_request_body(
mock_cli,
create_operation,
SimpleNamespace(),
)
assert json.loads(result) == body

def test_build_request_body_raw_conflict(
self, mock_cli, create_operation, capsys: CaptureFixture
):
mock_cli.raw_body = json.dumps({"foo": "bar"})

with pytest.raises(SystemExit) as err:
api_request._build_request_body(
mock_cli,
create_operation,
SimpleNamespace(foo="bar", bar="foo"),
)

assert err.value.code == ExitCodes.ARGUMENT_ERROR
assert (
"--raw-body cannot be specified with action arguments: --bar, --foo"
in capsys.readouterr().err
)

def test_build_request_body_raw_get(
self, mock_cli, list_operation, capsys: CaptureFixture
):
mock_cli.raw_body = json.dumps({"foo": "bar"})

with pytest.raises(SystemExit) as err:
api_request._build_request_body(
mock_cli,
list_operation,
SimpleNamespace(),
)

assert err.value.code == ExitCodes.ARGUMENT_ERROR
assert (
"--raw-body cannot be specified for actions with method get"
in capsys.readouterr().err
)

def test_build_request_url_get(self, mock_cli, list_operation):
result = api_request._build_request_url(
mock_cli, list_operation, SimpleNamespace()
Expand Down