diff --git a/dabestaniha-authenticate-bridge.php b/dabestaniha-authenticate-bridge.php index aa01930..59a071e 100644 --- a/dabestaniha-authenticate-bridge.php +++ b/dabestaniha-authenticate-bridge.php @@ -28,5 +28,3 @@ add_action('init', fn() => AuthenticateController::resolve()); add_action('admin_menu', fn() => SettingsPageMenuController::resolve()); add_action('admin_init', fn() => SettingsPageFormController::resolve()); - -echo true; diff --git a/src/app/Http/Controllers/AuthenticateController.php b/src/app/Http/Controllers/AuthenticateController.php index 93c0d57..dfb5356 100644 --- a/src/app/Http/Controllers/AuthenticateController.php +++ b/src/app/Http/Controllers/AuthenticateController.php @@ -54,12 +54,13 @@ class AuthenticateController extends InvocableController } $random_password = wp_generate_password(12, true); $username = $identifier_value; + $email = $identifier_name === 'email' ? $identifier_value : ''; if (username_exists($username)) { $username .= '_'.wp_generate_password(4, false); } - $user_id = wp_create_user($username, $random_password); + $user_id = wp_create_user($username, $random_password, $email); if (is_wp_error($user_id)) { wp_die(view('invalid-token'), 'Invalid Token', ['response' => 500]); diff --git a/src/helpers.php b/src/helpers.php index c335b16..0c318fc 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -1,10 +1,13 @@ get_error_message()); + } + + assert_true($activationOutput === '', 'Plugin activation produced unexpected output: '.$activationOutput); + } + + assert_true(is_plugin_active(TEST_PLUGIN), 'Plugin was not activated.'); + + update_option('mahak_jwt_secret', TEST_SECRET); + update_option('mahak_user_identifier', 'email'); + update_option('mahak_login_route', 'mahak/login'); + update_option('mahak_after_login_route', 'wp-admin/profile.php'); + + pass('Plugin activated and configured'); +} + +function base64_url_encode(string $value): string +{ + return rtrim(strtr(base64_encode($value), '+/', '-_'), '='); +} + +function make_jwt(array $payload, string $secret): string +{ + $header = base64_url_encode(json_encode(['typ' => 'JWT', 'alg' => 'HS256'], JSON_THROW_ON_ERROR)); + $body = base64_url_encode(json_encode($payload, JSON_THROW_ON_ERROR)); + $signature = base64_url_encode(hash_hmac('sha256', $header.'.'.$body, $secret, true)); + + return $header.'.'.$body.'.'.$signature; +} + +function http_request(string $url, string $cookieHeader = ''): array +{ + $headers = "Connection: close\r\n"; + + if ($cookieHeader !== '') { + $headers .= "Cookie: $cookieHeader\r\n"; + } + + $context = stream_context_create([ + 'http' => [ + 'ignore_errors' => true, + 'timeout' => 20, + 'follow_location' => 0, + 'max_redirects' => 1, + 'header' => $headers, + ], + ]); + + $body = @file_get_contents($url, false, $context); + $responseHeaders = $http_response_header ?? []; + $status = 0; + + if (isset($responseHeaders[0]) && preg_match('/^HTTP\/\S+\s+(\d+)/', $responseHeaders[0], $matches)) { + $status = (int) $matches[1]; + } + + return [ + 'status' => $status, + 'headers' => $responseHeaders, + 'body' => $body === false ? '' : $body, + ]; +} + +function wait_for_http(): void +{ + for ($attempt = 1; $attempt <= 60; $attempt++) { + $response = http_request(TEST_SITE_URL.'/wp-login.php'); + + if ($response['status'] > 0) { + pass('WordPress HTTP server is reachable'); + return; + } + + sleep(1); + } + + fail('WordPress HTTP server did not become reachable.'); +} + +function header_contains(array $headers, string $needle): bool +{ + foreach ($headers as $header) { + if (stripos($header, $needle) !== false) { + return true; + } + } + + return false; +} + +function cookie_header_from_response(array $headers): string +{ + $cookies = []; + + foreach ($headers as $header) { + if (stripos($header, 'Set-Cookie:') !== 0) { + continue; + } + + $cookie = trim(substr($header, strlen('Set-Cookie:'))); + $cookiePair = explode(';', $cookie, 2)[0] ?? ''; + + if ($cookiePair !== '') { + $cookies[] = $cookiePair; + } + } + + return implode('; ', $cookies); +} + +function test_valid_authentication(): void +{ + $token = make_jwt([ + 'iat' => time() - 10, + 'exp' => time() + 300, + 'data' => [ + 'name' => TEST_NAME, + 'email' => TEST_EMAIL, + 'mobile' => '9123456789', + 'username' => 'mahak-auth-user', + ], + ], TEST_SECRET); + + $response = http_request(TEST_SITE_URL.'/mahak/login/?token='.rawurlencode($token)); + + assert_true($response['status'] === 302, 'Valid authentication request should return HTTP 302.'); + assert_true( + header_contains($response['headers'], 'Set-Cookie: wordpress_logged_in_'), + 'Valid authentication request did not issue a WordPress logged-in cookie.' + ); + assert_true( + header_contains($response['headers'], 'Location: '.TEST_SITE_URL.'/wp-admin/profile.php'), + 'Valid authentication request did not redirect to the configured route.' + ); + + $user = get_user_by('email', TEST_EMAIL); + + assert_true($user !== false, 'Authenticated user was not created.'); + assert_true($user->display_name === TEST_NAME, 'Authenticated user display name was not saved.'); + + $cookieHeader = cookie_header_from_response($response['headers']); + $profileResponse = http_request(TEST_SITE_URL.'/wp-admin/profile.php', $cookieHeader); + + assert_true( + $profileResponse['status'] !== 302 || !header_contains($profileResponse['headers'], 'wp-login.php'), + 'Issued cookies did not authenticate a follow-up WordPress admin request.' + ); + + pass('Valid JWT creates/logs in user and issued cookies authenticate follow-up request'); +} + +function test_invalid_authentication(): void +{ + $token = make_jwt([ + 'iat' => time() - 10, + 'exp' => time() + 300, + 'data' => [ + 'name' => 'Invalid User', + 'email' => 'invalid-user@example.test', + ], + ], TEST_SECRET); + + $response = http_request(TEST_SITE_URL.'/mahak/login/?token='.rawurlencode($token.'tampered')); + + assert_true( + $response['status'] === 500, + 'Invalid authentication request should return HTTP 500. Got HTTP '.$response['status'].' with body: '.substr($response['body'], 0, 200) + ); + assert_true( + !header_contains($response['headers'], 'Set-Cookie: wordpress_logged_in_'), + 'Invalid authentication request issued a logged-in cookie.' + ); + + pass('Invalid JWT is rejected without login cookies'); +} + +boot_wordpress(); +install_wordpress(); +activate_and_configure_plugin(); +wait_for_http(); +test_valid_authentication(); +test_invalid_authentication(); + +echo "[OK] Authentication integration test passed\n"; diff --git a/test/run-authentication-test.sh b/test/run-authentication-test.sh new file mode 100755 index 0000000..fbebb23 --- /dev/null +++ b/test/run-authentication-test.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_NAME="${COMPOSE_PROJECT_NAME:-mahak-authenticate-bridge-test}" +WORDPRESS_PORT="${WORDPRESS_PORT:-8080}" +KEEP_TEST_ENV="${KEEP_TEST_ENV:-0}" + +if docker info >/dev/null 2>&1; then + DOCKER=(docker) +elif command -v sudo >/dev/null 2>&1; then + DOCKER=(sudo docker) +else + echo "Docker is not available." >&2 + exit 1 +fi + +COMPOSE=("${DOCKER[@]}" compose) + +compose() { + WORDPRESS_PORT="$WORDPRESS_PORT" "${COMPOSE[@]}" \ + -p "$PROJECT_NAME" \ + -f "$SCRIPT_DIR/docker-compose.yml" \ + "$@" +} + +cleanup() { + if [[ "$KEEP_TEST_ENV" != "1" ]]; then + compose down --volumes --remove-orphans >/dev/null 2>&1 || true + fi +} + +wait_for_wordpress_files() { + for _ in $(seq 1 90); do + if compose exec -T wordpress php -r 'exit(is_file("/var/www/html/wp-load.php") ? 0 : 1);' >/dev/null 2>&1; then + return 0 + fi + + sleep 1 + done + + echo "WordPress files were not ready in time." >&2 + return 1 +} + +run_php_lint() { + compose exec -T wordpress sh -lc ' + find /var/www/html/wp-content/plugins/mahak-authenticate-bridge \ + -path "*/.git" -prune -o \ + -name "*.php" -print \ + | xargs -n1 php -l + ' +} + +trap cleanup EXIT + +echo "Resetting test containers and volumes..." +compose down --volumes --remove-orphans + +echo "Pulling Docker images..." +compose pull + +echo "Starting fresh WordPress on http://127.0.0.1:${WORDPRESS_PORT} ..." +compose up -d + +echo "Waiting for WordPress files..." +wait_for_wordpress_files + +echo "Running PHP 7.4 syntax checks..." +run_php_lint + +echo "Running authentication integration test in PHP 7.4..." +compose exec -T wordpress php /var/www/html/wp-content/plugins/mahak-authenticate-bridge/test/integration-authentication.php + +echo "Integration test passed." + +if [[ "$KEEP_TEST_ENV" == "1" ]]; then + echo "Test environment kept running. Clean it up with:" + echo " ${COMPOSE[*]} -p $PROJECT_NAME -f $SCRIPT_DIR/docker-compose.yml down --volumes --remove-orphans" +fi