Get and auto-create categories and tags
This commit is contained in:
+80
-18
@@ -1,28 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
# Minimal mock of the WordPress REST API /wp/v2/posts endpoint, for testing
|
||||
# post.sh without a real WordPress install.
|
||||
# Minimal mock of the WordPress REST API, for testing post.sh and terms.sh
|
||||
# without a real WordPress install.
|
||||
#
|
||||
# Behavior is controlled by two files (paths given via env vars):
|
||||
# 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 request. If absent, responds
|
||||
# 201 with the submitted fields echoed back under an "id".
|
||||
# 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:
|
||||
@@ -30,12 +92,18 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
except json.JSONDecodeError:
|
||||
data = {"_raw": body.decode(errors="replace")}
|
||||
|
||||
with open(REQUEST_FILE, "w") as f:
|
||||
json.dump({
|
||||
"path": self.path,
|
||||
"payload": data,
|
||||
"user_agent": self.headers.get("User-Agent", ""),
|
||||
}, f)
|
||||
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):
|
||||
@@ -43,13 +111,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
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)
|
||||
self._write_json(status, resp_body)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
@@ -16,6 +16,7 @@ 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=$!
|
||||
@@ -53,6 +54,10 @@ run_post_sh() {
|
||||
printf "" | "$PWD/post.sh" "$@"
|
||||
}
|
||||
|
||||
run_terms_sh() {
|
||||
"$PWD/terms.sh" "$@"
|
||||
}
|
||||
|
||||
FAIL=0
|
||||
assert_eq() {
|
||||
local desc="$1" expected="$2" actual="$3"
|
||||
@@ -146,6 +151,55 @@ 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."
|
||||
|
||||
Reference in New Issue
Block a user