mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 05:29:45 +00:00
Keep RBAC administrative recipes bounded and private
Reuse the existing private-response API helper for all role and user operations, preserving exact methods, bodies, privileges and targets. Document independent decisions and uncertain mutation readback; exercise copied commands against synthetic private success, denial, redirect and incomplete responses without exposing credentials or retrying. Contract-Neutral: Documentation, mirrored guides and copied-recipe regression tests only; no runtime authentication, RBAC, API or frontend behaviour changes. Change-source: pulse-maintainer
This commit is contained in:
+11
-2
@@ -58,7 +58,7 @@ pulse_api() (
|
||||
set -eu
|
||||
umask 077
|
||||
if [ "$#" -ne 2 ]; then
|
||||
printf 'Usage: pulse_api GET|POST /api/path\n' >&2
|
||||
printf 'Usage: pulse_api GET|POST|PUT|DELETE /api/path\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
method=$1
|
||||
@@ -70,7 +70,9 @@ pulse_api() (
|
||||
case "$method" in
|
||||
GET) set -- ;;
|
||||
POST) set -- --header 'Content-Type: application/json' --request POST --data-binary @- ;;
|
||||
*) printf 'This example helper accepts only GET and POST.\n' >&2; exit 2 ;;
|
||||
PUT) set -- --header 'Content-Type: application/json' --request PUT --data-binary @- ;;
|
||||
DELETE) set -- --request DELETE ;;
|
||||
*) printf 'This example helper accepts only GET, POST, PUT and DELETE.\n' >&2; exit 2 ;;
|
||||
esac
|
||||
auth_dir="$HOME/.config/pulse"
|
||||
auth_file="$auth_dir/api-header"
|
||||
@@ -118,6 +120,13 @@ No request is retried automatically, especially a POST whose outcome is
|
||||
uncertain: inspect the existing action and target before deciding what to do.
|
||||
A 2xx response is only HTTP success, not proof that an action completed.
|
||||
|
||||
The helper also supports the existing [RBAC administration examples](RBAC.md#managing-roles).
|
||||
POST and PUT read their JSON body from standard input; GET and DELETE send no
|
||||
body. A method being supported is not permission to change a resource. Choose
|
||||
each administrative change separately and check its current state afterwards.
|
||||
A timeout or incomplete response can occur **after a change was applied**;
|
||||
do not repeat a POST, PUT or DELETE merely because the response was lost.
|
||||
|
||||
Keep `--disable` first: it ignores local curl configuration that could enable
|
||||
trace output, bypass TLS or follow redirects. On an authentication-enabled
|
||||
instance, the protected summary checks token access; `/api/health` is public
|
||||
|
||||
+30
-28
@@ -55,15 +55,29 @@ full-access (`*`) API token bound to an authorised administrator; a monitoring
|
||||
token is not enough. Do not widen an agent's token for this job.
|
||||
|
||||
If you already use an administration token, prepare its private header file
|
||||
as described in [API authentication](API.md#-authentication). The examples read
|
||||
`$HOME/.config/pulse/api-header`; never paste the token or a session cookie into
|
||||
a command. Keep this file outside shared repositories and diagnostics.
|
||||
and define `pulse_api` **in the same Bash session** using
|
||||
[API authentication](API.md#-authentication). The helper is local example code,
|
||||
not an installed Pulse command. Never paste the token or a session cookie into
|
||||
a command, URL or report, or use browser **Copy as cURL**.
|
||||
|
||||
The loopback URLs work on the Pulse host. For remote use, substitute your
|
||||
Pulse HTTPS URL and keep certificate verification enabled. Use curl 7.76 or
|
||||
later; `--disable` must remain first to ignore local trace/verbose defaults,
|
||||
and `--fail-with-body` makes HTTP failures return a non-zero exit. Run each
|
||||
change separately and check its response before continuing.
|
||||
On the Pulse host, the helper uses loopback; for remote use, follow that guide's
|
||||
HTTPS-origin instructions and keep certificate verification enabled. It makes
|
||||
one request with a five-second connection limit and a twenty-second total limit,
|
||||
prints only the HTTP status and saves each response in a new owner-only file.
|
||||
Inspect the file privately, including on failure: role lists, user identities
|
||||
and errors can contain private information. Share only a relevant redacted
|
||||
excerpt, not full responses, header files or tokens. HTTP 401/402/403, redirects
|
||||
and transport errors are not an empty role or user list.
|
||||
|
||||
**These are separate operations, not a script to run from top to bottom.**
|
||||
Choose the intended target and change before making a request; preserve an
|
||||
existing administrator recovery path. Deleting a role or user and sending an
|
||||
empty role list remove access. After a change, check that target's saved roles
|
||||
and intended access through the normal UI or a separate read-only request.
|
||||
The helper does not follow redirects or retry. A timeout or partial response
|
||||
can occur **after the change was applied**; inspect the current state before
|
||||
deciding what to do, rather than repeating the mutation blindly. HTTP success
|
||||
alone does not establish the intended access or disable an upstream IdP account.
|
||||
|
||||
### Creating a Role
|
||||
|
||||
@@ -74,9 +88,7 @@ Use a new custom role ID. Built-in roles (`admin`, `operator`, `viewer`,
|
||||
|
||||
**API:**
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--header 'Content-Type: application/json' --request POST \
|
||||
--data-binary @- http://127.0.0.1:7655/api/admin/roles <<'JSON'
|
||||
pulse_api POST /api/admin/roles <<'JSON'
|
||||
{
|
||||
"id": "alert-manager",
|
||||
"name": "Alert Manager",
|
||||
@@ -93,16 +105,13 @@ JSON
|
||||
### Listing Roles
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/admin/roles
|
||||
pulse_api GET /api/admin/roles
|
||||
```
|
||||
|
||||
### Updating a Role
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--header 'Content-Type: application/json' --request PUT \
|
||||
--data-binary @- http://127.0.0.1:7655/api/admin/roles/alert-manager <<'JSON'
|
||||
pulse_api PUT /api/admin/roles/alert-manager <<'JSON'
|
||||
{
|
||||
"name": "Alert Manager",
|
||||
"description": "Updated description",
|
||||
@@ -119,8 +128,7 @@ JSON
|
||||
### Deleting a Role
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--request DELETE http://127.0.0.1:7655/api/admin/roles/alert-manager
|
||||
pulse_api DELETE /api/admin/roles/alert-manager
|
||||
```
|
||||
|
||||
---
|
||||
@@ -130,8 +138,7 @@ curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
### Listing Users and Their Roles
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/admin/users
|
||||
pulse_api GET /api/admin/users
|
||||
```
|
||||
|
||||
SSO users are displayed using the latest configured username claim and email
|
||||
@@ -148,9 +155,7 @@ SSO principal). `jane` below is an example local username. Create the custom
|
||||
role before assigning it; do not run the deletion example first.
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--header 'Content-Type: application/json' --request PUT \
|
||||
--data-binary @- http://127.0.0.1:7655/api/admin/users/jane/roles <<'JSON'
|
||||
pulse_api PUT /api/admin/users/jane/roles <<'JSON'
|
||||
{"roleIds": ["alert-manager", "viewer"]}
|
||||
JSON
|
||||
```
|
||||
@@ -158,9 +163,7 @@ JSON
|
||||
To clear a user's entire role assignment, including built-in roles, send an empty list:
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--header 'Content-Type: application/json' --request PUT \
|
||||
--data-binary @- http://127.0.0.1:7655/api/admin/users/jane/roles <<'JSON'
|
||||
pulse_api PUT /api/admin/users/jane/roles <<'JSON'
|
||||
{"roleIds": []}
|
||||
JSON
|
||||
```
|
||||
@@ -170,8 +173,7 @@ Note: Users cannot modify their own role assignments (self-escalation prevention
|
||||
### Removing User Access
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--request DELETE http://127.0.0.1:7655/api/admin/users/jane
|
||||
pulse_api DELETE /api/admin/users/jane
|
||||
```
|
||||
|
||||
This removes the Pulse identity and all role assignments and revokes its active
|
||||
|
||||
@@ -58,7 +58,7 @@ pulse_api() (
|
||||
set -eu
|
||||
umask 077
|
||||
if [ "$#" -ne 2 ]; then
|
||||
printf 'Usage: pulse_api GET|POST /api/path\n' >&2
|
||||
printf 'Usage: pulse_api GET|POST|PUT|DELETE /api/path\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
method=$1
|
||||
@@ -70,7 +70,9 @@ pulse_api() (
|
||||
case "$method" in
|
||||
GET) set -- ;;
|
||||
POST) set -- --header 'Content-Type: application/json' --request POST --data-binary @- ;;
|
||||
*) printf 'This example helper accepts only GET and POST.\n' >&2; exit 2 ;;
|
||||
PUT) set -- --header 'Content-Type: application/json' --request PUT --data-binary @- ;;
|
||||
DELETE) set -- --request DELETE ;;
|
||||
*) printf 'This example helper accepts only GET, POST, PUT and DELETE.\n' >&2; exit 2 ;;
|
||||
esac
|
||||
auth_dir="$HOME/.config/pulse"
|
||||
auth_file="$auth_dir/api-header"
|
||||
@@ -118,6 +120,13 @@ No request is retried automatically, especially a POST whose outcome is
|
||||
uncertain: inspect the existing action and target before deciding what to do.
|
||||
A 2xx response is only HTTP success, not proof that an action completed.
|
||||
|
||||
The helper also supports the existing [RBAC administration examples](RBAC.md#managing-roles).
|
||||
POST and PUT read their JSON body from standard input; GET and DELETE send no
|
||||
body. A method being supported is not permission to change a resource. Choose
|
||||
each administrative change separately and check its current state afterwards.
|
||||
A timeout or incomplete response can occur **after a change was applied**;
|
||||
do not repeat a POST, PUT or DELETE merely because the response was lost.
|
||||
|
||||
Keep `--disable` first: it ignores local curl configuration that could enable
|
||||
trace output, bypass TLS or follow redirects. On an authentication-enabled
|
||||
instance, the protected summary checks token access; `/api/health` is public
|
||||
|
||||
@@ -55,15 +55,29 @@ full-access (`*`) API token bound to an authorised administrator; a monitoring
|
||||
token is not enough. Do not widen an agent's token for this job.
|
||||
|
||||
If you already use an administration token, prepare its private header file
|
||||
as described in [API authentication](API.md#-authentication). The examples read
|
||||
`$HOME/.config/pulse/api-header`; never paste the token or a session cookie into
|
||||
a command. Keep this file outside shared repositories and diagnostics.
|
||||
and define `pulse_api` **in the same Bash session** using
|
||||
[API authentication](API.md#-authentication). The helper is local example code,
|
||||
not an installed Pulse command. Never paste the token or a session cookie into
|
||||
a command, URL or report, or use browser **Copy as cURL**.
|
||||
|
||||
The loopback URLs work on the Pulse host. For remote use, substitute your
|
||||
Pulse HTTPS URL and keep certificate verification enabled. Use curl 7.76 or
|
||||
later; `--disable` must remain first to ignore local trace/verbose defaults,
|
||||
and `--fail-with-body` makes HTTP failures return a non-zero exit. Run each
|
||||
change separately and check its response before continuing.
|
||||
On the Pulse host, the helper uses loopback; for remote use, follow that guide's
|
||||
HTTPS-origin instructions and keep certificate verification enabled. It makes
|
||||
one request with a five-second connection limit and a twenty-second total limit,
|
||||
prints only the HTTP status and saves each response in a new owner-only file.
|
||||
Inspect the file privately, including on failure: role lists, user identities
|
||||
and errors can contain private information. Share only a relevant redacted
|
||||
excerpt, not full responses, header files or tokens. HTTP 401/402/403, redirects
|
||||
and transport errors are not an empty role or user list.
|
||||
|
||||
**These are separate operations, not a script to run from top to bottom.**
|
||||
Choose the intended target and change before making a request; preserve an
|
||||
existing administrator recovery path. Deleting a role or user and sending an
|
||||
empty role list remove access. After a change, check that target's saved roles
|
||||
and intended access through the normal UI or a separate read-only request.
|
||||
The helper does not follow redirects or retry. A timeout or partial response
|
||||
can occur **after the change was applied**; inspect the current state before
|
||||
deciding what to do, rather than repeating the mutation blindly. HTTP success
|
||||
alone does not establish the intended access or disable an upstream IdP account.
|
||||
|
||||
### Creating a Role
|
||||
|
||||
@@ -74,9 +88,7 @@ Use a new custom role ID. Built-in roles (`admin`, `operator`, `viewer`,
|
||||
|
||||
**API:**
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--header 'Content-Type: application/json' --request POST \
|
||||
--data-binary @- http://127.0.0.1:7655/api/admin/roles <<'JSON'
|
||||
pulse_api POST /api/admin/roles <<'JSON'
|
||||
{
|
||||
"id": "alert-manager",
|
||||
"name": "Alert Manager",
|
||||
@@ -93,16 +105,13 @@ JSON
|
||||
### Listing Roles
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/admin/roles
|
||||
pulse_api GET /api/admin/roles
|
||||
```
|
||||
|
||||
### Updating a Role
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--header 'Content-Type: application/json' --request PUT \
|
||||
--data-binary @- http://127.0.0.1:7655/api/admin/roles/alert-manager <<'JSON'
|
||||
pulse_api PUT /api/admin/roles/alert-manager <<'JSON'
|
||||
{
|
||||
"name": "Alert Manager",
|
||||
"description": "Updated description",
|
||||
@@ -119,8 +128,7 @@ JSON
|
||||
### Deleting a Role
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--request DELETE http://127.0.0.1:7655/api/admin/roles/alert-manager
|
||||
pulse_api DELETE /api/admin/roles/alert-manager
|
||||
```
|
||||
|
||||
---
|
||||
@@ -130,8 +138,7 @@ curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
### Listing Users and Their Roles
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/admin/users
|
||||
pulse_api GET /api/admin/users
|
||||
```
|
||||
|
||||
SSO users are displayed using the latest configured username claim and email
|
||||
@@ -148,9 +155,7 @@ SSO principal). `jane` below is an example local username. Create the custom
|
||||
role before assigning it; do not run the deletion example first.
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--header 'Content-Type: application/json' --request PUT \
|
||||
--data-binary @- http://127.0.0.1:7655/api/admin/users/jane/roles <<'JSON'
|
||||
pulse_api PUT /api/admin/users/jane/roles <<'JSON'
|
||||
{"roleIds": ["alert-manager", "viewer"]}
|
||||
JSON
|
||||
```
|
||||
@@ -158,9 +163,7 @@ JSON
|
||||
To clear a user's entire role assignment, including built-in roles, send an empty list:
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--header 'Content-Type: application/json' --request PUT \
|
||||
--data-binary @- http://127.0.0.1:7655/api/admin/users/jane/roles <<'JSON'
|
||||
pulse_api PUT /api/admin/users/jane/roles <<'JSON'
|
||||
{"roleIds": []}
|
||||
JSON
|
||||
```
|
||||
@@ -170,8 +173,7 @@ Note: Users cannot modify their own role assignments (self-escalation prevention
|
||||
### Removing User Access
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
--request DELETE http://127.0.0.1:7655/api/admin/users/jane
|
||||
pulse_api DELETE /api/admin/users/jane
|
||||
```
|
||||
|
||||
This removes the Pulse identity and all role assignments and revokes its active
|
||||
|
||||
@@ -69,7 +69,7 @@ def recipes(name: str) -> list[str]:
|
||||
def executable_recipe(name: str, step: str) -> str:
|
||||
# Use the actual shared helper, not a test-only safe client. Legacy curl
|
||||
# snippets remain executable so the parent control can expose their leak.
|
||||
if name == "AUDIT_LOGGING" and "pulse_api " in step:
|
||||
if "pulse_api " in step:
|
||||
return request_helper() + "\n" + step
|
||||
return step
|
||||
|
||||
@@ -83,13 +83,13 @@ class AdminDocsTest(unittest.TestCase):
|
||||
for step in steps:
|
||||
self.assertNotRegex(step, r"(?:\b\w*TOKEN=|Authorization:|X-API-Token:|Bearer\s|--cookie\b)")
|
||||
self.assertNotRegex(step, r"(?:--insecure|--verbose|--trace\S*|--location|\s-k\b)")
|
||||
if name == "AUDIT_LOGGING":
|
||||
self.assertIn("pulse_api GET ", step)
|
||||
if name in ("AUDIT_LOGGING", "RBAC"):
|
||||
self.assertIn("pulse_api ", step)
|
||||
self.assertNotIn("curl ", step)
|
||||
else:
|
||||
self.assertIn('curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header"', step)
|
||||
if re.search(r"--request (POST|PUT)", step):
|
||||
self.assertIn("--data-binary @-", step)
|
||||
if re.search(r"(?:--request|pulse_api) (POST|PUT)", step):
|
||||
self.assertIn("--data-binary @-", request_helper() if "pulse_api " in step else step)
|
||||
self.assertIn("<<'JSON'", step)
|
||||
|
||||
def test_guides_explain_authority_and_sensitive_output(self):
|
||||
@@ -154,7 +154,7 @@ class AdminDocsTest(unittest.TestCase):
|
||||
self.assertNotIn(b"Saved private export", result.stdout)
|
||||
# curl preserves an error body, in stdout or the requested file.
|
||||
exports = list(Path(temporary).glob("*/audit-export.json"))
|
||||
if name == "AUDIT_LOGGING":
|
||||
if name in ("AUDIT_LOGGING", "RBAC"):
|
||||
responses = list((Path(temporary) / ".config/pulse").glob("api-response.*"))
|
||||
self.assertEqual(len(responses), 1)
|
||||
output = responses[0].read_bytes()
|
||||
@@ -205,6 +205,56 @@ class AdminDocsTest(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, expected_exit, result.stderr.decode())
|
||||
self.assertEqual(len(requests), before + 1, "must not follow redirects or retry partial reads")
|
||||
|
||||
def test_rbac_guidance_preserves_private_and_uncertain_change_boundaries(self):
|
||||
guide = (DOCS / "RBAC.md").read_text()
|
||||
for boundary in ("in the same Bash session", "not an installed Pulse command",
|
||||
"owner-only file", "redacted", "five-second", "twenty-second",
|
||||
"not a script to run from top to bottom", "administrator recovery path",
|
||||
"empty role list remove access", "does not follow redirects or retry",
|
||||
"after the change was applied", "inspect the current state",
|
||||
"HTTP success", "upstream IdP account"):
|
||||
self.assertIn(boundary, guide)
|
||||
|
||||
def test_rbac_operations_keep_private_success_bodies_in_new_owner_only_files(self):
|
||||
steps = recipes("RBAC")
|
||||
self.assertEqual(len(steps), len(EXPECTED["RBAC"]))
|
||||
with private_recording_server() as (port, requests), tempfile.TemporaryDirectory(prefix="rbac docs ") as temporary:
|
||||
home = Path(temporary)
|
||||
for header in (f"X-API-Token: {TEST_TOKEN}", f"Authorization: Bearer {TEST_TOKEN}"):
|
||||
for step, (method, path, _) in zip(steps, EXPECTED["RBAC"]):
|
||||
with self.subTest(method=method, path=path, header=header.split(":")[0]):
|
||||
before = len(requests)
|
||||
result = exercise_curl(self, home, header, port,
|
||||
executable_recipe("RBAC", step), private_response=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr.decode())
|
||||
self.assertEqual(len(requests), before + 1)
|
||||
self.assertEqual((requests[-1][2], requests[-1][0]), (method, path))
|
||||
self.assertIn(b"HTTP 200", result.stdout)
|
||||
self.assertEqual(len(list((home / ".config/pulse").glob("api-response.*"))), 17)
|
||||
|
||||
def test_rbac_operations_keep_private_http_errors_and_fail_without_retry(self):
|
||||
for status in (400, 401, 402, 403, 500, 503):
|
||||
with private_recording_server(status) as (port, requests):
|
||||
for step in recipes("RBAC"):
|
||||
with self.subTest(status=status, step=step), tempfile.TemporaryDirectory() as temporary:
|
||||
before = len(requests)
|
||||
result = exercise_curl(self, Path(temporary), f"X-API-Token: {TEST_TOKEN}", port,
|
||||
executable_recipe("RBAC", step), private_response=True)
|
||||
self.assertEqual(result.returncode, 22, result.stderr.decode())
|
||||
self.assertEqual(len(requests), before + 1)
|
||||
self.assertIn(f"HTTP {status}".encode(), result.stdout)
|
||||
|
||||
def test_rbac_operations_refuse_redirect_success_and_uncertain_transfer_retry(self):
|
||||
for status, partial, expected_exit in ((302, False, 1), (200, True, 18)):
|
||||
with private_recording_server(status, partial) as (port, requests):
|
||||
for step in recipes("RBAC"):
|
||||
with self.subTest(status=status, partial=partial, step=step), tempfile.TemporaryDirectory() as temporary:
|
||||
before = len(requests)
|
||||
result = exercise_curl(self, Path(temporary), f"X-API-Token: {TEST_TOKEN}", port,
|
||||
executable_recipe("RBAC", step), private_response=True)
|
||||
self.assertEqual(result.returncode, expected_exit, result.stderr.decode())
|
||||
self.assertEqual(len(requests), before + 1, "an uncertain mutation must not be repeated or redirected")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -366,6 +366,39 @@ class APIAuthDocsTest(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, exitcode, result.stderr.decode())
|
||||
self.assertEqual(len(requests), 1, "uncertain POST must not be repeated or redirected")
|
||||
|
||||
def test_shared_helper_preserves_method_and_body_for_existing_admin_operations(self):
|
||||
with private_recording_server() as (port, requests), tempfile.TemporaryDirectory() as temporary:
|
||||
for method in ("GET", "POST", "PUT", "DELETE"):
|
||||
with self.subTest(method=method):
|
||||
call = f"pulse_api {method} /api/admin/roles <<'JSON'\n{{\"fixture\":true}}\nJSON\n"
|
||||
result = self.run_documented_request(Path(temporary), "X-API-Token: " + TEST_TOKEN,
|
||||
port, request_helper() + "\n" + call)
|
||||
self.assertEqual(result.returncode, 0, result.stderr.decode())
|
||||
path, headers, sent_method, body = requests[-1]
|
||||
self.assertEqual((sent_method, path), (method, "/api/admin/roles"))
|
||||
if method in ("POST", "PUT"):
|
||||
self.assertEqual(headers["Content-Type"], "application/json")
|
||||
self.assertEqual(json.loads(body), {"fixture": True})
|
||||
else:
|
||||
self.assertEqual(body, b"")
|
||||
self.assertNotIn("Content-Type", headers)
|
||||
self.assertEqual(len(requests), 4)
|
||||
|
||||
def test_shared_helper_refuses_unsupported_methods_urls_and_missing_auth_before_transport(self):
|
||||
with recording_server() as (port, requests), tempfile.TemporaryDirectory() as temporary:
|
||||
env = dict(os.environ, HOME=temporary)
|
||||
for call, expected_exit in (("pulse_api PATCH /api/admin/roles", 2),
|
||||
("pulse_api GET https://example.invalid/api/admin/roles", 2),
|
||||
("pulse_api DELETE", 2),
|
||||
("pulse_api DELETE /api/admin/roles/unused", 1)):
|
||||
with self.subTest(call=call):
|
||||
command = (request_helper() + "\n" + call).replace("http://127.0.0.1:7655", f"http://127.0.0.1:{port}")
|
||||
result = subprocess.run(["bash", "-eu", "-c", command], env=env,
|
||||
capture_output=True, timeout=10)
|
||||
self.assertEqual(result.returncode, expected_exit, result.stderr.decode())
|
||||
self.assertEqual(requests, [])
|
||||
self.assertFalse((Path(temporary) / ".config").exists())
|
||||
|
||||
def test_private_response_assertion_rejects_console_output_regression(self):
|
||||
request = commands()[1]
|
||||
self.assertEqual(request.count('--output "$result_file" '), 1)
|
||||
|
||||
Reference in New Issue
Block a user