mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Keep copied API requests private, bounded and one-shot
Save every existing reference response privately, including errors and partial POST results. Preserve private header files, reject unsafe preparation and URLs, and stop on redirects or transport errors without retries. Exercise all eight copied GET/POST examples against real synthetic HTTP/TLS peers, including the old response exposure. Contract-Neutral: Harden existing API documentation recipes and their shipped mirror with connected tests; no runtime, endpoint, authentication, scope, action, dependency or permission contract changes. Change-source: pulse-maintainer
This commit is contained in:
+91
-33
@@ -17,7 +17,8 @@ secret does not appear in shell history or process arguments.
|
||||
|
||||
On a trusted machine, as the account running curl, prepare the file and open
|
||||
it in an editor. This preserves an existing file, restricts its directory and
|
||||
stops before editing if preparation fails or a credential path is a symlink:
|
||||
stops before editing if preparation fails or a credential path is symlinked or
|
||||
not a regular file:
|
||||
|
||||
```bash
|
||||
(
|
||||
@@ -29,6 +30,10 @@ stops before editing if preparation fails or a credential path is a symlink:
|
||||
printf 'Refusing a symlinked credential path.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -e "$auth_file" ] && [ ! -f "$auth_file" ]; then
|
||||
printf 'Credential file must be a regular file.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$auth_dir"
|
||||
chmod 700 "$auth_dir"
|
||||
touch "$auth_file"
|
||||
@@ -43,20 +48,82 @@ In the editor, save just this line, replacing `<token>` with the API token:
|
||||
X-API-Token: <token>
|
||||
```
|
||||
|
||||
Then make a read-only request (curl 7.76 or later):
|
||||
Define this helper in the same Bash session (curl 7.76 or later). The examples
|
||||
on this page use it to make **one request**, print only the HTTP status and save
|
||||
each response to a new owner-only file. This keeps resource names, audit details
|
||||
and credential-bearing error bodies out of terminal output and recordings.
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/state/summary
|
||||
pulse_api() (
|
||||
set -eu
|
||||
umask 077
|
||||
if [ "$#" -ne 2 ]; then
|
||||
printf 'Usage: pulse_api GET|POST /api/path\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
method=$1
|
||||
api_path=$2
|
||||
case "$api_path" in
|
||||
/api/*) ;;
|
||||
*) printf 'Use an API path on this Pulse instance, not a full URL.\n' >&2; exit 2 ;;
|
||||
esac
|
||||
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 ;;
|
||||
esac
|
||||
auth_dir="$HOME/.config/pulse"
|
||||
auth_file="$auth_dir/api-header"
|
||||
if [ -L "$HOME/.config" ] || [ -L "$auth_dir" ] || [ -L "$auth_file" ] || [ ! -f "$auth_file" ]; then
|
||||
printf 'Prepare a regular, private header file first.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod 700 "$auth_dir" || exit "$?"
|
||||
chmod 600 "$auth_file" || exit "$?"
|
||||
result_file=$(mktemp "$auth_dir/api-response.XXXXXX") || exit "$?"
|
||||
curl_exit=0
|
||||
status=$(curl --disable --fail-with-body --silent --show-error \
|
||||
--proto '=http' --noproxy 127.0.0.1 --connect-timeout 5 --max-time 20 \
|
||||
--header "@$auth_file" --output "$result_file" --write-out '%{http_code}' \
|
||||
"$@" "http://127.0.0.1:7655$api_path") || curl_exit=$?
|
||||
printf 'HTTP %s\nPrivate response: %s\n' "$status" "$result_file"
|
||||
[ "$curl_exit" -eq 0 ] || exit "$curl_exit"
|
||||
case "$status" in
|
||||
2??) ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
)
|
||||
```
|
||||
|
||||
This loopback URL applies only when curl runs on the Pulse host. For remote
|
||||
access, use your Pulse HTTPS URL and keep certificate verification enabled;
|
||||
do not use `--insecure`. On an authentication-enabled instance, the protected
|
||||
summary checks token access; `/api/health` is public and does not verify authentication.
|
||||
`--fail-with-body` returns a non-zero exit on HTTP errors, including 401 and
|
||||
403, while retaining the error response. Keep `--disable` first: it ignores
|
||||
local curl configuration that could otherwise enable credential-bearing trace output.
|
||||
Then make a read-only request with a `monitoring:read` token:
|
||||
|
||||
```bash
|
||||
pulse_api GET /api/state/summary
|
||||
```
|
||||
|
||||
The default loopback origin applies only when curl runs on the Pulse host and
|
||||
explicitly bypasses proxy environment settings. For remote access, replace
|
||||
`http://127.0.0.1:7655` **in the helper** with your Pulse HTTPS origin, and replace
|
||||
`--proto '=http' --noproxy 127.0.0.1` with `--proto '=https'`. Keep certificate
|
||||
verification enabled; for a private CA, add `--cacert` with a CA file you
|
||||
verified separately, never `--insecure`. Do not put credentials in the URL.
|
||||
|
||||
Open the reported response file privately to inspect it, including on failure;
|
||||
share only the relevant redacted error, not the whole response or file. Each
|
||||
call preserves earlier responses. HTTP 401 and 403 return curl's non-zero exit
|
||||
and retain the error body; redirects are not followed and are not success.
|
||||
The connection limit is five seconds and the whole request limit is twenty
|
||||
seconds. A partial file after a transport error is not a complete result.
|
||||
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.
|
||||
|
||||
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
|
||||
and does not verify authentication. The helper is local example code, not an
|
||||
installed Pulse command; define it again in a new shell before using these
|
||||
examples. Dispose of response files under your normal private-data policy.
|
||||
|
||||
Do not paste tokens into command lines, URLs or issue reports. Keep the header
|
||||
file private and outside shared repositories and diagnostics; do not use curl
|
||||
@@ -67,7 +134,7 @@ needed in Pulse's API Access settings.
|
||||
### Bearer Token
|
||||
|
||||
The same API token can use a Bearer header instead. Replace the header file's
|
||||
line in the editor with the following, then use the same curl command above;
|
||||
line in the editor with the following, then use the same helper above;
|
||||
do not send both authentication headers:
|
||||
|
||||
```text
|
||||
@@ -326,12 +393,11 @@ Returns the canonical fleet connections ledger with per-row fleet-governance sta
|
||||
|
||||
The payload is the source of truth for enrollment, liveness, version drift, adapter health, config rollout, credential posture, update posture, and remote-control posture. Consumers must not rebuild those states from provider-specific config stores or display labels.
|
||||
|
||||
Use the private header file prepared in [Authentication](#-authentication),
|
||||
Define the private-response helper in [Authentication](#-authentication),
|
||||
with `settings:read` on the token:
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/connections
|
||||
pulse_api GET /api/connections
|
||||
```
|
||||
|
||||
### Unified Action Planning
|
||||
@@ -339,9 +405,10 @@ curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
The existing action API separates planning, approval and execution. Run these
|
||||
steps separately, inspecting each response before moving on; do not paste the
|
||||
whole sequence as an unattended recovery script. Use the private header file
|
||||
from [Authentication](#-authentication), not a token in a command or environment
|
||||
assignment. For remote access, replace the loopback origin with your Pulse HTTPS
|
||||
URL and keep certificate verification enabled.
|
||||
and `pulse_api` helper from [Authentication](#-authentication), not a token in a
|
||||
command or environment assignment. Inspect each reported private response file
|
||||
before proceeding. For remote access, change the helper as described there,
|
||||
using your Pulse HTTPS origin with certificate verification enabled.
|
||||
|
||||
| Endpoint | Token scope and access |
|
||||
| --- | --- |
|
||||
@@ -365,8 +432,7 @@ not an action-control or audit token.
|
||||
An empty capabilities list means there is nothing to plan for that resource.
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/agent/resource-capabilities/vm%3A42
|
||||
pulse_api GET /api/agent/resource-capabilities/vm%3A42
|
||||
```
|
||||
|
||||
2. **Plan only.** Use an advertised capability and its actual parameter schema;
|
||||
@@ -375,9 +441,7 @@ curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
authenticated credential, not a caller-supplied `requestedBy` value.
|
||||
|
||||
```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/actions/plan <<'JSON'
|
||||
pulse_api POST /api/actions/plan <<'JSON'
|
||||
{
|
||||
"requestId": "manual-recovery-123",
|
||||
"resourceId": "vm:42",
|
||||
@@ -398,9 +462,7 @@ or currently available.
|
||||
response rather than weakening the policy or creating another recovery action.
|
||||
|
||||
```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/actions/act_.../decision <<'JSON'
|
||||
pulse_api POST /api/actions/act_.../decision <<'JSON'
|
||||
{
|
||||
"outcome": "approved",
|
||||
"reason": "Inside maintenance window",
|
||||
@@ -413,9 +475,7 @@ JSON
|
||||
approval-free executable plan, use the executor's authorised header file:
|
||||
|
||||
```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/actions/act_.../execute <<'JSON'
|
||||
pulse_api POST /api/actions/act_.../execute <<'JSON'
|
||||
{
|
||||
"reason": "Execute approved recovery",
|
||||
"planHash": "sha256:..."
|
||||
@@ -431,13 +491,11 @@ resource's observed state.
|
||||
5. **Read the audit and events**, using a private header file with audit access:
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
'http://127.0.0.1:7655/api/audit/actions?resourceId=vm%3A42&limit=10'
|
||||
pulse_api GET '/api/audit/actions?resourceId=vm%3A42&limit=10'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/audit/actions/act_.../events
|
||||
pulse_api GET /api/audit/actions/act_.../events
|
||||
```
|
||||
|
||||
When audit logging is unavailable, use Pulse's action detail in the signed-in
|
||||
|
||||
@@ -17,7 +17,8 @@ secret does not appear in shell history or process arguments.
|
||||
|
||||
On a trusted machine, as the account running curl, prepare the file and open
|
||||
it in an editor. This preserves an existing file, restricts its directory and
|
||||
stops before editing if preparation fails or a credential path is a symlink:
|
||||
stops before editing if preparation fails or a credential path is symlinked or
|
||||
not a regular file:
|
||||
|
||||
```bash
|
||||
(
|
||||
@@ -29,6 +30,10 @@ stops before editing if preparation fails or a credential path is a symlink:
|
||||
printf 'Refusing a symlinked credential path.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -e "$auth_file" ] && [ ! -f "$auth_file" ]; then
|
||||
printf 'Credential file must be a regular file.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$auth_dir"
|
||||
chmod 700 "$auth_dir"
|
||||
touch "$auth_file"
|
||||
@@ -43,20 +48,82 @@ In the editor, save just this line, replacing `<token>` with the API token:
|
||||
X-API-Token: <token>
|
||||
```
|
||||
|
||||
Then make a read-only request (curl 7.76 or later):
|
||||
Define this helper in the same Bash session (curl 7.76 or later). The examples
|
||||
on this page use it to make **one request**, print only the HTTP status and save
|
||||
each response to a new owner-only file. This keeps resource names, audit details
|
||||
and credential-bearing error bodies out of terminal output and recordings.
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/state/summary
|
||||
pulse_api() (
|
||||
set -eu
|
||||
umask 077
|
||||
if [ "$#" -ne 2 ]; then
|
||||
printf 'Usage: pulse_api GET|POST /api/path\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
method=$1
|
||||
api_path=$2
|
||||
case "$api_path" in
|
||||
/api/*) ;;
|
||||
*) printf 'Use an API path on this Pulse instance, not a full URL.\n' >&2; exit 2 ;;
|
||||
esac
|
||||
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 ;;
|
||||
esac
|
||||
auth_dir="$HOME/.config/pulse"
|
||||
auth_file="$auth_dir/api-header"
|
||||
if [ -L "$HOME/.config" ] || [ -L "$auth_dir" ] || [ -L "$auth_file" ] || [ ! -f "$auth_file" ]; then
|
||||
printf 'Prepare a regular, private header file first.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod 700 "$auth_dir" || exit "$?"
|
||||
chmod 600 "$auth_file" || exit "$?"
|
||||
result_file=$(mktemp "$auth_dir/api-response.XXXXXX") || exit "$?"
|
||||
curl_exit=0
|
||||
status=$(curl --disable --fail-with-body --silent --show-error \
|
||||
--proto '=http' --noproxy 127.0.0.1 --connect-timeout 5 --max-time 20 \
|
||||
--header "@$auth_file" --output "$result_file" --write-out '%{http_code}' \
|
||||
"$@" "http://127.0.0.1:7655$api_path") || curl_exit=$?
|
||||
printf 'HTTP %s\nPrivate response: %s\n' "$status" "$result_file"
|
||||
[ "$curl_exit" -eq 0 ] || exit "$curl_exit"
|
||||
case "$status" in
|
||||
2??) ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
)
|
||||
```
|
||||
|
||||
This loopback URL applies only when curl runs on the Pulse host. For remote
|
||||
access, use your Pulse HTTPS URL and keep certificate verification enabled;
|
||||
do not use `--insecure`. On an authentication-enabled instance, the protected
|
||||
summary checks token access; `/api/health` is public and does not verify authentication.
|
||||
`--fail-with-body` returns a non-zero exit on HTTP errors, including 401 and
|
||||
403, while retaining the error response. Keep `--disable` first: it ignores
|
||||
local curl configuration that could otherwise enable credential-bearing trace output.
|
||||
Then make a read-only request with a `monitoring:read` token:
|
||||
|
||||
```bash
|
||||
pulse_api GET /api/state/summary
|
||||
```
|
||||
|
||||
The default loopback origin applies only when curl runs on the Pulse host and
|
||||
explicitly bypasses proxy environment settings. For remote access, replace
|
||||
`http://127.0.0.1:7655` **in the helper** with your Pulse HTTPS origin, and replace
|
||||
`--proto '=http' --noproxy 127.0.0.1` with `--proto '=https'`. Keep certificate
|
||||
verification enabled; for a private CA, add `--cacert` with a CA file you
|
||||
verified separately, never `--insecure`. Do not put credentials in the URL.
|
||||
|
||||
Open the reported response file privately to inspect it, including on failure;
|
||||
share only the relevant redacted error, not the whole response or file. Each
|
||||
call preserves earlier responses. HTTP 401 and 403 return curl's non-zero exit
|
||||
and retain the error body; redirects are not followed and are not success.
|
||||
The connection limit is five seconds and the whole request limit is twenty
|
||||
seconds. A partial file after a transport error is not a complete result.
|
||||
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.
|
||||
|
||||
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
|
||||
and does not verify authentication. The helper is local example code, not an
|
||||
installed Pulse command; define it again in a new shell before using these
|
||||
examples. Dispose of response files under your normal private-data policy.
|
||||
|
||||
Do not paste tokens into command lines, URLs or issue reports. Keep the header
|
||||
file private and outside shared repositories and diagnostics; do not use curl
|
||||
@@ -67,7 +134,7 @@ needed in Pulse's API Access settings.
|
||||
### Bearer Token
|
||||
|
||||
The same API token can use a Bearer header instead. Replace the header file's
|
||||
line in the editor with the following, then use the same curl command above;
|
||||
line in the editor with the following, then use the same helper above;
|
||||
do not send both authentication headers:
|
||||
|
||||
```text
|
||||
@@ -326,12 +393,11 @@ Returns the canonical fleet connections ledger with per-row fleet-governance sta
|
||||
|
||||
The payload is the source of truth for enrollment, liveness, version drift, adapter health, config rollout, credential posture, update posture, and remote-control posture. Consumers must not rebuild those states from provider-specific config stores or display labels.
|
||||
|
||||
Use the private header file prepared in [Authentication](#-authentication),
|
||||
Define the private-response helper in [Authentication](#-authentication),
|
||||
with `settings:read` on the token:
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/connections
|
||||
pulse_api GET /api/connections
|
||||
```
|
||||
|
||||
### Unified Action Planning
|
||||
@@ -339,9 +405,10 @@ curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
The existing action API separates planning, approval and execution. Run these
|
||||
steps separately, inspecting each response before moving on; do not paste the
|
||||
whole sequence as an unattended recovery script. Use the private header file
|
||||
from [Authentication](#-authentication), not a token in a command or environment
|
||||
assignment. For remote access, replace the loopback origin with your Pulse HTTPS
|
||||
URL and keep certificate verification enabled.
|
||||
and `pulse_api` helper from [Authentication](#-authentication), not a token in a
|
||||
command or environment assignment. Inspect each reported private response file
|
||||
before proceeding. For remote access, change the helper as described there,
|
||||
using your Pulse HTTPS origin with certificate verification enabled.
|
||||
|
||||
| Endpoint | Token scope and access |
|
||||
| --- | --- |
|
||||
@@ -365,8 +432,7 @@ not an action-control or audit token.
|
||||
An empty capabilities list means there is nothing to plan for that resource.
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/agent/resource-capabilities/vm%3A42
|
||||
pulse_api GET /api/agent/resource-capabilities/vm%3A42
|
||||
```
|
||||
|
||||
2. **Plan only.** Use an advertised capability and its actual parameter schema;
|
||||
@@ -375,9 +441,7 @@ curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
authenticated credential, not a caller-supplied `requestedBy` value.
|
||||
|
||||
```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/actions/plan <<'JSON'
|
||||
pulse_api POST /api/actions/plan <<'JSON'
|
||||
{
|
||||
"requestId": "manual-recovery-123",
|
||||
"resourceId": "vm:42",
|
||||
@@ -398,9 +462,7 @@ or currently available.
|
||||
response rather than weakening the policy or creating another recovery action.
|
||||
|
||||
```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/actions/act_.../decision <<'JSON'
|
||||
pulse_api POST /api/actions/act_.../decision <<'JSON'
|
||||
{
|
||||
"outcome": "approved",
|
||||
"reason": "Inside maintenance window",
|
||||
@@ -413,9 +475,7 @@ JSON
|
||||
approval-free executable plan, use the executor's authorised header file:
|
||||
|
||||
```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/actions/act_.../execute <<'JSON'
|
||||
pulse_api POST /api/actions/act_.../execute <<'JSON'
|
||||
{
|
||||
"reason": "Execute approved recovery",
|
||||
"planHash": "sha256:..."
|
||||
@@ -431,13 +491,11 @@ resource's observed state.
|
||||
5. **Read the audit and events**, using a private header file with audit access:
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
'http://127.0.0.1:7655/api/audit/actions?resourceId=vm%3A42&limit=10'
|
||||
pulse_api GET '/api/audit/actions?resourceId=vm%3A42&limit=10'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
http://127.0.0.1:7655/api/audit/actions/act_.../events
|
||||
pulse_api GET /api/audit/actions/act_.../events
|
||||
```
|
||||
|
||||
When audit logging is unavailable, use Pulse's action detail in the signed-in
|
||||
|
||||
@@ -0,0 +1,495 @@
|
||||
package installtests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Execute the actual reference recipes against synthetic peers only. In
|
||||
// particular, action examples must not acquire an automatic retry or emit a
|
||||
// private response just because a POST failed or its response was incomplete.
|
||||
func pulseAPIRecipes(doc string) (preparation, helper string, calls []string, err error) {
|
||||
for _, match := range regexp.MustCompile("(?s)```bash\n(.*?)```").FindAllStringSubmatch(doc, -1) {
|
||||
block := match[1]
|
||||
switch {
|
||||
case strings.Contains(block, "api-header") && strings.Contains(block, "vi "):
|
||||
preparation = block
|
||||
case strings.HasPrefix(block, "pulse_api() ("):
|
||||
helper = block
|
||||
case strings.HasPrefix(block, "pulse_api "):
|
||||
calls = append(calls, block)
|
||||
case strings.Contains(block, "curl "):
|
||||
return "", "", nil, fmt.Errorf("request bypasses the private-response helper")
|
||||
}
|
||||
}
|
||||
if preparation == "" || helper == "" || len(calls) != 8 {
|
||||
err = fmt.Errorf("missing credential/helper recipe or request examples (got %d)", len(calls))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func pulseAPIReference(t *testing.T) (string, string, []string) {
|
||||
t.Helper()
|
||||
doc, err := os.ReadFile(repoFile("docs", "API.md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mirror, err := os.ReadFile(repoFile("frontend-modern", "public", "docs", "API.md"))
|
||||
if err != nil || string(doc) != string(mirror) {
|
||||
t.Fatal("shipped API reference differs from the tested recipes")
|
||||
}
|
||||
prep, helper, calls, err := pulseAPIRecipes(string(doc))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return prep, helper, calls
|
||||
}
|
||||
|
||||
func runPulseAPIRecipe(t *testing.T, script, home, bin string, extraEnv ...string) (string, int) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "bash", "-c", script)
|
||||
cmd.Env = append(os.Environ(), "HOME="+home, "PATH="+bin+":"+os.Getenv("PATH"),
|
||||
"http_proxy=", "HTTP_PROXY=", "https_proxy=", "HTTPS_PROXY=", "ALL_PROXY=", "all_proxy=", "NO_PROXY=", "no_proxy=")
|
||||
cmd.Env = append(cmd.Env, extraEnv...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if ctx.Err() != nil {
|
||||
t.Fatal("API recipe exceeded the test watchdog; its own deadline did not complete")
|
||||
}
|
||||
if err == nil {
|
||||
return string(out), 0
|
||||
}
|
||||
if failure, ok := err.(*exec.ExitError); ok {
|
||||
return string(out), failure.ExitCode()
|
||||
}
|
||||
t.Fatal(err)
|
||||
return "", -1
|
||||
}
|
||||
|
||||
func TestPulseAPIGuidancePreparationIsPrivateAndFailClosed(t *testing.T) {
|
||||
prep, _, _ := pulseAPIReference(t)
|
||||
for _, mode := range []string{"new", "existing", "config symlink", "directory symlink", "file symlink", "dangling symlink", "non-regular", "mkdir failure", "chmod failure", "touch failure", "editor failure"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
home, bin := t.TempDir(), t.TempDir()
|
||||
dir := filepath.Join(home, ".config", "pulse")
|
||||
file := filepath.Join(dir, "api-header")
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mode == "new" || mode == "mkdir failure" {
|
||||
if err := os.Remove(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if mode == "existing" {
|
||||
if err := os.WriteFile(file, []byte("synthetic-existing-header"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if mode == "non-regular" {
|
||||
if err := os.Mkdir(file, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
target := filepath.Join(home, "unchanged")
|
||||
if strings.Contains(mode, "symlink") {
|
||||
link := file
|
||||
if mode == "config symlink" || mode == "directory symlink" {
|
||||
link = dir
|
||||
if mode == "config symlink" {
|
||||
link = filepath.Join(home, ".config")
|
||||
}
|
||||
if err := os.Remove(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mode == "config symlink" {
|
||||
if err := os.Remove(link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.Mkdir(target, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else if mode == "file symlink" {
|
||||
if err := os.WriteFile(target, []byte("synthetic-existing-header"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
edited := filepath.Join(home, "editor-started")
|
||||
editor := "#!/bin/sh\nprintf started > \"$API_EDITED\"\n"
|
||||
if mode == "editor failure" {
|
||||
editor += "exit 7\n"
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(bin, "vi"), []byte(editor), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.HasSuffix(mode, " failure") && mode != "editor failure" {
|
||||
name := strings.TrimSuffix(mode, " failure")
|
||||
if err := os.WriteFile(filepath.Join(bin, name), []byte("#!/bin/sh\nexit 7\n"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
output, exit := runPulseAPIRecipe(t, prep, home, bin, "API_EDITED="+edited)
|
||||
_, editorErr := os.Stat(edited)
|
||||
if mode == "new" || mode == "existing" {
|
||||
if exit != 0 || editorErr != nil {
|
||||
t.Fatalf("safe preparation failed: exit %d", exit)
|
||||
}
|
||||
for path, perm := range map[string]os.FileMode{dir: 0700, file: 0600} {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.Mode().Perm() != perm {
|
||||
t.Fatal("credential paths are not private")
|
||||
}
|
||||
}
|
||||
if mode == "existing" {
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil || string(data) != "synthetic-existing-header" {
|
||||
t.Fatal("preparation did not preserve the existing header")
|
||||
}
|
||||
}
|
||||
} else if exit == 0 || (mode != "editor failure" && !os.IsNotExist(editorErr)) {
|
||||
t.Fatal("unsafe or failed preparation reached the editor or succeeded")
|
||||
}
|
||||
if mode == "file symlink" {
|
||||
data, err := os.ReadFile(target)
|
||||
info, statErr := os.Stat(target)
|
||||
if err != nil || statErr != nil || string(data) != "synthetic-existing-header" || info.Mode().Perm() != 0644 {
|
||||
t.Fatal("preparation modified the symlink target")
|
||||
}
|
||||
}
|
||||
if mode == "config symlink" || mode == "directory symlink" {
|
||||
info, err := os.Stat(target)
|
||||
if err != nil || info.Mode().Perm() != 0755 {
|
||||
t.Fatal("preparation changed the target directory")
|
||||
}
|
||||
}
|
||||
if strings.Contains(output, "synthetic-existing-header") {
|
||||
t.Fatal("preparation disclosed a header")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPulseAPIGuidanceHelperStopsBeforeCurlOnUnsafeFilesOrPreparation(t *testing.T) {
|
||||
_, helper, _ := pulseAPIReference(t)
|
||||
for _, mode := range []string{"missing", "file symlink", "config symlink", "directory symlink", "non-regular", "chmod failure", "mktemp failure", "unsupported method", "full URL", "extra argument"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
home, bin := t.TempDir(), t.TempDir()
|
||||
dir := filepath.Join(home, ".config", "pulse")
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file := filepath.Join(dir, "api-header")
|
||||
if mode != "missing" && mode != "file symlink" && mode != "non-regular" {
|
||||
if err := os.WriteFile(file, []byte("synthetic-header"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if mode == "file symlink" {
|
||||
if err := os.Symlink(filepath.Join(home, "absent"), file); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if mode == "config symlink" || mode == "directory symlink" {
|
||||
link := dir
|
||||
if mode == "config symlink" {
|
||||
link = filepath.Join(home, ".config")
|
||||
}
|
||||
if err := os.Rename(link, link+"-retained"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(link+"-retained", link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if mode == "non-regular" {
|
||||
if err := os.Mkdir(file, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
called := filepath.Join(home, "curl-called")
|
||||
if err := os.WriteFile(filepath.Join(bin, "curl"), []byte("#!/bin/sh\nprintf started > \"$API_CALLED\"\n"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.HasSuffix(mode, " failure") {
|
||||
if err := os.WriteFile(filepath.Join(bin, strings.TrimSuffix(mode, " failure")), []byte("#!/bin/sh\nexit 7\n"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
call := "pulse_api GET /api/state/summary"
|
||||
switch mode {
|
||||
case "unsupported method":
|
||||
call = "pulse_api DELETE /api/actions/act_example"
|
||||
case "full URL":
|
||||
call = "pulse_api GET https://not-this-instance.invalid/api/state"
|
||||
case "extra argument":
|
||||
call += " --location"
|
||||
}
|
||||
// Bash disables implicit errexit inside functions used as conditions.
|
||||
// The documented helper must still stop on chmod/mktemp failure.
|
||||
script := helper + "\nif " + call + "; then exit 0; else exit $?; fi\n"
|
||||
_, exit := runPulseAPIRecipe(t, script, home, bin, "API_CALLED="+called)
|
||||
_, err := os.Stat(called)
|
||||
if exit == 0 || !os.IsNotExist(err) {
|
||||
t.Fatal("invalid input or failed preparation reached curl")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type apiRecipeRequest struct{ method, uri, token, contentType, body string }
|
||||
|
||||
func TestPulseAPIGuidanceCopiedRequestsKeepResponsesPrivateAndOneShot(t *testing.T) {
|
||||
_, helper, calls := pulseAPIReference(t)
|
||||
paths := []string{"/api/state/summary", "/api/connections", "/api/agent/resource-capabilities/vm%3A42", "/api/actions/plan", "/api/actions/act_.../decision", "/api/actions/act_.../execute", "/api/audit/actions?resourceId=vm%3A42&limit=10", "/api/audit/actions/act_.../events"}
|
||||
for index, call := range calls {
|
||||
for _, scenario := range []string{"success", "401", "403", "redirect", "partial", "untrusted HTTPS", "trusted HTTPS"} {
|
||||
t.Run(fmt.Sprintf("request-%d/%s", index, scenario), func(t *testing.T) {
|
||||
home, bin := t.TempDir(), t.TempDir()
|
||||
dir := filepath.Join(home, ".config", "pulse")
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const token = "synthetic-fixture-secret"
|
||||
const response = `{"token":"synthetic-fixture-secret","resource":"private-fixture-name","audit":"private-fixture-event"}`
|
||||
if err := os.WriteFile(filepath.Join(dir, "api-header"), []byte("X-API-Token: "+token+"\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
retained := filepath.Join(dir, "api-response.retained")
|
||||
if err := os.WriteFile(retained, []byte("previous-result"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
capture, trace := filepath.Join(home, "argv"), filepath.Join(home, "trace")
|
||||
realCurl, err := exec.LookPath("curl")
|
||||
if err != nil {
|
||||
t.Fatal("curl is required to verify copied API requests")
|
||||
}
|
||||
wrapper := "#!/bin/sh\nprintf '%s\\0' \"$@\" > \"$API_ARGV\"\nexec \"$API_CURL\" \"$@\"\n"
|
||||
if err := os.WriteFile(filepath.Join(bin, "curl"), []byte(wrapper), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(home, ".curlrc"), []byte("insecure\nlocation\ntrace = "+trace+"\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
requests := make(chan apiRecipeRequest, 4)
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
requests <- apiRecipeRequest{r.Method, r.URL.RequestURI(), r.Header.Get("X-API-Token"), r.Header.Get("Content-Type"), string(body)}
|
||||
switch scenario {
|
||||
case "401":
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case "403":
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case "redirect":
|
||||
w.Header().Set("Location", "/should-not-receive-header")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
case "partial":
|
||||
w.Header().Set("Content-Length", fmt.Sprint(len(response)+100))
|
||||
}
|
||||
_, _ = io.WriteString(w, response)
|
||||
})
|
||||
var server *httptest.Server
|
||||
recipe := helper
|
||||
if strings.Contains(scenario, "HTTPS") {
|
||||
server = httptest.NewUnstartedServer(handler)
|
||||
server.Config.ErrorLog = log.New(io.Discard, "", 0)
|
||||
server.StartTLS()
|
||||
recipe = strings.Replace(recipe, "--proto '=http' --noproxy 127.0.0.1", "--proto '=https'", 1)
|
||||
if scenario == "trusted HTTPS" {
|
||||
ca := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw})
|
||||
if err := os.WriteFile(filepath.Join(home, "ca.pem"), ca, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
recipe = strings.Replace(recipe, "curl --disable ", `curl --disable --cacert "$HOME/ca.pem" `, 1)
|
||||
}
|
||||
} else {
|
||||
server = httptest.NewServer(handler)
|
||||
}
|
||||
t.Cleanup(server.Close)
|
||||
recipe = strings.Replace(recipe, "http://127.0.0.1:7655", server.URL, 1)
|
||||
extra := []string{"API_ARGV=" + capture, "API_CURL=" + realCurl}
|
||||
if !strings.Contains(scenario, "HTTPS") {
|
||||
// The local recipe must not send credentials to an ambient proxy.
|
||||
extra = append(extra, "http_proxy=http://127.0.0.1:1", "ALL_PROXY=http://127.0.0.1:1")
|
||||
}
|
||||
output, exit := runPulseAPIRecipe(t, recipe+"\n"+call, home, bin, extra...)
|
||||
wantExit := map[string]int{"success": 0, "401": 22, "403": 22, "redirect": 1, "partial": 18, "untrusted HTTPS": 60, "trusted HTTPS": 0}[scenario]
|
||||
if exit != wantExit {
|
||||
t.Fatalf("request exit %d, want %d", exit, wantExit)
|
||||
}
|
||||
argv, err := os.ReadFile(capture)
|
||||
if err != nil || !strings.HasPrefix(string(argv), "--disable\x00") {
|
||||
t.Fatal("curl did not ignore configuration first")
|
||||
}
|
||||
for _, sensitive := range []string{token, "private-fixture-name", "private-fixture-event"} {
|
||||
if strings.Contains(output, sensitive) || strings.Contains(string(argv), sensitive) {
|
||||
t.Fatal("request printed private credentials or response data")
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(trace); !os.IsNotExist(err) {
|
||||
t.Fatal("local curl configuration enabled tracing")
|
||||
}
|
||||
responses, err := filepath.Glob(filepath.Join(dir, "api-response.??????"))
|
||||
if err != nil || len(responses) != 1 || !strings.Contains(output, responses[0]) {
|
||||
t.Fatal("response was not saved to one new, reported private file")
|
||||
}
|
||||
for path, perm := range map[string]os.FileMode{dir: 0700, filepath.Join(dir, "api-header"): 0600, responses[0]: 0600} {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.Mode().Perm() != perm {
|
||||
t.Fatal("request paths are not private")
|
||||
}
|
||||
}
|
||||
if data, err := os.ReadFile(retained); err != nil || string(data) != "previous-result" {
|
||||
t.Fatal("request replaced an earlier response")
|
||||
}
|
||||
if scenario == "untrusted HTTPS" {
|
||||
if len(requests) != 0 {
|
||||
t.Fatal("untrusted TLS peer received credentials")
|
||||
}
|
||||
} else {
|
||||
if len(requests) != 1 {
|
||||
t.Fatal("request was missing, redirected or retried")
|
||||
}
|
||||
got := <-requests
|
||||
method := "GET"
|
||||
contentType, body := "", ""
|
||||
if index >= 3 && index <= 5 {
|
||||
method, contentType = "POST", "application/json"
|
||||
_, body, _ = strings.Cut(call, "<<'JSON'\n")
|
||||
body = strings.TrimSuffix(body, "JSON\n")
|
||||
}
|
||||
if got != (apiRecipeRequest{method, paths[index], token, contentType, body}) {
|
||||
t.Fatal("request method, endpoint, private header or JSON body differs from the example")
|
||||
}
|
||||
if data, err := os.ReadFile(responses[0]); err != nil || string(data) != response {
|
||||
t.Fatal("HTTP response, including failure or partial body, was not retained privately")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPulseAPIGuidanceTimedOutPOSTIsNotRepeated(t *testing.T) {
|
||||
_, helper, calls := pulseAPIReference(t)
|
||||
home, bin := t.TempDir(), t.TempDir()
|
||||
dir := filepath.Join(home, ".config", "pulse")
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "api-header"), []byte("X-API-Token: synthetic-timeout-secret\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
requests := make(chan struct{}, 4)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.Copy(io.Discard, r.Body)
|
||||
requests <- struct{}{}
|
||||
w.Header().Set("Content-Length", "100")
|
||||
_, _ = io.WriteString(w, "private-partial-response")
|
||||
w.(http.Flusher).Flush()
|
||||
<-r.Context().Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
if !strings.Contains(helper, "--connect-timeout 5 --max-time 20") {
|
||||
t.Fatal("documented request deadlines are missing")
|
||||
}
|
||||
// Use the actual documented twenty-second limit, not a test-only shortened
|
||||
// deadline. An execution endpoint can have acted even when this read fails.
|
||||
helper = strings.Replace(helper, "http://127.0.0.1:7655", server.URL, 1)
|
||||
start := time.Now()
|
||||
output, exit := runPulseAPIRecipe(t, helper+"\n"+calls[5], home, bin)
|
||||
if exit != 28 || time.Since(start) > 25*time.Second || len(requests) != 1 {
|
||||
t.Fatal("POST did not stop at its request deadline or was retried")
|
||||
}
|
||||
if strings.Contains(output, "private-partial-response") || strings.Contains(output, "synthetic-timeout-secret") {
|
||||
t.Fatal("timed-out POST disclosed a private body or credential")
|
||||
}
|
||||
responses, err := filepath.Glob(filepath.Join(dir, "api-response.??????"))
|
||||
if err != nil || len(responses) != 1 {
|
||||
t.Fatal("timed-out POST did not retain its response")
|
||||
}
|
||||
if data, err := os.ReadFile(responses[0]); err != nil || string(data) != "private-partial-response" {
|
||||
t.Fatal("partial response was not retained for private reconciliation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPulseAPIGuidanceExactParentPrintsPrivateResponse(t *testing.T) {
|
||||
inputs := os.Getenv("PULSE_PROOF_INPUTS")
|
||||
if inputs == "" {
|
||||
t.Skip("exact-parent input is provided by the candidate source proof")
|
||||
}
|
||||
old, err := os.ReadFile(filepath.Join(inputs, "api-parent.md"))
|
||||
if os.IsNotExist(err) {
|
||||
t.Skip("unrelated proof input does not supply the exact parent")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var request string
|
||||
for _, match := range regexp.MustCompile("(?s)```bash\n(.*?)```").FindAllStringSubmatch(string(old), -1) {
|
||||
if strings.Contains(match[1], "/api/state/summary") && strings.Contains(match[1], "curl ") {
|
||||
request = match[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if request == "" {
|
||||
t.Fatal("exact old reference summary command is missing")
|
||||
}
|
||||
home, bin := t.TempDir(), t.TempDir()
|
||||
dir := filepath.Join(home, ".config", "pulse")
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "api-header"), []byte("X-API-Token: synthetic-parent-secret\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const response = "synthetic-parent-secret private-parent-resource"
|
||||
requests := make(chan struct{}, 2)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests <- struct{}{}
|
||||
if r.Header.Get("X-API-Token") != "synthetic-parent-secret" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
_, _ = io.WriteString(w, response)
|
||||
}))
|
||||
defer server.Close()
|
||||
request = strings.Replace(request, "http://127.0.0.1:7655", server.URL, 1)
|
||||
output, exit := runPulseAPIRecipe(t, request, home, bin)
|
||||
if exit != 0 || len(requests) != 1 || !strings.Contains(output, response) {
|
||||
t.Fatal("exact parent did not reproduce the private-response console exposure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPulseAPIGuidanceRejectsExactOldReference(t *testing.T) {
|
||||
inputs := os.Getenv("PULSE_PROOF_INPUTS")
|
||||
if inputs == "" {
|
||||
t.Skip("exact-parent input is provided by the candidate source proof")
|
||||
}
|
||||
old, err := os.ReadFile(filepath.Join(inputs, "api-parent.md"))
|
||||
if os.IsNotExist(err) {
|
||||
t.Skip("unrelated proof input does not supply the exact parent")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, _, err := pulseAPIRecipes(string(old)); err == nil {
|
||||
t.Fatal("old direct, unbounded console-response examples were accepted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user