diff --git a/tests/Integration/mcp-smoke.sh b/tests/Integration/mcp-smoke.sh new file mode 100644 index 0000000..92fdc23 --- /dev/null +++ b/tests/Integration/mcp-smoke.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# End-to-end checks of the OAuth-protected MCP endpoint. +# Runs INSIDE the DDEV web container (wp-cli and curl available there): +# cd /home/janniclas/Projekte/WordPress && ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/mcp-smoke.sh +set -euo pipefail + +PLUGIN=wp-content/plugins/WP-LIT-MCP +ENDPOINT=http://localhost/wp-json/mcp/mcp-oauth-server +cd /var/www/html + +fail() { echo "FAIL: $*"; exit 1; } +pass() { echo "ok - $*"; } + +# --- Cleanup: remove every "lit-mcp-integration-test" application password +# this script mints (for every user it mints one for) and the throwaway +# subscriber it creates. Registered up front so it runs on any exit path +# (success, fail(), or an unexpected error under `set -e`). +ADMIN_LOGIN="" + +cleanup_app_passwords_for() { # $1 = user_login + local login="$1" + [ -n "$login" ] || return 0 + local escaped=${login//\'/\\\'} + wp eval " +\$u = get_user_by('login', '${escaped}'); +if (\$u) { + foreach (WP_Application_Passwords::get_user_application_passwords(\$u->ID) as \$pw) { + if ((\$pw['name'] ?? '') === 'lit-mcp-integration-test') { + WP_Application_Passwords::delete_application_password(\$u->ID, \$pw['uuid']); + } + } +} +" >/dev/null 2>&1 || true +} + +cleanup() { + cleanup_app_passwords_for "$ADMIN_LOGIN" + cleanup_app_passwords_for lit-smoke-sub + wp user delete lit-smoke-sub --yes >/dev/null 2>&1 || true +} +trap cleanup EXIT + +mcp() { # $1 token, $2 json body, $3 session id (optional) -> prints headers+body + local session_header=() + [ -n "${3:-}" ] && session_header=(-H "Mcp-Session-Id: $3") + curl -s -i -X POST "$ENDPOINT" \ + -H "Authorization: Bearer $1" \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + "${session_header[@]}" -d "$2" +} + +# 1. Discovery documents +curl -s http://localhost/.well-known/oauth-protected-resource | grep -q '"resource"' \ + || fail "protected-resource metadata missing" +pass "RFC 9728 protected-resource metadata served" +curl -s http://localhost/.well-known/oauth-authorization-server | grep -q 'client_id_metadata_document_supported' \ + || fail "authorization-server metadata missing CIMD flag" +pass "RFC 8414 authorization-server metadata advertises CIMD" + +# 2. Unauthenticated request -> 401 with WWW-Authenticate +UNAUTH=$(curl -s -i -X POST "$ENDPOINT" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}') +echo "$UNAUTH" | head -1 | grep -q ' 401' || fail "expected 401 without token" +echo "$UNAUTH" | grep -qi '^www-authenticate: Bearer' || fail "missing WWW-Authenticate header" +pass "401 + WWW-Authenticate without token" + +# 3. Administrator: initialize, tools/list, tools/call +ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1) +TOKEN=$(wp eval-file "$PLUGIN/tests/Integration/mint-token.php" "$ADMIN_LOGIN") +INIT=$(mcp "$TOKEN" '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}') +SESSION=$(echo "$INIT" | grep -i '^mcp-session-id:' | awk '{print $2}' | tr -d '\r') +echo "$INIT" | grep -q 'LIT MCP pflegt' || fail "instructions missing in initialize result" +pass "initialize returns LIT instructions (session: ${SESSION:-none})" + +LIST=$(mcp "$TOKEN" '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' "$SESSION") +for tool in lit-guide lit-site-status mcp-adapter-execute-ability; do + echo "$LIST" | grep -q "\"$tool\"" || fail "tools/list lacks $tool" +done +pass "tools/list contains direct tools and meta-tools" + +CALL=$(mcp "$TOKEN" '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"lit-site-status","arguments":{}}}' "$SESSION") +echo "$CALL" | grep -q '"isError":false' || fail "lit-site-status failed: $CALL" +echo "$CALL" | grep -q 'the-events-calendar' || fail "site-status lacks components" +pass "tools/call lit-site-status works" + +# 4. Token must not authenticate other REST routes (mcp-oauth issue #62) +ME=$(curl -s -o /dev/null -w '%{http_code}' http://localhost/wp-json/wp/v2/users/me -H "Authorization: Bearer $TOKEN") +[ "$ME" = "401" ] || fail "MCP token authenticates /wp/v2/users/me (HTTP $ME) – scope filter needed" +pass "MCP token is rejected on /wp/v2/users/me" + +# 5. Role gate: subscriber has a valid token but must not use tools +wp user create lit-smoke-sub lit-smoke-sub@example.invalid --role=subscriber >/dev/null +SUB_TOKEN=$(wp eval-file "$PLUGIN/tests/Integration/mint-token.php" lit-smoke-sub) +SUB_INIT=$(mcp "$SUB_TOKEN" '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}') +SUB_SESSION=$(echo "$SUB_INIT" | grep -i '^mcp-session-id:' | awk '{print $2}' | tr -d '\r') +SUB_CALL=$(mcp "$SUB_TOKEN" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"lit-site-status","arguments":{}}}' "$SUB_SESSION") +echo "$SUB_CALL" | grep -q '"isError":true\|"error"' || fail "subscriber could call lit-site-status: $SUB_CALL" +SUB_META=$(mcp "$SUB_TOKEN" '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"mcp-adapter-execute-ability","arguments":{"ability_name":"lit/site-status","parameters":{}}}}' "$SUB_SESSION") +echo "$SUB_META" | grep -q '"isError":true\|"error"' || fail "subscriber could use execute-ability: $SUB_META" +pass "role gate blocks subscriber (direct tool and meta-tool)" + +echo "ALL CHECKS PASSED" diff --git a/tests/Integration/mint-token.php b/tests/Integration/mint-token.php new file mode 100644 index 0000000..6d7907d --- /dev/null +++ b/tests/Integration/mint-token.php @@ -0,0 +1,39 @@ + + * + * Note: no `declare(strict_types=1);` here — PHP forbids that declaration + * inside eval()'d code, and `wp eval-file` runs this file through eval(). + * Confirmed with a minimal repro during Task 7 (see task-7-report.md). + */ + +use WPMedia\MCP\OAuth\Auth\JWT; +use WPMedia\MCP\OAuth\Auth\SecretManager; + +$login = $args[0] ?? ''; +$user = get_user_by('login', $login); +if (! $user) { + fwrite(STDERR, "unknown user {$login}\n"); + exit(1); +} + +[, $item] = WP_Application_Passwords::create_new_application_password( + $user->ID, + ['name' => 'lit-mcp-integration-test', 'app_id' => 'lit-mcp-integration-test'] +); + +$now = time(); +echo JWT::encode([ + 'iss' => home_url(), + 'aud' => get_rest_url(null, 'mcp/mcp-oauth-server'), + 'sub' => (string) $user->ID, + 'app_pass_id' => $item['uuid'], + 'client_id' => 'lit-mcp-integration-test', + 'scope' => 'mcp', + 'iat' => $now, + 'exp' => $now + 600, +], SecretManager::get_secret());