Skip to content

Commit 7a77276

Browse files
Exclude parameter values from API request bodies (#681)
Co-authored-by: ykim-1 <[email protected]>
1 parent 41aee24 commit 7a77276

File tree

6 files changed

+229
-1
lines changed

6 files changed

+229
-1
lines changed

linodecli/api_request.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,13 @@ def _build_request_body(ctx, operation, parsed_args) -> Optional[str]:
259259
if ctx.defaults:
260260
parsed_args = ctx.config.update(parsed_args, operation.allowed_defaults)
261261

262+
param_names = {param.name for param in operation.params}
263+
262264
expanded_json = {}
263265

264266
# expand paths
265267
for k, v in vars(parsed_args).items():
266-
if v is None:
268+
if v is None or k in param_names:
267269
continue
268270

269271
cur = expanded_json

linodecli/baked/operation.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,20 @@ def __init__(
371371
if param.name not in {"apiVersion"}
372372
]
373373

374+
# Validation to ensure no conflicting arguments & param names are found.
375+
# This is necessary because arguments and parameters are both parsed into the
376+
# same result namespace by argparse.
377+
if self.request is not None and hasattr(self.request, "attrs"):
378+
param_names = {param.name for param in self.params}
379+
380+
for attr in self.request.attrs:
381+
if attr not in param_names:
382+
continue
383+
384+
raise ValueError(
385+
f"Attribute {attr.name} conflicts with parameter of the same name"
386+
)
387+
374388
(
375389
self.url_base,
376390
self.url_path,
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
openapi: 3.0.1
2+
info:
3+
title: API Specification
4+
version: 1.0.0
5+
servers:
6+
- url: http://localhost/v4
7+
8+
paths:
9+
/foo/bar/{barId}:
10+
parameters:
11+
- name: barId
12+
description: The ID of the bar.
13+
in: path
14+
required: true
15+
schema:
16+
type: string
17+
x-linode-cli-command: foo
18+
put:
19+
x-linode-cli-action: bar-update
20+
summary: update foobar
21+
operationId: fooBarPut
22+
description: This is description
23+
requestBody:
24+
description: >
25+
The parameters to set when updating the Foobar.
26+
required: True
27+
content:
28+
application/json:
29+
schema:
30+
allOf:
31+
- $ref: '#/components/schemas/FooBarUpdate'
32+
responses:
33+
'200':
34+
description: Successful response
35+
content:
36+
application/json:
37+
schema:
38+
$ref: '#/components/schemas/OpenAPIResponseAttr'
39+
40+
components:
41+
schemas:
42+
OpenAPIResponseAttr:
43+
type: object
44+
properties:
45+
filterable_result:
46+
x-linode-filterable: true
47+
type: string
48+
description: Filterable result value
49+
PaginationEnvelope:
50+
type: object
51+
properties:
52+
pages:
53+
type: integer
54+
readOnly: true
55+
description: The total number of pages.
56+
example: 1
57+
page:
58+
type: integer
59+
readOnly: true
60+
description: The current page.
61+
example: 1
62+
results:
63+
type: integer
64+
readOnly: true
65+
description: The total number of results.
66+
example: 1
67+
FooBarUpdate:
68+
type: object
69+
description: Foobar object request
70+
properties:
71+
test_param:
72+
x-linode-filterable: true
73+
type: integer
74+
description: The test parameter
75+
generic_arg:
76+
x-linode-filterable: true
77+
type: string
78+
description: The generic argument
79+
region:
80+
x-linode-filterable: true
81+
type: string
82+
description: The region
83+
nullable_int:
84+
type: integer
85+
nullable: true
86+
description: An arbitrary nullable int
87+
nullable_string:
88+
type: string
89+
nullable: true
90+
description: An arbitrary nullable string
91+
nullable_float:
92+
type: number
93+
nullable: true
94+
description: An arbitrary nullable float
95+
object_list:
96+
type: array
97+
description: An arbitrary list of objects.
98+
items:
99+
type: object
100+
description: An arbitrary object.
101+
properties:
102+
field_dict:
103+
type: object
104+
description: An arbitrary nested dict.
105+
properties:
106+
nested_string:
107+
type: string
108+
description: A deeply nested string.
109+
nested_int:
110+
type: number
111+
description: A deeply nested integer.
112+
field_array:
113+
type: array
114+
description: An arbitrary deeply nested array.
115+
items:
116+
type: string
117+
field_string:
118+
type: string
119+
description: An arbitrary field.
120+
field_int:
121+
type: number
122+
description: An arbitrary field.
123+
nullable_string:
124+
type: string
125+
description: An arbitrary nullable string.
126+
nullable: true

tests/integration/cli/test_help.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from tests.integration.helpers import (
66
contains_at_least_one_of,
7+
exec_failing_test_command,
78
exec_test_command,
89
)
910

@@ -50,3 +51,30 @@ def test_help_page_for_aliased_actions():
5051

5152
assert "You may filter results with:" in wrapped_output
5253
assert "--tags" in wrapped_output
54+
55+
56+
def test_debug_output_contains_request_url(monkeypatch: pytest.MonkeyPatch):
57+
env_vars = {
58+
"LINODE_CLI_API_HOST": "api.linode.com",
59+
"LINODE_CLI_API_VERSION": "v4",
60+
"LINODE_CLI_API_SCHEME": "https",
61+
}
62+
for key, value in env_vars.items():
63+
monkeypatch.setenv(key, value)
64+
65+
output = exec_failing_test_command(
66+
[
67+
"linode-cli",
68+
"linodes",
69+
"update",
70+
"--label",
71+
"foobar",
72+
"12345",
73+
"--debug",
74+
]
75+
).stderr.decode()
76+
wrapped_output = textwrap.fill(output, width=180).replace("\n", "")
77+
78+
assert (
79+
"PUT https://api.linode.com/v4/linode/instances/12345" in wrapped_output
80+
)

tests/unit/conftest.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,37 @@ def create_operation():
182182
return create_operation
183183

184184

185+
@pytest.fixture
186+
def update_operation():
187+
"""
188+
Creates the following CLI operation:
189+
190+
linode-cli foo bar-update --generic_arg [generic_arg] test_param
191+
192+
PUT http://localhost/v4/foo/bar/{fooId}
193+
{
194+
"generic_arg": "[generic_arg]",
195+
"test_param": test_param
196+
}
197+
"""
198+
199+
spec = _get_parsed_spec("api_request_test_foobar_put.yaml")
200+
201+
dict_values = list(spec.paths.values())
202+
203+
# Get parameters for OpenAPIOperation() from yaml fixture
204+
path = dict_values[0]
205+
command = path.extensions.get("linode-cli-command", "default")
206+
operation = getattr(path, "put")
207+
method = "put"
208+
209+
create_operation = make_test_operation(
210+
command, operation, method, path.parameters
211+
)
212+
213+
return create_operation
214+
215+
185216
@pytest.fixture
186217
def list_operation_for_output_tests():
187218
"""

tests/unit/test_api_request.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,33 @@ def validate_http_request(url, headers=None, data=None, **kwargs):
389389

390390
assert result == mock_response
391391

392+
def test_do_request_put(self, mock_cli, update_operation):
393+
mock_response = Mock(status_code=200, reason="OK")
394+
395+
def validate_http_request(url, headers=None, data=None, **kwargs):
396+
assert url == "http://localhost/v4/foo/bar/567"
397+
assert data == json.dumps(
398+
{
399+
"test_param": 12345,
400+
"generic_arg": "foobar",
401+
"region": "us-southeast", # default
402+
}
403+
)
404+
assert "Authorization" in headers
405+
406+
return mock_response
407+
408+
update_operation.allowed_defaults = ["region"]
409+
410+
with patch("linodecli.api_request.requests.put", validate_http_request):
411+
result = api_request.do_request(
412+
mock_cli,
413+
update_operation,
414+
["--generic_arg", "foobar", "--test_param", "12345", "567"],
415+
)
416+
417+
assert result == mock_response
418+
392419
def test_outdated_cli(self, mock_cli):
393420
# "outdated" version
394421
mock_cli.suppress_warnings = False

0 commit comments

Comments
 (0)