Compare commits
5 Commits
0a86cfaee6
...
072f74ed2d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
072f74ed2d | ||
|
|
ebc2de4190 | ||
|
|
c283c47cef | ||
|
|
8d1488f354 | ||
|
|
160388194a |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.claude/
|
||||||
52
README.md
52
README.md
@ -4,18 +4,60 @@ CLI tool for submitting posts to WordPress through its REST API using curl.
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
### WordPress
|
### 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
|
### 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.
|
Copy `config.sh.template` to `~/.config/wordpress-rest-curl/config.sh` and
|
||||||
|
fill in whatever's shared across sites (`TRANSFORM`, `EDITOR`, `STATUS`, ...).
|
||||||
|
`config.sh` is always loaded first if it exists — it doesn't need `USER`,
|
||||||
|
`PASSWORD`, or `SERVER` filled in.
|
||||||
|
|
||||||
Copy the posting script to e.g. `/usr/local/bin/wordpress-post` so that you can run it from anywhere.
|
If you post to more than one WordPress instance, copy `site.sh.template` to
|
||||||
|
`~/.config/wordpress-rest-curl/NAME.sh` once per site, e.g.
|
||||||
|
`blog-example-com.sh` and `news-example-com.sh`, containing just the per-instance bits
|
||||||
|
(`USER`, `PASSWORD`, `SERVER`, and any `CATEGORIES`/`TAGS` that differ). Pick
|
||||||
|
one at run time with `--site NAME` (see Options below) — its values are
|
||||||
|
layered on top of `config.sh`, overriding only what it sets.
|
||||||
|
|
||||||
|
Copy the scripts to e.g. `/usr/local/bin/wordpress-post` and `/usr/local/bin/wordpress-terms` so that you can run them from anywhere. Keep them in the same directory as `lib/config.sh`, or copy that directory alongside them — both scripts source it.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
* Run the script
|
* Run `post.sh`
|
||||||
* Text editor opens. Type your post, save it.
|
* Text editor opens. Type your post, save it.
|
||||||
* Confirm the displayed post by pressing enter.
|
* Confirm the displayed post by pressing enter.
|
||||||
* Done!
|
* Done!
|
||||||
|
|
||||||
|
### Options
|
||||||
|
* `--site NAME` — use `~/.config/wordpress-rest-curl/NAME.sh` instead of the default `config.sh`. Useful when posting to multiple WordPress instances with different credentials.
|
||||||
|
* `--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.
|
||||||
|
* `--date DATE` — backdate the post to DATE, an ISO 8601 UTC timestamp (e.g. `2026-07-27T10:00:00Z`, matching a Gitea commit date) — sent as the post's `date_gmt`.
|
||||||
|
* `--category NAME` — attach category NAME by name (repeatable). Looked up on confirm; created automatically if it doesn't exist yet. Merged with any numeric IDs already in `CATEGORIES`.
|
||||||
|
* `--tag NAME` — same as `--category`, for tags (repeatable). Merged with `TAGS`.
|
||||||
|
|
||||||
|
## Categories and tags
|
||||||
|
`post.sh`'s `CATEGORIES`/`TAGS` config vars are numeric IDs, but `--category`/`--tag` (see Options) let you pass names directly on the command line — no separate lookup step needed, they're resolved (or created if missing) as part of running `post.sh`.
|
||||||
|
|
||||||
|
`terms.sh` is for inspecting or managing terms directly (same `--site NAME` config as `post.sh`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./terms.sh --list # id, name, slug, count — one per line
|
||||||
|
./terms.sh --type tag --list # tags instead of categories
|
||||||
|
./terms.sh --find "Hardware" # prints the id, or exits 1 if missing
|
||||||
|
./terms.sh --create "Hardware" # creates it, prints the new id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
`tests/run_tests.sh` runs post.sh and terms.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 scripts
|
||||||
|
themselves (`curl`, `jq`, `python3-markdown2`).
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# Shared defaults, loaded before any --site NAME file. If you only ever post
|
||||||
|
# to one WordPress instance, fill in USER/PASSWORD/SERVER here too and skip
|
||||||
|
# --site entirely. If you post to more than one, leave those three blank
|
||||||
|
# here and put them in per-site files instead (see README).
|
||||||
EDITOR=vim
|
EDITOR=vim
|
||||||
TRANSFORM=('title' 'markdown') # empty or a subset of: 'title' 'markdown'
|
TRANSFORM=('title' 'markdown') # empty or a subset of: 'title' 'markdown'
|
||||||
USER="" # WP user to create the post
|
USER="" # WP user to create the post
|
||||||
@ -7,4 +11,5 @@ SERVER="" # server hostname, optionally with subdirectories
|
|||||||
STATUS="draft" # one of publish,future,draft,pending,private
|
STATUS="draft" # one of publish,future,draft,pending,private
|
||||||
CATEGORIES="" # comma separated integer IDs of categories
|
CATEGORIES="" # comma separated integer IDs of categories
|
||||||
TAGS="" # comma separated integer IDs of tags
|
TAGS="" # comma separated integer IDs of tags
|
||||||
|
# USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) wordpress-rest-curl/1.0" # uncomment to override; some hosts' DDoS/bot protection blocks curl's default UA
|
||||||
|
|
||||||
|
|||||||
78
lib/config.sh
Normal file
78
lib/config.sh
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Shared by post.sh and terms.sh: layered config loading and base URL
|
||||||
|
# construction. The caller sets SITE (possibly empty) and CONFIG_DIR before
|
||||||
|
# calling wp_load_config, and USER/PASSWORD/SERVER/... end up in scope.
|
||||||
|
|
||||||
|
CONFIG_DIR="${WORDPRESS_REST_CURL_CONFIG_DIR:-$HOME/.config/wordpress-rest-curl}"
|
||||||
|
|
||||||
|
wp_load_config() {
|
||||||
|
local default_config_file="$CONFIG_DIR/config.sh"
|
||||||
|
[[ -e "$default_config_file" ]] && source "$default_config_file"
|
||||||
|
|
||||||
|
if [[ -n "$SITE" ]]; then
|
||||||
|
local site_config_file="$CONFIG_DIR/$SITE.sh"
|
||||||
|
[[ -e "$site_config_file" ]] || { echo "Config file not found: $site_config_file" >&2; exit 1; }
|
||||||
|
source "$site_config_file"
|
||||||
|
elif [[ ! -e "$default_config_file" ]]; then
|
||||||
|
echo "No site specified and no default config found. Pass --site NAME or create $default_config_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prints $SERVER normalized into a scheme-qualified, slash-trimmed base URL.
|
||||||
|
wp_base_url() {
|
||||||
|
local base_url="$SERVER"
|
||||||
|
[[ "$base_url" != http://* && "$base_url" != https://* ]] && base_url="https://$base_url"
|
||||||
|
echo "${base_url%/}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runs curl against the WP REST API with shared auth/UA/error-handling.
|
||||||
|
# Usage: wp_api_call METHOD PATH [curl-args...]
|
||||||
|
# Prints the response body on success; on a non-2xx status prints the error
|
||||||
|
# to stderr and returns 1 (does not exit, so callers can decide what to do).
|
||||||
|
wp_api_call() {
|
||||||
|
local method="$1" path="$2"
|
||||||
|
shift 2
|
||||||
|
local response http_code body
|
||||||
|
response=$(curl --silent --show-error --user "$USER:$PASSWORD" -X "$method" \
|
||||||
|
-A "$USER_AGENT" \
|
||||||
|
-w '\n%{http_code}' \
|
||||||
|
"$@" \
|
||||||
|
"$(wp_base_url)$path")
|
||||||
|
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
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prints every item across all pages of a paginated WP list endpoint at
|
||||||
|
# PATH, one JSON object per line.
|
||||||
|
wp_api_list_all() {
|
||||||
|
local path="$1" page=1 count body
|
||||||
|
while :; do
|
||||||
|
body=$(wp_api_call GET "$path?per_page=100&page=$page") || exit 1
|
||||||
|
count=$(echo "$body" | jq 'length')
|
||||||
|
[[ "$count" -eq 0 ]] && break
|
||||||
|
echo "$body" | jq -c '.[]'
|
||||||
|
[[ "$count" -lt 100 ]] && break
|
||||||
|
page=$((page + 1))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolves NAME to a numeric term ID under taxonomy endpoint PATH (e.g.
|
||||||
|
# /wp-json/wp/v2/categories), creating the term first if it doesn't exist.
|
||||||
|
wp_resolve_term() {
|
||||||
|
local path="$1" name="$2" found_id payload body
|
||||||
|
found_id=$(wp_api_list_all "$path" | jq -r --arg name "$name" 'select(.name == $name) | .id' | head -n1)
|
||||||
|
if [[ -n "$found_id" ]]; then
|
||||||
|
echo "$found_id"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
payload=$(jq -n --arg name "$name" '{name: $name}')
|
||||||
|
body=$(wp_api_call POST "$path" -H "Content-Type: application/json" --data "$payload") || exit 1
|
||||||
|
echo "$body" | jq -r '.id'
|
||||||
|
}
|
||||||
119
post.sh
119
post.sh
@ -11,18 +11,71 @@ STATUS="draft" # one of publish,future,draft,pending,private
|
|||||||
CATEGORIES="" # comma separated integer IDs of categories
|
CATEGORIES="" # comma separated integer IDs of categories
|
||||||
TAGS="" # comma separated integer IDs of tags
|
TAGS="" # comma separated integer IDs of tags
|
||||||
TMPFILE=/tmp/wordpress-post.txt # location of a temporary file with the post text
|
TMPFILE=/tmp/wordpress-post.txt # location of a temporary file with the post text
|
||||||
|
USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) wordpress-rest-curl/1.0" # some hosts block curl's default UA as bot traffic
|
||||||
|
|
||||||
source ~/.config/wordpress-rest-curl/config.sh
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/lib/config.sh"
|
||||||
|
|
||||||
# let the user create the post
|
# CLI options:
|
||||||
|
# --site NAME after config.sh, also source $CONFIG_DIR/NAME.sh, whose
|
||||||
|
# values override config.sh's (lets you keep just the
|
||||||
|
# per-instance bits — USER/PASSWORD/SERVER — in each site
|
||||||
|
# file, and shared defaults in config.sh)
|
||||||
|
# --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
|
||||||
|
# --date DATE backdate the post to DATE, an ISO 8601 UTC timestamp
|
||||||
|
# (e.g. 2026-07-27T10:00:00Z, matching a Gitea commit
|
||||||
|
# date) — sent as the post's date_gmt
|
||||||
|
# --category NAME attach category NAME, looking up its ID or creating it
|
||||||
|
# if it doesn't exist yet (repeatable)
|
||||||
|
# --tag NAME same as --category, for tags (repeatable)
|
||||||
|
SITE=""
|
||||||
|
POST_ID=""
|
||||||
|
SOURCE_FILE=""
|
||||||
|
PUBLISH=0
|
||||||
|
DATE=""
|
||||||
|
CATEGORY_NAMES=()
|
||||||
|
TAG_NAMES=()
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--site)
|
||||||
|
SITE="$2"; shift 2 ;;
|
||||||
|
--edit)
|
||||||
|
POST_ID="$2"; shift 2 ;;
|
||||||
|
--publish)
|
||||||
|
PUBLISH=1; shift ;;
|
||||||
|
--file)
|
||||||
|
SOURCE_FILE="$2"; shift 2 ;;
|
||||||
|
--date)
|
||||||
|
DATE="$2"; shift 2 ;;
|
||||||
|
--category)
|
||||||
|
CATEGORY_NAMES+=("$2"); shift 2 ;;
|
||||||
|
--tag)
|
||||||
|
TAG_NAMES+=("$2"); shift 2 ;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
wp_load_config
|
||||||
|
|
||||||
|
[[ "$PUBLISH" -eq 1 ]] && STATUS="publish"
|
||||||
|
|
||||||
|
# let the user provide the post content
|
||||||
|
if [[ -n "$SOURCE_FILE" ]]; then
|
||||||
|
cp "$SOURCE_FILE" "$TMPFILE" || exit 1
|
||||||
|
else
|
||||||
$EDITOR $TMPFILE || exit 1
|
$EDITOR $TMPFILE || exit 1
|
||||||
|
fi
|
||||||
[[ -e $TMPFILE ]] || exit 1
|
[[ -e $TMPFILE ]] || exit 1
|
||||||
|
|
||||||
# transformations
|
# transformations
|
||||||
cp $TMPFILE $TMPFILE.trans
|
cp $TMPFILE $TMPFILE.trans
|
||||||
for T in "${TRANSFORM[@]}"; do
|
for T in "${TRANSFORM[@]}"; do
|
||||||
if [[ "$T" == "title" ]]; then
|
if [[ "$T" == "title" ]]; then
|
||||||
python >$TMPFILE.trans2 <<EOF
|
python3 >$TMPFILE.trans2 <<EOF
|
||||||
import re
|
import re
|
||||||
with open('$TMPFILE.trans', 'r') as file:
|
with open('$TMPFILE.trans', 'r') as file:
|
||||||
title = ''
|
title = ''
|
||||||
@ -39,7 +92,7 @@ EOF
|
|||||||
rm $TMPFILE.title
|
rm $TMPFILE.title
|
||||||
mv $TMPFILE.trans2 $TMPFILE.trans
|
mv $TMPFILE.trans2 $TMPFILE.trans
|
||||||
elif [[ "$T" == "markdown" ]]; then
|
elif [[ "$T" == "markdown" ]]; then
|
||||||
python >$TMPFILE.trans2 <<EOF
|
python3 >$TMPFILE.trans2 <<EOF
|
||||||
import markdown2
|
import markdown2
|
||||||
print(markdown2.markdown_path('$TMPFILE.trans'))
|
print(markdown2.markdown_path('$TMPFILE.trans'))
|
||||||
EOF
|
EOF
|
||||||
@ -50,26 +103,62 @@ CONTENT=`cat $TMPFILE.trans`
|
|||||||
rm $TMPFILE.trans
|
rm $TMPFILE.trans
|
||||||
|
|
||||||
echo "--- START POST ---"
|
echo "--- START POST ---"
|
||||||
echo $CONTENT
|
echo "$CONTENT"
|
||||||
echo "--- END POST ---"
|
echo "--- END POST ---"
|
||||||
echo "Title: $TITLE"
|
echo "Title: $TITLE"
|
||||||
echo "User: $USER"
|
echo "User: $USER"
|
||||||
echo "Server: $SERVER"
|
echo "Server: $SERVER"
|
||||||
echo "Status: $STATUS"
|
echo "Status: $STATUS"
|
||||||
echo "Categories: $CATEGORIES"
|
CATEGORIES_DISPLAY="$CATEGORIES"
|
||||||
echo "Tags: $TAGS"
|
[[ ${#CATEGORY_NAMES[@]} -gt 0 ]] && CATEGORIES_DISPLAY="$CATEGORIES_DISPLAY (+ ${CATEGORY_NAMES[*]}, resolved/created on confirm)"
|
||||||
|
echo "Categories: $CATEGORIES_DISPLAY"
|
||||||
|
TAGS_DISPLAY="$TAGS"
|
||||||
|
[[ ${#TAG_NAMES[@]} -gt 0 ]] && TAGS_DISPLAY="$TAGS_DISPLAY (+ ${TAG_NAMES[*]}, resolved/created on confirm)"
|
||||||
|
echo "Tags: $TAGS_DISPLAY"
|
||||||
|
if [[ -n "$DATE" ]]; then
|
||||||
|
echo "Date: $DATE"
|
||||||
|
fi
|
||||||
|
if [[ -n "$POST_ID" ]]; then
|
||||||
|
echo "Editing post: $POST_ID"
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
read -p "Press enter to confirm..."
|
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
|
||||||
|
for NAME in "${CATEGORY_NAMES[@]}"; do
|
||||||
|
ID=$(wp_resolve_term "/wp-json/wp/v2/categories" "$NAME") || exit 1
|
||||||
|
CATEGORIES_JSON=$(jq -c --argjson id "$ID" '. + [$id] | unique' <<<"$CATEGORIES_JSON")
|
||||||
|
done
|
||||||
|
|
||||||
|
TAGS_JSON="[]"
|
||||||
|
if [[ -n "$TAGS" ]]; then
|
||||||
|
TAGS_JSON=$(echo "$TAGS" | tr ',' '\n' | jq -R 'select(length > 0) | tonumber' | jq -s '.')
|
||||||
|
fi
|
||||||
|
for NAME in "${TAG_NAMES[@]}"; do
|
||||||
|
ID=$(wp_resolve_term "/wp-json/wp/v2/tags" "$NAME") || exit 1
|
||||||
|
TAGS_JSON=$(jq -c --argjson id "$ID" '. + [$id] | unique' <<<"$TAGS_JSON")
|
||||||
|
done
|
||||||
|
|
||||||
|
PAYLOAD=$(jq -n \
|
||||||
|
--arg title "$TITLE" \
|
||||||
|
--arg content "$CONTENT" \
|
||||||
|
--arg status "$STATUS" \
|
||||||
|
--argjson categories "$CATEGORIES_JSON" \
|
||||||
|
--argjson tags "$TAGS_JSON" \
|
||||||
|
--arg date_gmt "${DATE%Z}" \
|
||||||
|
'{title: $title, content: $content, status: $status, categories: $categories, tags: $tags}
|
||||||
|
+ (if $date_gmt != "" then {date_gmt: $date_gmt} else {} end)')
|
||||||
|
|
||||||
|
POST_PATH="/wp-json/wp/v2/posts/"
|
||||||
|
[[ -n "$POST_ID" ]] && POST_PATH="/wp-json/wp/v2/posts/$POST_ID"
|
||||||
|
|
||||||
# push the post!
|
# push the post!
|
||||||
curl --user "$USER:$PASSWORD" -X POST \
|
BODY=$(wp_api_call POST "$POST_PATH" -H "Content-Type: application/json" --data "$PAYLOAD") || exit 1
|
||||||
--data-urlencode "title=$TITLE" \
|
echo "$BODY"
|
||||||
--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
|
|
||||||
|
|
||||||
# backup the posted data (temporarily until it is auto-removed)
|
# backup the posted data (temporarily until it is auto-removed)
|
||||||
mv $TMPFILE $TMPFILE.posted
|
mv $TMPFILE $TMPFILE.posted
|
||||||
|
|
||||||
|
|||||||
9
site.sh.template
Normal file
9
site.sh.template
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Per-site overrides, loaded after config.sh when running with --site NAME.
|
||||||
|
# Copy to ~/.config/wordpress-rest-curl/NAME.sh and fill in. Only set what
|
||||||
|
# differs from config.sh -- anything left out here falls back to config.sh.
|
||||||
|
USER="" # WP user to create the post
|
||||||
|
PASSWORD="" # application password generated for your WP user
|
||||||
|
SERVER="" # server hostname, optionally with subdirectories
|
||||||
|
CATEGORIES="" # comma separated integer IDs of categories
|
||||||
|
TAGS="" # comma separated integer IDs of tags
|
||||||
68
terms.sh
Executable file
68
terms.sh
Executable file
@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Manage WordPress categories and tags: list existing ones or create new
|
||||||
|
# ones, so post.sh's CATEGORIES/TAGS (numeric IDs) can be filled in.
|
||||||
|
|
||||||
|
USER="" # WP user
|
||||||
|
PASSWORD="" # application password generated for your WP user
|
||||||
|
SERVER="" # server hostname, optionally with subdirectories
|
||||||
|
USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) wordpress-rest-curl/1.0" # some hosts block curl's default UA as bot traffic
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/lib/config.sh"
|
||||||
|
|
||||||
|
# CLI options:
|
||||||
|
# --site NAME same config layering as post.sh
|
||||||
|
# --type TYPE 'category' (default) or 'tag'
|
||||||
|
# --list list existing terms as "id<TAB>name<TAB>slug<TAB>count"
|
||||||
|
# --create NAME create a new term, print its id
|
||||||
|
# --find NAME print the id of an existing term with an exact name
|
||||||
|
# match, or exit 1 if none exists
|
||||||
|
SITE=""
|
||||||
|
TYPE="category"
|
||||||
|
ACTION=""
|
||||||
|
NAME=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--site)
|
||||||
|
SITE="$2"; shift 2 ;;
|
||||||
|
--type)
|
||||||
|
TYPE="$2"; shift 2 ;;
|
||||||
|
--list)
|
||||||
|
ACTION="list"; shift ;;
|
||||||
|
--create)
|
||||||
|
ACTION="create"; NAME="$2"; shift 2 ;;
|
||||||
|
--find)
|
||||||
|
ACTION="find"; NAME="$2"; shift 2 ;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ "$TYPE" == "category" || "$TYPE" == "tag" ]] || { echo "Unknown --type: $TYPE (expected 'category' or 'tag')" >&2; exit 1; }
|
||||||
|
[[ -n "$ACTION" ]] || { echo "Specify one of --list, --create NAME, --find NAME" >&2; exit 1; }
|
||||||
|
|
||||||
|
wp_load_config
|
||||||
|
|
||||||
|
ENDPOINT_PATH="/wp-json/wp/v2/categories"
|
||||||
|
[[ "$TYPE" == "tag" ]] && ENDPOINT_PATH="/wp-json/wp/v2/tags"
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
list)
|
||||||
|
wp_api_list_all "$ENDPOINT_PATH" | jq -r '[.id, .name, .slug, .count] | @tsv'
|
||||||
|
;;
|
||||||
|
find)
|
||||||
|
FOUND_ID=$(wp_api_list_all "$ENDPOINT_PATH" | jq -r --arg name "$NAME" 'select(.name == $name) | .id' | head -n1)
|
||||||
|
if [[ -n "$FOUND_ID" ]]; then
|
||||||
|
echo "$FOUND_ID"
|
||||||
|
else
|
||||||
|
echo "No $TYPE named '$NAME' found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
create)
|
||||||
|
PAYLOAD=$(jq -n --arg name "$NAME" '{name: $name}')
|
||||||
|
BODY=$(wp_api_call POST "$ENDPOINT_PATH" -H "Content-Type: application/json" --data "$PAYLOAD") || exit 1
|
||||||
|
echo "$BODY" | jq -r '.id'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
121
tests/mock_wp_server.py
Executable file
121
tests/mock_wp_server.py
Executable file
@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Minimal mock of the WordPress REST API, for testing post.sh and terms.sh
|
||||||
|
# without a real WordPress install.
|
||||||
|
#
|
||||||
|
# Handles POST /wp-json/wp/v2/posts(/ID) same as before, plus
|
||||||
|
# GET/POST /wp-json/wp/v2/categories and /wp-json/wp/v2/tags.
|
||||||
|
#
|
||||||
|
# Behavior is controlled by files (paths given via env vars):
|
||||||
|
# MOCK_WP_CONTROL_FILE - optional JSON {"status": int, "body": {...}} that
|
||||||
|
# determines the response to the next POST /posts 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": ..., "user_agent": ...} so a test can assert
|
||||||
|
# on what was sent.
|
||||||
|
# MOCK_WP_TERMS_FILE - JSON {"categories": [...], "tags": [...]}, used as
|
||||||
|
# the backing store for the categories/tags endpoints. Created terms
|
||||||
|
# are appended and persisted back to this file.
|
||||||
|
#
|
||||||
|
# Listens on 127.0.0.1:$MOCK_WP_PORT (default 8899).
|
||||||
|
|
||||||
|
import http.server
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
|
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"]
|
||||||
|
TERMS_FILE = os.environ["MOCK_WP_TERMS_FILE"]
|
||||||
|
|
||||||
|
TERM_TYPES = {
|
||||||
|
"/wp-json/wp/v2/categories": "categories",
|
||||||
|
"/wp-json/wp/v2/tags": "tags",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def slugify(name):
|
||||||
|
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
||||||
|
|
||||||
|
|
||||||
|
def load_terms():
|
||||||
|
if os.path.exists(TERMS_FILE):
|
||||||
|
with open(TERMS_FILE) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {"categories": [], "tags": []}
|
||||||
|
|
||||||
|
|
||||||
|
def save_terms(terms):
|
||||||
|
with open(TERMS_FILE, "w") as f:
|
||||||
|
json.dump(terms, f)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(http.server.BaseHTTPRequestHandler):
|
||||||
|
def _write_json(self, status, body):
|
||||||
|
payload = json.dumps(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 _record_request(self, path, data):
|
||||||
|
with open(REQUEST_FILE, "w") as f:
|
||||||
|
json.dump({
|
||||||
|
"path": path,
|
||||||
|
"payload": data,
|
||||||
|
"user_agent": self.headers.get("User-Agent", ""),
|
||||||
|
}, f)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
parsed = urlparse(self.path)
|
||||||
|
term_type = TERM_TYPES.get(parsed.path)
|
||||||
|
if term_type is None:
|
||||||
|
self._write_json(404, {"code": "not_found"})
|
||||||
|
return
|
||||||
|
|
||||||
|
qs = parse_qs(parsed.query)
|
||||||
|
per_page = int(qs.get("per_page", ["10"])[0])
|
||||||
|
page = int(qs.get("page", ["1"])[0])
|
||||||
|
|
||||||
|
terms = load_terms()[term_type]
|
||||||
|
start = (page - 1) * per_page
|
||||||
|
self._write_json(200, terms[start:start + per_page])
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
parsed = urlparse(self.path)
|
||||||
|
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")}
|
||||||
|
|
||||||
|
self._record_request(self.path, data)
|
||||||
|
|
||||||
|
term_type = TERM_TYPES.get(parsed.path)
|
||||||
|
if term_type is not None:
|
||||||
|
terms = load_terms()
|
||||||
|
next_id = max([t["id"] for t in terms[term_type]], default=0) + 1
|
||||||
|
name = data.get("name", "")
|
||||||
|
term = {"id": next_id, "name": name, "slug": slugify(name), "count": 0}
|
||||||
|
terms[term_type].append(term)
|
||||||
|
save_terms(terms)
|
||||||
|
self._write_json(201, term)
|
||||||
|
return
|
||||||
|
|
||||||
|
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)
|
||||||
|
self._write_json(status, resp_body)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
http.server.HTTPServer(("127.0.0.1", PORT), Handler).serve_forever()
|
||||||
209
tests/run_tests.sh
Executable file
209
tests/run_tests.sh
Executable file
@ -0,0 +1,209 @@
|
|||||||
|
#!/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"
|
||||||
|
export MOCK_WP_TERMS_FILE="$WORKDIR/terms.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
|
||||||
|
# Deliberately sparse: only the fields that differ from config.sh, to prove
|
||||||
|
# --site layers on top of the default config rather than replacing it.
|
||||||
|
cat > "$HOME/.config/wordpress-rest-curl/othersite.sh" <<EOF
|
||||||
|
USER="other-tester"
|
||||||
|
PASSWORD="other-pass"
|
||||||
|
CATEGORIES="1"
|
||||||
|
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" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_terms_sh() {
|
||||||
|
"$PWD/terms.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")"
|
||||||
|
assert_eq "create: sends a non-default user agent" "true" "$(jq -r '.user_agent != "" and (.user_agent | test("^curl/") | not)' <<<"$REQ")"
|
||||||
|
|
||||||
|
# --- USER_AGENT is configurable, e.g. to dodge a host's DDoS protection ---
|
||||||
|
rm -f "$MOCK_WP_REQUEST_FILE" "$MOCK_WP_CONTROL_FILE"
|
||||||
|
cat >> "$HOME/.config/wordpress-rest-curl/config.sh" <<'EOF3'
|
||||||
|
USER_AGENT="Mozilla/5.0 (test override)"
|
||||||
|
EOF3
|
||||||
|
run_post_sh --file "$POST_FILE" >/dev/null
|
||||||
|
assert_eq "user agent: exit code" "0" "$?"
|
||||||
|
REQ=$(cat "$MOCK_WP_REQUEST_FILE")
|
||||||
|
assert_eq "user agent: override from config is sent" "Mozilla/5.0 (test override)" "$(jq -r '.user_agent' <<<"$REQ")"
|
||||||
|
|
||||||
|
# --- trailing slash on SERVER doesn't produce a double slash in the URL ---
|
||||||
|
rm -f "$MOCK_WP_REQUEST_FILE" "$MOCK_WP_CONTROL_FILE"
|
||||||
|
cat >> "$HOME/.config/wordpress-rest-curl/config.sh" <<EOF3
|
||||||
|
SERVER="http://127.0.0.1:$PORT/"
|
||||||
|
EOF3
|
||||||
|
run_post_sh --file "$POST_FILE" >/dev/null
|
||||||
|
assert_eq "trailing slash: exit code" "0" "$?"
|
||||||
|
REQ=$(cat "$MOCK_WP_REQUEST_FILE")
|
||||||
|
assert_eq "trailing slash: path has no double slash" "/wp-json/wp/v2/posts/" "$(jq -r '.path' <<<"$REQ")"
|
||||||
|
|
||||||
|
# --- --date backdates the post via date_gmt ---
|
||||||
|
rm -f "$MOCK_WP_REQUEST_FILE" "$MOCK_WP_CONTROL_FILE"
|
||||||
|
run_post_sh --file "$POST_FILE" --date "2026-07-27T10:00:00Z" >/dev/null
|
||||||
|
assert_eq "date: exit code" "0" "$?"
|
||||||
|
REQ=$(cat "$MOCK_WP_REQUEST_FILE")
|
||||||
|
assert_eq "date: date_gmt sent, trailing Z stripped" "2026-07-27T10:00:00" "$(jq -r '.payload.date_gmt' <<<"$REQ")"
|
||||||
|
|
||||||
|
rm -f "$MOCK_WP_REQUEST_FILE" "$MOCK_WP_CONTROL_FILE"
|
||||||
|
run_post_sh --file "$POST_FILE" >/dev/null
|
||||||
|
REQ=$(cat "$MOCK_WP_REQUEST_FILE")
|
||||||
|
assert_eq "no date: date_gmt omitted" "true" "$(jq -r '.payload | has("date_gmt") | not' <<<"$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")"
|
||||||
|
|
||||||
|
# --- --site layers on top of config.sh: overrides what it sets, inherits the rest ---
|
||||||
|
rm -f "$MOCK_WP_REQUEST_FILE" "$MOCK_WP_CONTROL_FILE"
|
||||||
|
run_post_sh --site othersite --file "$POST_FILE" >/dev/null
|
||||||
|
assert_eq "site: exit code" "0" "$?"
|
||||||
|
REQ=$(cat "$MOCK_WP_REQUEST_FILE")
|
||||||
|
assert_eq "site: categories overridden by site config" "[1]" "$(jq -c '.payload.categories' <<<"$REQ")"
|
||||||
|
assert_eq "site: tags inherited from default config" "[10]" "$(jq -c '.payload.tags' <<<"$REQ")"
|
||||||
|
assert_eq "site: title transform inherited from default config" "My Test Title" "$(jq -r '.payload.title' <<<"$REQ")"
|
||||||
|
|
||||||
|
rm -f "$MOCK_WP_REQUEST_FILE" "$MOCK_WP_CONTROL_FILE"
|
||||||
|
OUT=$(run_post_sh --site does-not-exist --file "$POST_FILE" 2>&1)
|
||||||
|
assert_eq "site: unknown site exits non-zero" "1" "$?"
|
||||||
|
assert_eq "site: unknown site error message" "true" "$(grep -q "Config file not found" <<<"$OUT" && echo true || echo false)"
|
||||||
|
|
||||||
|
# --- no --site and no default config.sh: clean error, not a crash ---
|
||||||
|
mv "$HOME/.config/wordpress-rest-curl/config.sh" "$WORKDIR/config.sh.bak"
|
||||||
|
OUT=$(run_post_sh --file "$POST_FILE" 2>&1)
|
||||||
|
assert_eq "no config: exits non-zero" "1" "$?"
|
||||||
|
assert_eq "no config: error message" "true" "$(grep -q "No site specified and no default config found" <<<"$OUT" && echo true || echo false)"
|
||||||
|
mv "$WORKDIR/config.sh.bak" "$HOME/.config/wordpress-rest-curl/config.sh"
|
||||||
|
|
||||||
|
# --- 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"
|
||||||
|
|
||||||
|
# --- terms.sh: list starts empty ---
|
||||||
|
rm -f "$MOCK_WP_TERMS_FILE"
|
||||||
|
OUT=$(run_terms_sh --list)
|
||||||
|
assert_eq "terms list: exit code" "0" "$?"
|
||||||
|
assert_eq "terms list: empty when no categories exist" "" "$OUT"
|
||||||
|
|
||||||
|
# --- terms.sh: create a category, then find and list it ---
|
||||||
|
OUT=$(run_terms_sh --create "Hardware")
|
||||||
|
assert_eq "terms create: exit code" "0" "$?"
|
||||||
|
assert_eq "terms create: prints the new id" "1" "$OUT"
|
||||||
|
|
||||||
|
OUT=$(run_terms_sh --find "Hardware")
|
||||||
|
assert_eq "terms find: exit code" "0" "$?"
|
||||||
|
assert_eq "terms find: resolves name to id" "1" "$OUT"
|
||||||
|
|
||||||
|
OUT=$(run_terms_sh --list)
|
||||||
|
assert_eq "terms list: shows created category" "1 Hardware hardware 0" "$OUT"
|
||||||
|
|
||||||
|
# --- terms.sh: categories and tags are separate namespaces ---
|
||||||
|
OUT=$(run_terms_sh --type tag --create "arduino")
|
||||||
|
assert_eq "terms create tag: exit code" "0" "$?"
|
||||||
|
assert_eq "terms create tag: gets its own id sequence" "1" "$OUT"
|
||||||
|
|
||||||
|
OUT=$(run_terms_sh --type tag --list)
|
||||||
|
assert_eq "terms list tags: shows only the tag, not the category" "1 arduino arduino 0" "$OUT"
|
||||||
|
|
||||||
|
# --- terms.sh: --find on a name that doesn't exist ---
|
||||||
|
OUT=$(run_terms_sh --find "Does Not Exist" 2>&1)
|
||||||
|
assert_eq "terms find missing: exits non-zero" "1" "$?"
|
||||||
|
assert_eq "terms find missing: error message" "true" "$(grep -q "No category named" <<<"$OUT" && echo true || echo false)"
|
||||||
|
|
||||||
|
# --- post.sh: --category/--tag by name resolve to an existing term, merged with config IDs ---
|
||||||
|
rm -f "$MOCK_WP_REQUEST_FILE" "$MOCK_WP_CONTROL_FILE"
|
||||||
|
run_post_sh --file "$POST_FILE" --category "Hardware" >/dev/null
|
||||||
|
assert_eq "post category resolve: exit code" "0" "$?"
|
||||||
|
REQ=$(cat "$MOCK_WP_REQUEST_FILE")
|
||||||
|
assert_eq "post category resolve: merges existing id with config CATEGORIES" "[1,3,7]" "$(jq -c '.payload.categories | sort' <<<"$REQ")"
|
||||||
|
assert_eq "post category resolve: does not create a duplicate" "1" "$(run_terms_sh --list | wc -l)"
|
||||||
|
|
||||||
|
# --- post.sh: --category/--tag by a new name creates it, then attaches it ---
|
||||||
|
rm -f "$MOCK_WP_REQUEST_FILE" "$MOCK_WP_CONTROL_FILE"
|
||||||
|
run_post_sh --file "$POST_FILE" --category "Software" --tag "midi" >/dev/null
|
||||||
|
assert_eq "post category create: exit code" "0" "$?"
|
||||||
|
NEW_CAT_ID=$(run_terms_sh --find "Software")
|
||||||
|
NEW_TAG_ID=$(run_terms_sh --type tag --find "midi")
|
||||||
|
REQ=$(cat "$MOCK_WP_REQUEST_FILE")
|
||||||
|
assert_eq "post category create: includes the newly created category" "true" "$(jq --argjson id "$NEW_CAT_ID" '.payload.categories | index($id) != null' <<<"$REQ")"
|
||||||
|
assert_eq "post tag create: includes the newly created tag" "true" "$(jq --argjson id "$NEW_TAG_ID" '.payload.tags | index($id) != null' <<<"$REQ")"
|
||||||
|
|
||||||
|
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