153 lines
8.3 KiB
Bash
Executable File
153 lines
8.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# End-to-end checks of Etappe 1b: media tools, exposure policy, visibility for users
|
|
# without access, settings page rendering, change log.
|
|
# Runs INSIDE the DDEV web container:
|
|
# cd /home/janniclas/Projekte/WordPress && timeout 600 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/media-admin-smoke.sh
|
|
set -euo pipefail
|
|
cd /var/www/html
|
|
# shellcheck source=lib.sh
|
|
source wp-content/plugins/WP-LIT-MCP/tests/Integration/lib.sh
|
|
|
|
ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1)
|
|
ORIGINAL_EXPOSURE=$(wp option get lit_mcp_exposure --format=json 2>/dev/null || true)
|
|
CREATED_POSTS=()
|
|
# 1x1 PNG
|
|
PNG='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='
|
|
|
|
cleanup() {
|
|
for id in "${CREATED_POSTS[@]}"; do
|
|
wp post delete "$id" --force >/dev/null 2>&1 || true
|
|
done
|
|
if [ -n "$ORIGINAL_EXPOSURE" ]; then
|
|
wp option update lit_mcp_exposure "$ORIGINAL_EXPOSURE" --format=json >/dev/null 2>&1 || true
|
|
else
|
|
wp option delete lit_mcp_exposure >/dev/null 2>&1 || true
|
|
fi
|
|
cleanup_app_passwords_for "$ADMIN_LOGIN"
|
|
cleanup_app_passwords_for lit-smoke-sub2
|
|
wp user delete lit-smoke-sub2 --yes >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
connect "$ADMIN_LOGIN"
|
|
|
|
# 1. Direct media tools; image tool without outputSchema
|
|
LIST=$(rpc tools/list '{}')
|
|
for t in lit-media-search lit-media-preview lit-media-download lit-media-upload-from-url; do
|
|
echo "$LIST" | jq -e --arg t "$t" '.result.tools[] | select(.name == $t)' >/dev/null || fail "tools/list lacks $t"
|
|
done
|
|
echo "$LIST" | jq -e '.result.tools[] | select(.name == "lit-media-preview") | has("outputSchema") | not' >/dev/null \
|
|
|| fail "lit-media-preview must not declare an outputSchema"
|
|
pass "direct media tools listed"
|
|
|
|
# 2. Base64 upload, search, preview, metadata update, download
|
|
UPLOAD=$(ability lit/media-upload "{\"filename\":\"lit-smoke.png\",\"data\":\"$PNG\",\"title\":\"LIT Smoke Bild\"}")
|
|
MEDIA_ID=$(echo "$UPLOAD" | jq -r '.attachment.id // empty')
|
|
[ -n "$MEDIA_ID" ] || fail "media-upload: $UPLOAD"
|
|
CREATED_POSTS+=("$MEDIA_ID")
|
|
tool lit-media-search '{"search":"LIT Smoke","missing_alt":true}' \
|
|
| jq -e --argjson id "$MEDIA_ID" '.items[] | select(.id == $id)' >/dev/null || fail "missing_alt search"
|
|
# The file name (lit-smoke.png) is searched too, not only title/caption ("LIT Smoke Bild").
|
|
tool lit-media-search '{"search":"lit-smoke"}' \
|
|
| jq -e --argjson id "$MEDIA_ID" '.items[] | select(.id == $id)' >/dev/null || fail "file-name search"
|
|
PREVIEW=$(rpc tools/call "{\"name\":\"lit-media-preview\",\"arguments\":{\"id\":$MEDIA_ID}}")
|
|
echo "$PREVIEW" | jq -e '.result.content[0].type == "image" and (.result.content[0].data | length) > 0' >/dev/null \
|
|
|| fail "media-preview: $PREVIEW"
|
|
# media-update is all-or-nothing (spec/task 5): a valid single item succeeds and is stored...
|
|
UPDATE=$(ability lit/media-update "{\"items\":[{\"id\":$MEDIA_ID,\"alt\":\"Testbild\"}]}")
|
|
echo "$UPDATE" | jq -e '.results[0].ok == true' >/dev/null || fail "media-update: $UPDATE"
|
|
[ "$(wp post meta get "$MEDIA_ID" _wp_attachment_image_alt)" = "Testbild" ] || fail "alt text not stored"
|
|
# ...but a batch with one unknown id refuses the WHOLE batch (nothing partially applied).
|
|
BATCH_FAIL=$(ability lit/media-update "{\"items\":[{\"id\":$MEDIA_ID,\"alt\":\"geaendert\"},{\"id\":999999,\"alt\":\"x\"}]}")
|
|
[ "$(error_code "$BATCH_FAIL")" = "not_found" ] || fail "media-update batch with unknown id should refuse entirely: $BATCH_FAIL"
|
|
[ "$(wp post meta get "$MEDIA_ID" _wp_attachment_image_alt)" = "Testbild" ] \
|
|
|| fail "media-update batch refusal must not have changed anything"
|
|
DOWNLOAD=$(rpc tools/call "{\"name\":\"lit-media-download\",\"arguments\":{\"id\":$MEDIA_ID}}")
|
|
echo "$DOWNLOAD" | jq -e '.result.content[0].type == "resource" and .result.content[0].resource.mimeType == "image/png"
|
|
and (.result.content[0].resource.blob | length) > 0' >/dev/null || fail "media-download: $DOWNLOAD"
|
|
# Direct-only tools are not public for the meta-tools (their results only work as direct tools).
|
|
DOWNLOAD_META=$(ability lit/media-download "{\"id\":$MEDIA_ID}")
|
|
echo "$DOWNLOAD_META" | jq -e 'has("__error")' >/dev/null || fail "media-download via execute-ability: $DOWNLOAD_META"
|
|
pass "upload, search, preview, update (single ok, mixed batch refused), download (direct only)"
|
|
|
|
# 3. Import protection
|
|
SSRF=$(tool lit-media-upload-from-url '{"url":"http://127.0.0.1/wp-login.php"}')
|
|
[ "$(error_code "$SSRF")" = "validation_failed" ] || fail "internal URL accepted: $SSRF"
|
|
FAKE=$(ability lit/media-upload "{\"filename\":\"evil.png\",\"data\":\"$(printf '<?php echo 1;' | base64 -w0)\"}")
|
|
[ "$(error_code "$FAKE")" = "validation_failed" ] || fail "disguised PHP accepted: $FAKE"
|
|
EXTERNAL=$(tool lit-media-upload-from-url '{"url":"https://s.w.org/images/wmark.png","title":"LIT Smoke Extern"}')
|
|
EXTERNAL_ID=$(echo "$EXTERNAL" | jq -r '.attachment.id // empty')
|
|
if [ -n "$EXTERNAL_ID" ]; then
|
|
CREATED_POSTS+=("$EXTERNAL_ID")
|
|
pass "import from public URL"
|
|
else
|
|
echo "WARN - import from public URL failed (no internet in container?): $EXTERNAL"
|
|
fi
|
|
pass "internal URLs and disguised files rejected"
|
|
|
|
# 4. Usage check before delete
|
|
PAGE=$(tool lit-content-create "{\"post_type\":\"page\",\"title\":\"LIT Smoke Medien\",\"featured_image\":$MEDIA_ID}")
|
|
PAGE_ID=$(echo "$PAGE" | jq -r '.changed[0].id // empty')
|
|
[ -n "$PAGE_ID" ] || fail "page with featured image: $PAGE"
|
|
CREATED_POSTS+=("$PAGE_ID")
|
|
ability lit/media-get "{\"id\":$MEDIA_ID}" | jq -e '.usages[] | select(.where == "featured")' >/dev/null \
|
|
|| fail "usage not found"
|
|
NOCONFIRM=$(ability lit/media-delete "{\"id\":$MEDIA_ID}")
|
|
[ "$(error_code "$NOCONFIRM")" = "confirm_required" ] || fail "delete without confirm: $NOCONFIRM"
|
|
INUSE=$(ability lit/media-delete "{\"id\":$MEDIA_ID,\"confirm\":true}")
|
|
[ "$(error_code "$INUSE")" = "in_use" ] || fail "delete of used media without force: $INUSE"
|
|
ability lit/media-delete "{\"id\":$MEDIA_ID,\"confirm\":true,\"force\":true}" | jq -e '.ok == true' >/dev/null \
|
|
|| fail "forced delete"
|
|
pass "media delete: confirm, in_use, force"
|
|
|
|
# 5. Exposure policy
|
|
wp option update lit_mcp_exposure '{"lit/content-query":"indirect","lit/menu-list":"disabled"}' --format=json >/dev/null
|
|
LIST2=$(rpc tools/list '{}')
|
|
echo "$LIST2" | jq -e '.result.tools[] | select(.name == "lit-content-query")' >/dev/null \
|
|
&& fail "lit-content-query still direct"
|
|
ability lit/content-query '{"post_type":"page","limit":1}' | jq -e 'has("items")' >/dev/null \
|
|
|| fail "indirect content-query not executable"
|
|
ability lit/menu-list '{}' | jq -e 'has("__error")' >/dev/null || fail "disabled ability still executable"
|
|
wp option delete lit_mcp_exposure >/dev/null
|
|
pass "exposure policy: indirect and disabled"
|
|
|
|
# 6. Users without access see neither tools nor site details
|
|
wp user create lit-smoke-sub2 lit-smoke-sub2@example.invalid --role=subscriber >/dev/null
|
|
ADMIN_TOKEN=$TOKEN
|
|
ADMIN_SESSION=$SESSION
|
|
connect lit-smoke-sub2
|
|
rpc tools/list '{}' | jq -e '.result.tools | length == 0' >/dev/null || fail "subscriber sees tools"
|
|
echo "$INIT_RESPONSE" | grep -q 'nicht freigegeben' || fail "subscriber instructions lack the access hint"
|
|
echo "$INIT_RESPONSE" | grep -q 'Inhaltstypen' && fail "subscriber instructions leak site details"
|
|
TOKEN=$ADMIN_TOKEN
|
|
SESSION=$ADMIN_SESSION
|
|
pass "no tools and no site details for users without access"
|
|
|
|
# 7. Settings page renders every tab
|
|
RENDER=$(wp eval-file "$PLUGIN/tests/Integration/admin-render.php")
|
|
for tab in verbindung zugriff tools protokoll limits; do
|
|
echo "$RENDER" | grep -q "^$tab ok" || fail "settings tab $tab: $RENDER"
|
|
done
|
|
pass "settings page tabs render"
|
|
|
|
# 8. Change log
|
|
# Not `wp db query` here (as in wp-basis-smoke.sh): its write/read detection greps the raw SQL
|
|
# text for keywords like UPDATE/DELETE with no word boundary, so it misfires on the *values*
|
|
# 'lit/media-update' and 'lit/media-delete' and returns "Rows affected: -1" instead of the
|
|
# SELECT result. `wp eval` with a prepared $wpdb query sidesteps that entirely.
|
|
ROWS=$(wp eval '
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . "lit_mcp_log";
|
|
echo (int) $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$table} WHERE ability IN (%s, %s) AND client = %s"
|
|
. " AND created_at > (UTC_TIMESTAMP() - INTERVAL 10 MINUTE)",
|
|
"lit/media-upload",
|
|
"lit/media-delete",
|
|
"lit-mcp-integration-test"
|
|
));
|
|
')
|
|
[ "$ROWS" -ge 2 ] || fail "media changes not logged (found $ROWS)"
|
|
pass "media changes logged"
|
|
|
|
echo "ALL CHECKS PASSED"
|