Skip to content

Commit 97f7cd5

Browse files
committed
Add api command to client as escape hatch
for when otherwise-undefined API tomfoolery is needed
1 parent 2c564d7 commit 97f7cd5

File tree

2 files changed

+145
-19
lines changed

2 files changed

+145
-19
lines changed

fuzzbucket_client/__main__.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,26 @@ def main(sysargs: list[str] = sys.argv[:]) -> int:
493493
)
494494
parser_whoami.set_defaults(func=client.whoami)
495495

496+
parser_api = subparsers.add_parser(
497+
"api", help="make an arbitrary HTTP request to the fuzzbucket API"
498+
)
499+
parser_api.add_argument(
500+
"-X", "--request", default="GET", help="request method to use"
501+
)
502+
parser_api.add_argument(
503+
"-H", "--header", default=[], action="append", help="pass custom header(s)"
504+
)
505+
parser_api.add_argument(
506+
"-i",
507+
"--include",
508+
action="store_true",
509+
help="include response headers in the output",
510+
)
511+
parser_api.add_argument(
512+
"url", help="full URL or request path to be joined to FUZZBUCKET_URL"
513+
)
514+
parser_api.set_defaults(func=client.api)
515+
496516
known_args, unknown_args = parser.parse_known_args(sysargs[1:])
497517
known_args = _normalize_known_args(known_args)
498518
config_logging(level=logging.DEBUG if known_args.debug else logging.INFO)
@@ -1099,6 +1119,31 @@ def whoami(self, *_):
10991119
print("you are not currently logged in")
11001120
return False
11011121

1122+
@_command
1123+
def api(self, known_args, _):
1124+
full_url = known_args.url
1125+
1126+
if not full_url.startswith("http"):
1127+
full_url = _pjoin(self._url, full_url)
1128+
1129+
req = self._build_request(full_url, method=known_args.request)
1130+
1131+
for header in known_args.header:
1132+
key, value = header.split(":", maxsplit=1)
1133+
req.headers[key] = value
1134+
1135+
with self._urlopen(req) as response:
1136+
if known_args.include:
1137+
for header in response.headers:
1138+
print(f"{header}: {response.headers[header]}")
1139+
1140+
print("")
1141+
1142+
for chunk in response:
1143+
sys.stdout.write(chunk.decode("utf-8"))
1144+
1145+
return True
1146+
11021147
def _find_box(self, box_search):
11031148
results = self._find_boxes(box_search)
11041149

tests/test_fuzzbucket_client.py

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import argparse
22
import contextlib
33
import datetime
4+
import email
45
import io
56
import json
67
import logging
78
import os
89
import random
910
import re
1011
import urllib.request
12+
import urllib.response
1113

1214
import pytest
1315

