#!/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 'rc=$?; cleanup; guard_exit "$rc" || exit $?' 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" for internal in "http://169.254.169.254/latest/meta-data/" "http://100.64.0.1/x.png" "http://[::ffff:127.0.0.1]/x.png"; do BLOCKED=$(tool lit-media-upload-from-url "$(jq -cn --arg u "$internal" '{url: $u}')") [ "$(error_code "$BLOCKED")" = "validation_failed" ] || fail "internal URL $internal accepted: $BLOCKED" done FAKE=$(ability lit/media-upload "{\"filename\":\"evil.png\",\"data\":\"$(printf '/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" finish