Add CLI args and tests
This commit is contained in:
parent
0a86cfaee6
commit
160388194a
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.claude/
|
||||
20
README.md
20
README.md
@ -4,10 +4,15 @@ CLI tool for submitting posts to WordPress through its REST API using curl.
|
||||
|
||||
## Install
|
||||
### WordPress
|
||||
Install [Application Passwords](https://wordpress.org/plugins/application-passwords/) plugin and follow its installation steps (i.e. create a new passowrd for your user).
|
||||
Application Passwords are built into WordPress core since 5.6 — no plugin needed.
|
||||
Go to Users → Profile → Application Passwords on your site, generate a new
|
||||
password for your user, and note it down. Your site must be served over
|
||||
HTTPS for this to work.
|
||||
|
||||
### Local shell
|
||||
Make sure you have `curl` installed. If you want to enter text in markdown format, install `python-markdown2`.
|
||||
Make sure you have `curl` and `jq` installed. If you want to enter text in
|
||||
markdown format, install `python-markdown2` (`pip install markdown2` or your
|
||||
distro's `python3-markdown2` package).
|
||||
|
||||
Create a config file `~/.config/wordpress-rest-curl/config.sh` from the `config.sh.template` file.
|
||||
|
||||
@ -19,3 +24,14 @@ Copy the posting script to e.g. `/usr/local/bin/wordpress-post` so that you can
|
||||
* Confirm the displayed post by pressing enter.
|
||||
* Done!
|
||||
|
||||
### Options
|
||||
* `--file PATH` — use PATH as the post content instead of opening `$EDITOR`.
|
||||
* `--publish` — shortcut for `STATUS=publish`, posts live immediately.
|
||||
* `--edit POST_ID` — update an existing post instead of creating a new one.
|
||||
|
||||
## Testing
|
||||
`tests/run_tests.sh` runs post.sh against a local mock of the WordPress REST
|
||||
API (`tests/mock_wp_server.py`) — no real WordPress install or network access
|
||||
needed. Requires the same dependencies as the script itself (`curl`, `jq`,
|
||||
`python3-markdown2`).
|
||||
|
||||
|
||||
82
post.sh
82
post.sh
@ -14,15 +14,39 @@ TMPFILE=/tmp/wordpress-post.txt # location of a temporary file with the post tex
|
||||
|
||||
source ~/.config/wordpress-rest-curl/config.sh
|
||||
|
||||
# let the user create the post
|
||||
$EDITOR $TMPFILE || exit 1
|
||||
# CLI options (override config):
|
||||
# --edit POST_ID update an existing post instead of creating a new one
|
||||
# --publish shortcut for STATUS=publish
|
||||
# --file PATH use PATH as the post source instead of opening $EDITOR
|
||||
POST_ID=""
|
||||
SOURCE_FILE=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--edit)
|
||||
POST_ID="$2"; shift 2 ;;
|
||||
--publish)
|
||||
STATUS="publish"; shift ;;
|
||||
--file)
|
||||
SOURCE_FILE="$2"; shift 2 ;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# let the user provide the post content
|
||||
if [[ -n "$SOURCE_FILE" ]]; then
|
||||
cp "$SOURCE_FILE" "$TMPFILE" || exit 1
|
||||
else
|
||||
$EDITOR $TMPFILE || exit 1
|
||||
fi
|
||||
[[ -e $TMPFILE ]] || exit 1
|
||||
|
||||
# transformations
|
||||
cp $TMPFILE $TMPFILE.trans
|
||||
for T in "${TRANSFORM[@]}"; do
|
||||
if [[ "$T" == "title" ]]; then
|
||||
python >$TMPFILE.trans2 <<EOF
|
||||
python3 >$TMPFILE.trans2 <<EOF
|
||||
import re
|
||||
with open('$TMPFILE.trans', 'r') as file:
|
||||
title = ''
|
||||
@ -39,7 +63,7 @@ EOF
|
||||
rm $TMPFILE.title
|
||||
mv $TMPFILE.trans2 $TMPFILE.trans
|
||||
elif [[ "$T" == "markdown" ]]; then
|
||||
python >$TMPFILE.trans2 <<EOF
|
||||
python3 >$TMPFILE.trans2 <<EOF
|
||||
import markdown2
|
||||
print(markdown2.markdown_path('$TMPFILE.trans'))
|
||||
EOF
|
||||
@ -50,7 +74,7 @@ CONTENT=`cat $TMPFILE.trans`
|
||||
rm $TMPFILE.trans
|
||||
|
||||
echo "--- START POST ---"
|
||||
echo $CONTENT
|
||||
echo "$CONTENT"
|
||||
echo "--- END POST ---"
|
||||
echo "Title: $TITLE"
|
||||
echo "User: $USER"
|
||||
@ -58,18 +82,50 @@ echo "Server: $SERVER"
|
||||
echo "Status: $STATUS"
|
||||
echo "Categories: $CATEGORIES"
|
||||
echo "Tags: $TAGS"
|
||||
if [[ -n "$POST_ID" ]]; then
|
||||
echo "Editing post: $POST_ID"
|
||||
fi
|
||||
echo
|
||||
read -p "Press enter to confirm..."
|
||||
|
||||
# build the JSON payload (categories/tags as real arrays, not comma strings)
|
||||
CATEGORIES_JSON="[]"
|
||||
if [[ -n "$CATEGORIES" ]]; then
|
||||
CATEGORIES_JSON=$(echo "$CATEGORIES" | tr ',' '\n' | jq -R 'select(length > 0) | tonumber' | jq -s '.')
|
||||
fi
|
||||
TAGS_JSON="[]"
|
||||
if [[ -n "$TAGS" ]]; then
|
||||
TAGS_JSON=$(echo "$TAGS" | tr ',' '\n' | jq -R 'select(length > 0) | tonumber' | jq -s '.')
|
||||
fi
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg title "$TITLE" \
|
||||
--arg content "$CONTENT" \
|
||||
--arg status "$STATUS" \
|
||||
--argjson categories "$CATEGORIES_JSON" \
|
||||
--argjson tags "$TAGS_JSON" \
|
||||
'{title: $title, content: $content, status: $status, categories: $categories, tags: $tags}')
|
||||
|
||||
BASE_URL="$SERVER"
|
||||
[[ "$BASE_URL" != http://* && "$BASE_URL" != https://* ]] && BASE_URL="https://$BASE_URL"
|
||||
URL="$BASE_URL/wp-json/wp/v2/posts/"
|
||||
[[ -n "$POST_ID" ]] && URL="$BASE_URL/wp-json/wp/v2/posts/$POST_ID"
|
||||
|
||||
# push the post!
|
||||
curl --user "$USER:$PASSWORD" -X POST \
|
||||
--data-urlencode "title=$TITLE" \
|
||||
--data-urlencode "content=$CONTENT" \
|
||||
--data-urlencode "status=$STATUS" \
|
||||
--data-urlencode "categories=$CATEGORIES" \
|
||||
--data-urlencode "tags=$TAGS" \
|
||||
"https://$SERVER/wp-json/wp/v2/posts/" || exit 1
|
||||
RESPONSE=$(curl --silent --show-error --user "$USER:$PASSWORD" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "$PAYLOAD" \
|
||||
-w '\n%{http_code}' \
|
||||
"$URL")
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
if [[ "$HTTP_CODE" -lt 200 || "$HTTP_CODE" -ge 300 ]]; then
|
||||
echo "Error: WordPress API returned HTTP $HTTP_CODE" >&2
|
||||
echo "$BODY" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$BODY"
|
||||
|
||||
# backup the posted data (temporarily until it is auto-removed)
|
||||
mv $TMPFILE $TMPFILE.posted
|
||||
|
||||
|
||||
54
tests/mock_wp_server.py
Executable file
54
tests/mock_wp_server.py
Executable file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
# Minimal mock of the WordPress REST API /wp/v2/posts endpoint, for testing
|
||||
# post.sh without a real WordPress install.
|
||||
#
|
||||
# Behavior is controlled by two files (paths given via env vars):
|
||||
# MOCK_WP_CONTROL_FILE - optional JSON {"status": int, "body": {...}} that
|
||||
# determines the response to the next request. If absent, responds
|
||||
# 201 with the submitted fields echoed back under an "id".
|
||||
# MOCK_WP_REQUEST_FILE - overwritten on every request with
|
||||
# {"path": ..., "payload": ...} so a test can assert on what was sent.
|
||||
#
|
||||
# Listens on 127.0.0.1:$MOCK_WP_PORT (default 8899).
|
||||
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
|
||||
PORT = int(os.environ.get("MOCK_WP_PORT", "8899"))
|
||||
CONTROL_FILE = os.environ["MOCK_WP_CONTROL_FILE"]
|
||||
REQUEST_FILE = os.environ["MOCK_WP_REQUEST_FILE"]
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
data = json.loads(body) if body else {}
|
||||
except json.JSONDecodeError:
|
||||
data = {"_raw": body.decode(errors="replace")}
|
||||
|
||||
with open(REQUEST_FILE, "w") as f:
|
||||
json.dump({"path": self.path, "payload": data}, f)
|
||||
|
||||
status, resp_body = 201, {"id": 1, **data}
|
||||
if os.path.exists(CONTROL_FILE):
|
||||
with open(CONTROL_FILE) as f:
|
||||
control = json.load(f)
|
||||
status = control.get("status", status)
|
||||
resp_body = control.get("body", resp_body)
|
||||
|
||||
payload = json.dumps(resp_body).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
http.server.HTTPServer(("127.0.0.1", PORT), Handler).serve_forever()
|
||||
94
tests/run_tests.sh
Executable file
94
tests/run_tests.sh
Executable file
@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
# Test suite for post.sh, run against tests/mock_wp_server.py so no real
|
||||
# WordPress install is needed. Requires curl, jq, python3, python3-markdown2.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
for bin in curl jq python3; do
|
||||
command -v "$bin" >/dev/null || { echo "missing dependency: $bin"; exit 1; }
|
||||
done
|
||||
python3 -c "import markdown2" 2>/dev/null || { echo "missing dependency: python3-markdown2"; exit 1; }
|
||||
|
||||
PORT=8899
|
||||
WORKDIR=$(mktemp -d)
|
||||
trap 'kill $SERVER_PID 2>/dev/null; rm -rf "$WORKDIR"' EXIT
|
||||
|
||||
export MOCK_WP_PORT=$PORT
|
||||
export MOCK_WP_CONTROL_FILE="$WORKDIR/control.json"
|
||||
export MOCK_WP_REQUEST_FILE="$WORKDIR/request.json"
|
||||
|
||||
python3 -u tests/mock_wp_server.py &
|
||||
SERVER_PID=$!
|
||||
for i in $(seq 1 20); do
|
||||
curl -s -o /dev/null "http://127.0.0.1:$PORT/" && break
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
export HOME="$WORKDIR/home"
|
||||
mkdir -p "$HOME/.config/wordpress-rest-curl"
|
||||
cat > "$HOME/.config/wordpress-rest-curl/config.sh" <<EOF
|
||||
TRANSFORM=('title' 'markdown')
|
||||
USER="tester"
|
||||
PASSWORD="testpass"
|
||||
SERVER="http://127.0.0.1:$PORT"
|
||||
CATEGORIES="3,7"
|
||||
TAGS="10"
|
||||
EOF
|
||||
|
||||
POST_FILE="$WORKDIR/post.md"
|
||||
cat > "$POST_FILE" <<'EOF2'
|
||||
# My Test Title
|
||||
|
||||
Some **markdown** content here.
|
||||
EOF2
|
||||
|
||||
run_post_sh() {
|
||||
printf "" | "$PWD/post.sh" "$@"
|
||||
}
|
||||
|
||||
FAIL=0
|
||||
assert_eq() {
|
||||
local desc="$1" expected="$2" actual="$3"
|
||||
if [[ "$expected" != "$actual" ]]; then
|
||||
echo "FAIL: $desc (expected [$expected], got [$actual])"
|
||||
FAIL=1
|
||||
else
|
||||
echo "PASS: $desc"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- create + publish ---
|
||||
rm -f "$MOCK_WP_REQUEST_FILE" "$MOCK_WP_CONTROL_FILE"
|
||||
run_post_sh --file "$POST_FILE" --publish >/dev/null
|
||||
assert_eq "create: exit code" "0" "$?"
|
||||
REQ=$(cat "$MOCK_WP_REQUEST_FILE")
|
||||
assert_eq "create: path" "/wp-json/wp/v2/posts/" "$(jq -r '.path' <<<"$REQ")"
|
||||
assert_eq "create: title extracted from heading" "My Test Title" "$(jq -r '.payload.title' <<<"$REQ")"
|
||||
assert_eq "create: status" "publish" "$(jq -r '.payload.status' <<<"$REQ")"
|
||||
assert_eq "create: categories sent as array" "[3,7]" "$(jq -c '.payload.categories' <<<"$REQ")"
|
||||
assert_eq "create: tags sent as array" "[10]" "$(jq -c '.payload.tags' <<<"$REQ")"
|
||||
assert_eq "create: content rendered as markdown" "true" "$(jq -r '.payload.content | test("<strong>markdown</strong>")' <<<"$REQ")"
|
||||
|
||||
# --- edit an existing post ---
|
||||
rm -f "$MOCK_WP_REQUEST_FILE" "$MOCK_WP_CONTROL_FILE"
|
||||
run_post_sh --file "$POST_FILE" --edit 99 >/dev/null
|
||||
assert_eq "edit: exit code" "0" "$?"
|
||||
REQ=$(cat "$MOCK_WP_REQUEST_FILE")
|
||||
assert_eq "edit: path includes post ID" "/wp-json/wp/v2/posts/99" "$(jq -r '.path' <<<"$REQ")"
|
||||
assert_eq "edit: status defaults to draft" "draft" "$(jq -r '.payload.status' <<<"$REQ")"
|
||||
|
||||
# --- non-2xx response is surfaced and script exits non-zero ---
|
||||
rm -f "$MOCK_WP_REQUEST_FILE"
|
||||
echo '{"status": 401, "body": {"code": "rest_forbidden", "message": "Sorry, you are not allowed to do that."}}' > "$MOCK_WP_CONTROL_FILE"
|
||||
OUT=$(run_post_sh --file "$POST_FILE" 2>&1)
|
||||
assert_eq "error: exit code" "1" "$?"
|
||||
assert_eq "error: HTTP status surfaced" "true" "$(grep -q "HTTP 401" <<<"$OUT" && echo true || echo false)"
|
||||
rm -f "$MOCK_WP_CONTROL_FILE"
|
||||
|
||||
echo
|
||||
if [[ $FAIL -eq 0 ]]; then
|
||||
echo "All tests passed."
|
||||
else
|
||||
echo "Some tests FAILED."
|
||||
fi
|
||||
exit $FAIL
|
||||
Loading…
Reference in New Issue
Block a user