Files
WP-LIT-MCP/tests/Integration/media-admin-smoke.sh
T

146 lines
7.6 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"
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"
ability lit/media-download "{\"id\":$MEDIA_ID}" | jq -e '.type == "resource" and .resource.mimeType == "image/png"' >/dev/null \
|| fail "media-download"
pass "upload, search, preview, update (single ok, mixed batch refused), download"
# 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"