mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 21:20:00 +00:00
Keep node configuration API recipes scoped, private and trust-aware
Replace root-password and implicit-TLS examples with a private JSON-file recipe using the existing bounded helper. Distinguish live tests, persisted changes, platform credentials and uncertain results; exercise copied commands with synthetic success, denial, redirect and incomplete responses. Contract-Neutral: Documentation, mirrored guidance and copied-recipe regression tests only; no runtime API, authentication, TLS or frontend contract changes. Change-source: pulse-maintainer
This commit is contained in:
+99
-16
@@ -634,39 +634,122 @@ Version fields are returned as plain semantic versions (no leading `v`).
|
||||
Returns a small public config payload (update channel, auto-update enabled).
|
||||
|
||||
### List Nodes
|
||||
`GET /api/config/nodes`
|
||||
`GET /api/config/nodes` (admin, `settings:read`)
|
||||
|
||||
This administrative inventory covers saved **PVE, PBS and PMG connections**,
|
||||
not individual VMs or agents. Keep its response private. Use the returned
|
||||
connection `id` for updates, deletion and tests; a display name, VMID or another
|
||||
installation's node name is not that ID. Re-read the inventory before a change
|
||||
rather than guessing an ID from a list position.
|
||||
|
||||
### Add Node
|
||||
`POST /api/config/nodes`
|
||||
`POST /api/config/nodes` (admin, `settings:write`)
|
||||
|
||||
Adding a connection saves credentials and enables collection; it is not a
|
||||
passive validation step. Use an existing dedicated, least-privilege platform
|
||||
token. The **Pulse API token** in the private header file authorises this
|
||||
configuration request; the **Proxmox token** in the JSON body authorises reads
|
||||
from the monitored platform. They are different credentials. Do not use a root
|
||||
password or widen a department-scoped token to make denied peer reads succeed;
|
||||
follow the [scoped-access checks](TROUBLESHOOTING.md#check-permissions-proxmox).
|
||||
For PBS and PMG, use their [PBS](PBS.md#method-1-api-only-connection-recommended)
|
||||
and [PMG](MAIL_GATEWAY.md) token guidance. PBS password-based onboarding can
|
||||
also try to create a token and ACLs on the PBS server; it is not read-only.
|
||||
|
||||
Prepare the request in a private editor, not a credential-bearing shell
|
||||
assignment, here-document or command argument. Use the same Bash session as
|
||||
the [private-response helper](#api-token-recommended):
|
||||
|
||||
```bash
|
||||
node_request_dir=$(umask 077; mktemp -d "${TMPDIR:-/tmp}/pulse-node.XXXXXX") &&
|
||||
node_request_file="$node_request_dir/node.json" &&
|
||||
(umask 077; set -C; : > "$node_request_file") &&
|
||||
vi "$node_request_file"
|
||||
```
|
||||
|
||||
If preparation or the editor fails, stop. In that editor, enter the following
|
||||
JSON, replacing the example hostname, connection name, full platform token ID
|
||||
(`user@realm!token`) and token-secret placeholder with your intended values:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "pve",
|
||||
"name": "Proxmox 1",
|
||||
"host": "https://198.51.100.10:8006",
|
||||
"user": "root@pam",
|
||||
"password": "password"
|
||||
"name": "PVE connection A",
|
||||
"host": "https://pve.example.net:8006",
|
||||
"tokenName": "pulse-monitor@pve!pulse-readonly",
|
||||
"tokenValue": "<platform-token-secret>",
|
||||
"verifySSL": true
|
||||
}
|
||||
```
|
||||
|
||||
### Test Connection
|
||||
`POST /api/config/nodes/test-connection`
|
||||
Validate credentials before saving.
|
||||
Set `verifySSL` explicitly to `true`: omission on these new-connection and
|
||||
test request paths does **not** enable certificate verification. The example
|
||||
requires a certificate trusted by the Pulse server for that hostname. For a
|
||||
self-signed/private certificate, also supply `fingerprint` with the SHA-256
|
||||
certificate fingerprint verified through an independent trusted channel.
|
||||
A fingerprint merely fetched from the endpoint is not established trust.
|
||||
Do not clear an existing pin, set `verifySSL` to `false`, or change to HTTP to
|
||||
work around a certificate failure.
|
||||
|
||||
### Test Node Config (Validation Only)
|
||||
`POST /api/config/nodes/test-config`
|
||||
Validates node config without saving.
|
||||
After reviewing this one intended configuration change, send the file:
|
||||
|
||||
```bash
|
||||
pulse_api POST /api/config/nodes < "$node_request_file"
|
||||
```
|
||||
|
||||
The helper retains the response privately and makes one bounded request,
|
||||
without retries. A lost response can follow a successful save: re-read the
|
||||
saved inventory before deciding whether another request is needed. HTTP success
|
||||
does not establish fresh readings for every node, guest, backup or History view.
|
||||
Keep both request and response files out of reports and shared repositories;
|
||||
remove the temporary request under your credential policy when no longer needed.
|
||||
|
||||
### Test Connection
|
||||
`POST /api/config/nodes/test-connection` (admin, `settings:write`)
|
||||
|
||||
Tests supplied credentials with live platform requests without saving the
|
||||
connection. Use the same private JSON-file transport and explicit TLS settings,
|
||||
only when a live connection test is appropriate; it is not a required precursor
|
||||
to filing a report. Success covers the reads performed by that test, not all
|
||||
collection permissions, guest recovery or sustained monitoring.
|
||||
|
||||
### Test Node Config (No Save, Live Request)
|
||||
`POST /api/config/nodes/test-config` (admin, `settings:write`)
|
||||
|
||||
This also contacts the platform using the supplied credentials; **no save**
|
||||
does not mean offline schema validation. Do not run either test merely to
|
||||
collect new evidence during a freeze/thaw, backup or unresponsive-host incident.
|
||||
Use existing observations and the [safe diagnostic guidance](TROUBLESHOOTING.md#collect-diagnostics-safely).
|
||||
|
||||
### Update Node
|
||||
`PUT /api/config/nodes/{id}`
|
||||
`PUT /api/config/nodes/{id}` (admin, `settings:write`)
|
||||
|
||||
Review the current connection and submit only the intended supported changes
|
||||
from a private JSON file. Do not copy masked credentials from a GET response
|
||||
back as new secrets. Credential rotation and trust changes are separate
|
||||
decisions; preserve the existing credential and fingerprint when not changing
|
||||
them. A response failure does not prove the update was unapplied.
|
||||
|
||||
### Delete Node
|
||||
`DELETE /api/config/nodes/{id}`
|
||||
`DELETE /api/config/nodes/{id}` (admin, `settings:write`)
|
||||
|
||||
Removes the Pulse connection, not the platform workloads or the upstream token.
|
||||
Collection and related alert coverage can stop. Check the exact connection and
|
||||
arrange any needed independent coverage before an intentional removal; do not
|
||||
delete and re-add a connection to diagnose missing readings or identity mixing.
|
||||
|
||||
### Test Node (Legacy)
|
||||
`POST /api/config/nodes/{id}/test`
|
||||
`POST /api/config/nodes/{id}/test` (admin, `settings:write`)
|
||||
|
||||
Uses the saved connection for a live test. It is not a passive read of its
|
||||
last result or a guarantee that ordinary collection recovered.
|
||||
|
||||
### Refresh Cluster Nodes
|
||||
`POST /api/config/nodes/{id}/refresh-cluster`
|
||||
`POST /api/config/nodes/{id}/refresh-cluster` (admin, `settings:write`)
|
||||
|
||||
Actively rediscovers a saved PVE connection's cluster membership. It does not
|
||||
select authorised nodes, grant missing permissions or repair an unrelated
|
||||
standalone installation. Keep intentional token restrictions intact.
|
||||
|
||||
### Export Configuration
|
||||
`POST /api/config/export` (instance admin for the default organization, tenant
|
||||
|
||||
@@ -634,39 +634,122 @@ Version fields are returned as plain semantic versions (no leading `v`).
|
||||
Returns a small public config payload (update channel, auto-update enabled).
|
||||
|
||||
### List Nodes
|
||||
`GET /api/config/nodes`
|
||||
`GET /api/config/nodes` (admin, `settings:read`)
|
||||
|
||||
This administrative inventory covers saved **PVE, PBS and PMG connections**,
|
||||
not individual VMs or agents. Keep its response private. Use the returned
|
||||
connection `id` for updates, deletion and tests; a display name, VMID or another
|
||||
installation's node name is not that ID. Re-read the inventory before a change
|
||||
rather than guessing an ID from a list position.
|
||||
|
||||
### Add Node
|
||||
`POST /api/config/nodes`
|
||||
`POST /api/config/nodes` (admin, `settings:write`)
|
||||
|
||||
Adding a connection saves credentials and enables collection; it is not a
|
||||
passive validation step. Use an existing dedicated, least-privilege platform
|
||||
token. The **Pulse API token** in the private header file authorises this
|
||||
configuration request; the **Proxmox token** in the JSON body authorises reads
|
||||
from the monitored platform. They are different credentials. Do not use a root
|
||||
password or widen a department-scoped token to make denied peer reads succeed;
|
||||
follow the [scoped-access checks](TROUBLESHOOTING.md#check-permissions-proxmox).
|
||||
For PBS and PMG, use their [PBS](PBS.md#method-1-api-only-connection-recommended)
|
||||
and [PMG](MAIL_GATEWAY.md) token guidance. PBS password-based onboarding can
|
||||
also try to create a token and ACLs on the PBS server; it is not read-only.
|
||||
|
||||
Prepare the request in a private editor, not a credential-bearing shell
|
||||
assignment, here-document or command argument. Use the same Bash session as
|
||||
the [private-response helper](#api-token-recommended):
|
||||
|
||||
```bash
|
||||
node_request_dir=$(umask 077; mktemp -d "${TMPDIR:-/tmp}/pulse-node.XXXXXX") &&
|
||||
node_request_file="$node_request_dir/node.json" &&
|
||||
(umask 077; set -C; : > "$node_request_file") &&
|
||||
vi "$node_request_file"
|
||||
```
|
||||
|
||||
If preparation or the editor fails, stop. In that editor, enter the following
|
||||
JSON, replacing the example hostname, connection name, full platform token ID
|
||||
(`user@realm!token`) and token-secret placeholder with your intended values:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "pve",
|
||||
"name": "Proxmox 1",
|
||||
"host": "https://198.51.100.10:8006",
|
||||
"user": "root@pam",
|
||||
"password": "password"
|
||||
"name": "PVE connection A",
|
||||
"host": "https://pve.example.net:8006",
|
||||
"tokenName": "pulse-monitor@pve!pulse-readonly",
|
||||
"tokenValue": "<platform-token-secret>",
|
||||
"verifySSL": true
|
||||
}
|
||||
```
|
||||
|
||||
### Test Connection
|
||||
`POST /api/config/nodes/test-connection`
|
||||
Validate credentials before saving.
|
||||
Set `verifySSL` explicitly to `true`: omission on these new-connection and
|
||||
test request paths does **not** enable certificate verification. The example
|
||||
requires a certificate trusted by the Pulse server for that hostname. For a
|
||||
self-signed/private certificate, also supply `fingerprint` with the SHA-256
|
||||
certificate fingerprint verified through an independent trusted channel.
|
||||
A fingerprint merely fetched from the endpoint is not established trust.
|
||||
Do not clear an existing pin, set `verifySSL` to `false`, or change to HTTP to
|
||||
work around a certificate failure.
|
||||
|
||||
### Test Node Config (Validation Only)
|
||||
`POST /api/config/nodes/test-config`
|
||||
Validates node config without saving.
|
||||
After reviewing this one intended configuration change, send the file:
|
||||
|
||||
```bash
|
||||
pulse_api POST /api/config/nodes < "$node_request_file"
|
||||
```
|
||||
|
||||
The helper retains the response privately and makes one bounded request,
|
||||
without retries. A lost response can follow a successful save: re-read the
|
||||
saved inventory before deciding whether another request is needed. HTTP success
|
||||
does not establish fresh readings for every node, guest, backup or History view.
|
||||
Keep both request and response files out of reports and shared repositories;
|
||||
remove the temporary request under your credential policy when no longer needed.
|
||||
|
||||
### Test Connection
|
||||
`POST /api/config/nodes/test-connection` (admin, `settings:write`)
|
||||
|
||||
Tests supplied credentials with live platform requests without saving the
|
||||
connection. Use the same private JSON-file transport and explicit TLS settings,
|
||||
only when a live connection test is appropriate; it is not a required precursor
|
||||
to filing a report. Success covers the reads performed by that test, not all
|
||||
collection permissions, guest recovery or sustained monitoring.
|
||||
|
||||
### Test Node Config (No Save, Live Request)
|
||||
`POST /api/config/nodes/test-config` (admin, `settings:write`)
|
||||
|
||||
This also contacts the platform using the supplied credentials; **no save**
|
||||
does not mean offline schema validation. Do not run either test merely to
|
||||
collect new evidence during a freeze/thaw, backup or unresponsive-host incident.
|
||||
Use existing observations and the [safe diagnostic guidance](TROUBLESHOOTING.md#collect-diagnostics-safely).
|
||||
|
||||
### Update Node
|
||||
`PUT /api/config/nodes/{id}`
|
||||
`PUT /api/config/nodes/{id}` (admin, `settings:write`)
|
||||
|
||||
Review the current connection and submit only the intended supported changes
|
||||
from a private JSON file. Do not copy masked credentials from a GET response
|
||||
back as new secrets. Credential rotation and trust changes are separate
|
||||
decisions; preserve the existing credential and fingerprint when not changing
|
||||
them. A response failure does not prove the update was unapplied.
|
||||
|
||||
### Delete Node
|
||||
`DELETE /api/config/nodes/{id}`
|
||||
`DELETE /api/config/nodes/{id}` (admin, `settings:write`)
|
||||
|
||||
Removes the Pulse connection, not the platform workloads or the upstream token.
|
||||
Collection and related alert coverage can stop. Check the exact connection and
|
||||
arrange any needed independent coverage before an intentional removal; do not
|
||||
delete and re-add a connection to diagnose missing readings or identity mixing.
|
||||
|
||||
### Test Node (Legacy)
|
||||
`POST /api/config/nodes/{id}/test`
|
||||
`POST /api/config/nodes/{id}/test` (admin, `settings:write`)
|
||||
|
||||
Uses the saved connection for a live test. It is not a passive read of its
|
||||
last result or a guarantee that ordinary collection recovered.
|
||||
|
||||
### Refresh Cluster Nodes
|
||||
`POST /api/config/nodes/{id}/refresh-cluster`
|
||||
`POST /api/config/nodes/{id}/refresh-cluster` (admin, `settings:write`)
|
||||
|
||||
Actively rediscovers a saved PVE connection's cluster membership. It does not
|
||||
select authorised nodes, grant missing permissions or repair an unrelated
|
||||
standalone installation. Keep intentional token restrictions intact.
|
||||
|
||||
### Export Configuration
|
||||
`POST /api/config/export` (instance admin for the default organization, tenant
|
||||
|
||||
@@ -203,14 +203,17 @@ class APIAuthDocsTest(unittest.TestCase):
|
||||
self.assertNotRegex(bash, r"(?:--insecure|--verbose|--trace\S*|--location|\s-k\b)")
|
||||
blocks = re.findall(r"```bash\n(.*?)```", DOC.read_text(), re.DOTALL)
|
||||
requests = [block for block in blocks if block.startswith("pulse_api ")]
|
||||
self.assertEqual(len(requests), 8)
|
||||
self.assertEqual(len(requests), 9)
|
||||
self.assertEqual([block for block in blocks if "curl " in block], [request_helper()],
|
||||
"an example must not bypass the private-response helper")
|
||||
self.assertIn("curl --disable --fail-with-body ", request_helper())
|
||||
for request in requests:
|
||||
if request.startswith("pulse_api POST "):
|
||||
self.assertIn("--data-binary @-", request_helper())
|
||||
self.assertIn("<<'JSON'", request)
|
||||
if request.startswith("pulse_api POST /api/config/nodes "):
|
||||
self.assertEqual(request.strip(), 'pulse_api POST /api/config/nodes < "$node_request_file"')
|
||||
else:
|
||||
self.assertIn("<<'JSON'", request)
|
||||
|
||||
def test_every_documented_shell_recipe_parses(self):
|
||||
for block in re.findall(r"```bash\n(.*?)```", DOC.read_text(), re.DOTALL):
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise the existing node API guide with synthetic private credentials.
|
||||
|
||||
All HTTP is guest-local loopback, not a platform or production probe.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import test_api_auth_docs as auth
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DOC = ROOT / "docs/API.md"
|
||||
PLATFORM_SECRET = "synthetic-platform-token-secret"
|
||||
|
||||
|
||||
def section() -> str:
|
||||
return DOC.read_text().split("## 🖥️ Nodes & Config\n", 1)[1].split("\n## ", 1)[0]
|
||||
|
||||
|
||||
def preparation() -> str:
|
||||
blocks = re.findall(r"```bash\n(.*?)```", section(), re.DOTALL)
|
||||
return next(block for block in blocks if 'vi "$node_request_file"' in block)
|
||||
|
||||
|
||||
def sample() -> dict:
|
||||
blocks = re.findall(r"```json\n(.*?)```", section(), re.DOTALL)
|
||||
if len(blocks) != 1:
|
||||
raise AssertionError("expected one node configuration specimen")
|
||||
return json.loads(blocks[0])
|
||||
|
||||
|
||||
def request() -> str:
|
||||
blocks = re.findall(r"```bash\n(.*?)```", section(), re.DOTALL)
|
||||
return next(block for block in blocks if block.startswith("pulse_api POST "))
|
||||
|
||||
|
||||
class NodeAPIDocsTest(unittest.TestCase):
|
||||
def test_specimen_uses_scoped_token_and_explicit_tls(self):
|
||||
payload = sample()
|
||||
self.assertEqual(payload["type"], "pve")
|
||||
self.assertTrue(payload["host"].startswith("https://"))
|
||||
self.assertNotIn("user", payload)
|
||||
self.assertNotIn("password", payload)
|
||||
self.assertNotIn("root@pam", json.dumps(payload))
|
||||
self.assertEqual(payload["tokenName"], "pulse-monitor@pve!pulse-readonly")
|
||||
self.assertEqual(payload["tokenValue"], "<platform-token-secret>")
|
||||
self.assertIs(payload["verifySSL"], True)
|
||||
schema = (ROOT / "internal/api/configapi/config_handlers.go").read_text()
|
||||
schema = schema.split("type NodeConfigRequest struct {", 1)[1].split("\n}", 1)[0]
|
||||
fields = set(re.findall(r'json:"([^",]+)', schema))
|
||||
self.assertLessEqual(set(payload), fields, "the copied fields must be accepted by this request type")
|
||||
|
||||
def test_scopes_live_tests_and_connection_identity_are_explicit(self):
|
||||
text = section()
|
||||
routes = ("", "/test-connection", "/test-config", "/{id}", "/{id}/test", "/{id}/refresh-cluster")
|
||||
for route in routes:
|
||||
method = "PUT" if route == "/{id}" else "POST"
|
||||
self.assertIn(f"`{method} /api/config/nodes{route}` (admin, `settings:write`)", text)
|
||||
self.assertIn("`GET /api/config/nodes` (admin, `settings:read`)", text)
|
||||
self.assertIn("`DELETE /api/config/nodes/{id}` (admin, `settings:write`)", text)
|
||||
for boundary in ("not individual VMs or agents", "returned\nconnection `id`",
|
||||
"no save", "live platform requests", "not a passive read",
|
||||
"freeze/thaw", "not all\ncollection permissions",
|
||||
"does not\nselect authorised nodes"):
|
||||
self.assertIn(boundary, text)
|
||||
self.assertNotIn("Validation Only", text)
|
||||
|
||||
def test_credentials_trust_and_uncertain_changes_are_separate(self):
|
||||
text = section()
|
||||
for boundary in ("Pulse API token", "Proxmox token", "different credentials",
|
||||
"does **not** enable certificate verification",
|
||||
"independent trusted channel", "not established trust",
|
||||
"Do not clear an existing pin", "can\nalso try to create a token and ACLs",
|
||||
"without retries", "lost response", "re-read the\nsaved inventory",
|
||||
"masked credentials", "not the platform workloads or the upstream token",
|
||||
"Collection and related alert coverage can stop",
|
||||
"reports and shared repositories"):
|
||||
self.assertIn(boundary, text)
|
||||
self.assertNotRegex("\n".join(re.findall(r"```bash\n(.*?)```", text, re.DOTALL)),
|
||||
r"root@pam|tokenValue|password|--insecure|--location|--retry")
|
||||
|
||||
def test_copied_preparation_is_private_unique_and_stops_on_failure(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
home = Path(tmp)
|
||||
tools = home / "tools"
|
||||
tools.mkdir()
|
||||
calls = home / "editor-calls.jsonl"
|
||||
editor = tools / "vi"
|
||||
editor.write_text(
|
||||
"#!/usr/bin/env python3\nimport json, os, sys\nfrom pathlib import Path\n"
|
||||
"with Path(os.environ['EDITOR_CALLS']).open('a') as f: f.write(json.dumps(sys.argv[1:]) + '\\n')\n"
|
||||
"Path(sys.argv[1]).write_text('{\"fixture\": true}')\n"
|
||||
)
|
||||
editor.chmod(0o700)
|
||||
env = dict(os.environ, HOME=str(home), TMPDIR=str(home),
|
||||
PATH=f"{tools}:{os.environ['PATH']}", EDITOR_CALLS=str(calls))
|
||||
created = []
|
||||
for _ in range(2):
|
||||
result = subprocess.run(["bash", "-eu", "-c", preparation()], env=env,
|
||||
capture_output=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout, b"", "preparation must not print credentials")
|
||||
argv = json.loads(calls.read_text().splitlines()[-1])
|
||||
self.assertEqual(len(argv), 1)
|
||||
path = Path(argv[0])
|
||||
self.assertEqual(path.name, "node.json")
|
||||
self.assertEqual(path.read_text(), '{"fixture": true}')
|
||||
self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o600)
|
||||
self.assertEqual(stat.S_IMODE(path.parent.stat().st_mode), 0o700)
|
||||
created.append(path)
|
||||
self.assertNotEqual(*created, "a new preparation must not overwrite the previous request")
|
||||
before = calls.read_bytes()
|
||||
failed = subprocess.run(["bash", "-eu", "-c", preparation()],
|
||||
env=dict(env, TMPDIR=str(home / "missing")),
|
||||
capture_output=True, timeout=10)
|
||||
self.assertNotEqual(failed.returncode, 0)
|
||||
self.assertEqual(calls.read_bytes(), before, "failed preparation must not open an editor")
|
||||
self.assertEqual(created[0].read_text(), '{"fixture": true}')
|
||||
|
||||
def test_copied_file_dispatch_is_private_bounded_and_never_retried(self):
|
||||
payload = sample()
|
||||
payload["tokenValue"] = PLATFORM_SECRET
|
||||
# Independent operation expectation: the fixture must not accept a
|
||||
# different route, method, credential or body just because it was copied.
|
||||
self.assertEqual(request().strip(), 'pulse_api POST /api/config/nodes < "$node_request_file"')
|
||||
for status, partial in ((201, False), (401, False), (403, False),
|
||||
(302, False), (500, False), (200, True)):
|
||||
with self.subTest(status=status, partial=partial), tempfile.TemporaryDirectory() as tmp:
|
||||
home = Path(tmp)
|
||||
body = home / "node.json"
|
||||
body.write_text(json.dumps(payload))
|
||||
body.chmod(0o600)
|
||||
script = f'node_request_file={json.dumps(str(body))}\n' + auth.request_helper() + "\n" + request()
|
||||
with auth.private_recording_server(status, partial) as (port, calls):
|
||||
result = auth.exercise_curl(self, home, "X-API-Token: " + auth.TEST_TOKEN,
|
||||
port, script, private_response=True)
|
||||
self.assertEqual(len(calls), 1)
|
||||
path, headers, method, data = calls[0]
|
||||
self.assertEqual((path, method), ("/api/config/nodes", "POST"))
|
||||
self.assertEqual(headers["X-API-Token"], auth.TEST_TOKEN)
|
||||
self.assertEqual(headers["Content-Type"], "application/json")
|
||||
self.assertEqual(json.loads(data), payload)
|
||||
self.assertEqual(body.read_text(), json.dumps(payload))
|
||||
argv = (home / "argv.json").read_text()
|
||||
self.assertNotIn(PLATFORM_SECRET, argv)
|
||||
self.assertNotIn(PLATFORM_SECRET.encode(), result.stdout + result.stderr)
|
||||
self.assertEqual(result.returncode == 0, status == 201 and not partial)
|
||||
|
||||
def test_missing_request_file_sends_nothing(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
home = Path(tmp)
|
||||
with auth.recording_server() as (port, calls):
|
||||
script = f'node_request_file={json.dumps(str(home / "absent.json"))}\n'
|
||||
script += auth.request_helper().replace("http://127.0.0.1:7655", f"http://127.0.0.1:{port}")
|
||||
script += "\n" + request()
|
||||
result = subprocess.run(["bash", "-eu", "-c", script], capture_output=True, timeout=10)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(calls, [], "missing private input must not fall back to another request body")
|
||||
|
||||
def test_all_documented_node_shell_parses(self):
|
||||
for block in re.findall(r"```bash\n(.*?)```", section(), re.DOTALL):
|
||||
subprocess.run(["bash", "-n", "-c", block], check=True, capture_output=True)
|
||||
|
||||
def test_private_response_helper_is_preserved(self):
|
||||
helper = auth.request_helper()
|
||||
self.assertIn('curl --disable --fail-with-body --silent --show-error', helper)
|
||||
self.assertIn('--header "@$auth_file"', helper)
|
||||
self.assertIn('--data-binary @-', helper)
|
||||
self.assertIn('--output "$result_file"', helper)
|
||||
self.assertNotIn('--retry', helper)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user