@@ -84,7 +86,7 @@ def fake_urlopen(request):
8486
if request_hook is not None:
8587
request_hook(request)
8688
if request.get_method() in empty_methods:
87-
yield io.StringIO("")
89+
yield io.BytesIO()
8890
return
8991
if http_exc is not None:
9092
raise urllib.request.HTTPError(*(list(http_exc) + [response]))
@@ -301,8 +303,10 @@ def test_client_ls(monkeypatch, args, returning):
301303
client,
302304
"_urlopen",
303305
gen_fake_urlopen(
304-
io.StringIO(
305-
json.dumps({"boxes": [{"name": "sparkles", "fancy": "probably"}]})
306+
io.BytesIO(
307+
json.dumps(
308+
{"boxes": [{"name": "sparkles", "fancy": "probably"}]}
309+
).encode()
306310
)
307311
),
308312
)
@@ -405,7 +409,7 @@ def test_client_logout(monkeypatch):
405409
monkeypatch.setattr(
406410
client,
407411
"_urlopen",
408-
gen_fake_urlopen(io.StringIO("")),
412+
gen_fake_urlopen(io.BytesIO()),
409413
)
410414
ret = fuzzbucket_client.__main__.main(["fuzzbucket-client", "logout"])
411415
assert ret == 0
@@ -593,7 +597,9 @@ def test_client_create(
593597
monkeypatch.setattr(
594598
client,
595599
"_urlopen",
596-
gen_fake_urlopen(io.StringIO(json.dumps(api_response)), http_exc=http_exc),
600+
gen_fake_urlopen(
601+
io.BytesIO(json.dumps(api_response).encode()), http_exc=http_exc
602+
),
597603
)
598604
ret = fuzzbucket_client.__main__.main(
599605
["fuzzbucket-client", "create"] + list(cmd_args)
@@ -800,7 +806,7 @@ def request_hook(request):
800806
client,
801807
"_urlopen",
802808
gen_fake_urlopen(
803-
io.StringIO(json.dumps(api_response)),
809+
io.BytesIO(json.dumps(api_response).encode()),
804810
http_exc=http_exc,
805811
request_hook=request_hook,
806812
),
@@ -866,7 +872,7 @@ def test_client_delete(
866872
client,
867873
"_urlopen",
868874
gen_fake_urlopen(
869-
io.StringIO(json.dumps(api_response)),
875+
io.BytesIO(json.dumps(api_response).encode()),
870876
http_exc=http_exc,
871877
empty_methods=("DELETE",),
872878
),
@@ -927,7 +933,7 @@ def test_client_reboot(
927933
client,
928934
"_urlopen",
929935
gen_fake_urlopen(
930-
io.StringIO(json.dumps(api_response)),
936+
io.BytesIO(json.dumps(api_response).encode()),
931937
http_exc=http_exc,
932938
empty_methods=("POST",),
933939
),
@@ -1130,7 +1136,7 @@ def test_client_list_aliases(
11301136
monkeypatch.setattr(
11311137
client,
11321138
"_urlopen",
1133-
gen_fake_urlopen(io.StringIO(json.dumps(api_response))),
1139+
gen_fake_urlopen(io.BytesIO(json.dumps(api_response).encode())),
11341140
)
11351141
client.data_format = data_format
11361142

@@ -1175,7 +1181,7 @@ def test_client_create_alias(
11751181
monkeypatch.setattr(
11761182
client,
11771183
"_urlopen",
1178-
gen_fake_urlopen(io.StringIO(json.dumps(api_response))),
1184+
gen_fake_urlopen(io.BytesIO(json.dumps(api_response).encode())),
11791185
)
11801186
client.data_format = data_format
11811187

@@ -1194,7 +1200,7 @@ def test_client_delete_alias(monkeypatch, caplog):
11941200
client = fuzzbucket_client.__main__.Client()
11951201
monkeypatch.setattr(fuzzbucket_client.__main__, "default_client", lambda: client)
11961202

1197-
monkeypatch.setattr(client, "_urlopen", gen_fake_urlopen(io.StringIO("")))
1203+
monkeypatch.setattr(client, "_urlopen", gen_fake_urlopen(io.BytesIO()))
11981204

11991205
assert client.delete_alias(argparse.Namespace(alias="hurr"), "unknown")
12001206
assert "deleted alias" in caplog.text
@@ -1223,7 +1229,7 @@ def test_client_get_key(monkeypatch, capsys, data_format, matches):
12231229
monkeypatch.setattr(
12241230
client,
12251231
"_urlopen",
1226-
gen_fake_urlopen(io.StringIO('{"key":{"any":"fields","allowed":true}}')),
1232+
gen_fake_urlopen(io.BytesIO(b'{"key":{"any":"fields","allowed":true}}')),
12271233
)
12281234

12291235
assert client.get_key(argparse.Namespace(alias="hurr"), "unknown")
@@ -1271,7 +1277,7 @@ def test_client_list_keys(monkeypatch, capsys, data_format, matches):
12711277
client,
12721278
"_urlopen",
12731279
gen_fake_urlopen(
1274-
io.StringIO('{"keys":[{"braking":"litho","retrograde":true}]}')
1280+
io.BytesIO(b'{"keys":[{"braking":"litho","retrograde":true}]}')
12751281
),
12761282
)
12771283

@@ -1336,7 +1342,7 @@ def test_client_add_key(monkeypatch, caplog, alias, filename, key_material, succ
13361342
monkeypatch.setattr(
13371343
client,
13381344
"_urlopen",
1339-
gen_fake_urlopen(io.StringIO('{"key":{"braking":"litho","retrograde":true}}')),
1345+
gen_fake_urlopen(io.BytesIO(b'{"key":{"braking":"litho","retrograde":true}}')),
13401346
)
13411347

13421348
filename.text_content = key_material
@@ -1353,7 +1359,7 @@ def test_client_delete_key(monkeypatch, caplog):
13531359
monkeypatch.setattr(
13541360
client,
13551361
"_urlopen",
1356-
gen_fake_urlopen(io.StringIO('{"key":{"braking":"litho","retrograde":true}}')),
1362+
gen_fake_urlopen(io.BytesIO(b'{"key":{"braking":"litho","retrograde":true}}')),
13571363
)
13581364

13591365
assert client.delete_key(argparse.Namespace(alias="hurr"), "unknown")
@@ -1363,9 +1369,9 @@ def test_client_delete_key(monkeypatch, caplog):
13631369
@pytest.mark.parametrize(
13641370
("args", "response", "out_match", "returning"),
13651371
[
1366-
pytest.param(("whoami",), '{"you":"castleface"}', "castleface", 0),
1367-
pytest.param(("-j", "whoami"), '{"you":"castleface"}', "castleface", 0),
1368-
pytest.param(("whoami",), "{}", None, 86),
1372+
pytest.param(("whoami",), b'{"you":"castleface"}', "castleface", 0),
1373+
pytest.param(("-j", "whoami"), b'{"you":"castleface"}', "castleface", 0),
1374+
pytest.param(("whoami",), b"{}", None, 86),
13691375
],
13701376
)
13711377
def test_client_whoami(args, response, out_match, returning, monkeypatch, capsys):
@@ -1375,11 +1381,86 @@ def test_client_whoami(args, response, out_match, returning, monkeypatch, capsys
13751381
monkeypatch.setattr(
13761382
client,
13771383
"_urlopen",
1378-
gen_fake_urlopen(io.StringIO(response)),
1384+
gen_fake_urlopen(io.BytesIO(response)),
1385+
)
1386+
1387+
ret = fuzzbucket_client.__main__.main(["fuzzbucket-client"] + list(args))
1388+
assert ret == returning
1389+
1390+
if out_match is not None:
1391+
assert out_match in capsys.readouterr().out
1392+
1393+
1394+
@pytest.mark.parametrize(
1395+
("args", "response", "http_exc", "out_match", "returning"),
1396+
[
1397+
pytest.param(
1398+
("api", "foul/form"),
1399+
b'{"funeral":"solution"}',
1400+
None,
1401+
'{"funeral":"solution"}',
1402+
0,
1403+
id="simple",
1404+
),
1405+
pytest.param(
1406+
("-j", "api", "foul/form"),
1407+
b"<solution>funeral</solution>",
1408+
None,
1409+
"<solution>funeral</solution>",
1410+
0,
1411+
id="ignored_json_format",
1412+
),
1413+
pytest.param(
1414+
("api", "-H", "Spicy: 11", "-i", "foul/form"),
1415+
b'{"funeral":"solution"}',
1416+
None,
1417+
'Bear: Foz\n\n{"funeral":"solution"}',
1418+
0,
1419+
id="headers_time",
1420+
),
1421+
pytest.param(
1422+
("api", "frock/block"),
1423+
b"perm act\n",
1424+
(
1425+
"http://fakety.example.org/fake",
1426+
418,
1427+
b'{"hecking":"toot"}',
1428+
[("Content-Type", "application/json")],
1429+
),
1430+
None,
1431+
86,
1432+
id="teapot",
1433+
),
1434+
],
1435+
)
1436+
def test_client_api(
1437+
args,
1438+
response,
1439+
http_exc,
1440+
out_match,
1441+
returning,
1442+
monkeypatch,
1443+
capsys,
1444+
):
1445+
client = fuzzbucket_client.__main__.Client()
1446+
monkeypatch.setattr(fuzzbucket_client.__main__, "default_client", lambda: client)
1447+
1448+
monkeypatch.setattr(
1449+
client,
1450+
"_urlopen",
1451+
gen_fake_urlopen(
1452+
urllib.response.addinfourl(
1453+
io.BytesIO(response),
1454+
email.message_from_string("Bear: Foz\n"),
1455+
"http://ok.bears.example.org",
1456+
),
1457+
http_exc=http_exc,
1458+
),
13791459
)
13801460

13811461
ret = fuzzbucket_client.__main__.main(["fuzzbucket-client"] + list(args))
13821462
assert ret == returning
1463+
13831464
if out_match is not None:
13841465
assert out_match in capsys.readouterr().out
13851466

0 commit comments

Comments
 (0)