Added a Docker-based fresh WordPress integration test
This commit is contained in:
@@ -28,5 +28,3 @@ add_action('init', fn() => AuthenticateController::resolve());
|
|||||||
|
|
||||||
add_action('admin_menu', fn() => SettingsPageMenuController::resolve());
|
add_action('admin_menu', fn() => SettingsPageMenuController::resolve());
|
||||||
add_action('admin_init', fn() => SettingsPageFormController::resolve());
|
add_action('admin_init', fn() => SettingsPageFormController::resolve());
|
||||||
|
|
||||||
echo true;
|
|
||||||
|
|||||||
@@ -54,12 +54,13 @@ class AuthenticateController extends InvocableController
|
|||||||
}
|
}
|
||||||
$random_password = wp_generate_password(12, true);
|
$random_password = wp_generate_password(12, true);
|
||||||
$username = $identifier_value;
|
$username = $identifier_value;
|
||||||
|
$email = $identifier_name === 'email' ? $identifier_value : '';
|
||||||
|
|
||||||
if (username_exists($username)) {
|
if (username_exists($username)) {
|
||||||
$username .= '_'.wp_generate_password(4, false);
|
$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)) {
|
if (is_wp_error($user_id)) {
|
||||||
wp_die(view('invalid-token'), 'Invalid Token', ['response' => 500]);
|
wp_die(view('invalid-token'), 'Invalid Token', ['response' => 500]);
|
||||||
|
|||||||
+4
-1
@@ -1,10 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
if (!function_exists('view')) {
|
if (!function_exists('view')) {
|
||||||
function view(string $view)
|
function view(string $view): string
|
||||||
{
|
{
|
||||||
$basedir = plugin_dir_path(__FILE__);
|
$basedir = plugin_dir_path(__FILE__);
|
||||||
|
ob_start();
|
||||||
include "{$basedir}resources/views/$view.php";
|
include "{$basedir}resources/views/$view.php";
|
||||||
|
|
||||||
|
return ob_get_clean();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Authentication Integration Test
|
||||||
|
|
||||||
|
This test starts a fresh WordPress site with PHP 7.4, installs and configures this plugin, then verifies JWT login through the real HTTP route.
|
||||||
|
|
||||||
|
Run from the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash test/run-authentication-test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
If Docker requires root on your machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash test/run-authentication-test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The test resets its Docker volumes on every run. Set `KEEP_TEST_ENV=1` to leave WordPress running after the test:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
KEEP_TEST_ENV=1 bash test/run-authentication-test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Use another host port if `8080` is busy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
WORDPRESS_PORT=8081 bash test/run-authentication-test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The test covers:
|
||||||
|
|
||||||
|
- PHP 7.4 syntax checks for all project PHP files
|
||||||
|
- fresh WordPress database installation
|
||||||
|
- plugin activation without unexpected output
|
||||||
|
- plugin option configuration
|
||||||
|
- valid HS256 JWT login
|
||||||
|
- created WordPress user lookup
|
||||||
|
- WordPress auth cookie issuance
|
||||||
|
- follow-up authenticated admin request
|
||||||
|
- invalid/tampered JWT rejection
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mariadb:10.6
|
||||||
|
environment:
|
||||||
|
MARIADB_DATABASE: wordpress
|
||||||
|
MARIADB_USER: wordpress
|
||||||
|
MARIADB_PASSWORD: wordpress
|
||||||
|
MARIADB_ROOT_PASSWORD: root
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$${MARIADB_ROOT_PASSWORD} --silent"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 30
|
||||||
|
|
||||||
|
wordpress:
|
||||||
|
image: wordpress:php7.4-apache
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
WORDPRESS_DB_HOST: db:3306
|
||||||
|
WORDPRESS_DB_NAME: wordpress
|
||||||
|
WORDPRESS_DB_USER: wordpress
|
||||||
|
WORDPRESS_DB_PASSWORD: wordpress
|
||||||
|
WORDPRESS_DEBUG: "1"
|
||||||
|
WORDPRESS_CONFIG_EXTRA: |
|
||||||
|
define('WP_DEBUG_LOG', true);
|
||||||
|
define('WP_DEBUG_DISPLAY', false);
|
||||||
|
ports:
|
||||||
|
- "${WORDPRESS_PORT:-8080}:80"
|
||||||
|
volumes:
|
||||||
|
- wordpress_data:/var/www/html
|
||||||
|
- ..:/var/www/html/wp-content/plugins/mahak-authenticate-bridge:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
wordpress_data:
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
const TEST_SITE_URL = 'http://127.0.0.1';
|
||||||
|
const TEST_PLUGIN = 'mahak-authenticate-bridge/dabestaniha-authenticate-bridge.php';
|
||||||
|
const TEST_SECRET = 'integration-test-secret';
|
||||||
|
const TEST_EMAIL = 'mahak-auth-user@example.test';
|
||||||
|
const TEST_NAME = 'Mahak Auth User';
|
||||||
|
|
||||||
|
function fail(string $message): void
|
||||||
|
{
|
||||||
|
fwrite(STDERR, "[FAIL] $message\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pass(string $message): void
|
||||||
|
{
|
||||||
|
echo "[OK] $message\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert_true(bool $condition, string $message): void
|
||||||
|
{
|
||||||
|
if (!$condition) {
|
||||||
|
fail($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function boot_wordpress(): void
|
||||||
|
{
|
||||||
|
$wpLoad = '/var/www/html/wp-load.php';
|
||||||
|
|
||||||
|
if (!is_file($wpLoad)) {
|
||||||
|
fail("WordPress bootstrap file was not found at $wpLoad.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('WP_INSTALLING')) {
|
||||||
|
define('WP_INSTALLING', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
require_once $wpLoad;
|
||||||
|
ob_end_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
function install_wordpress(): void
|
||||||
|
{
|
||||||
|
require_once ABSPATH.'wp-admin/includes/upgrade.php';
|
||||||
|
|
||||||
|
if (!is_blog_installed()) {
|
||||||
|
wp_install(
|
||||||
|
'Mahak Authenticate Bridge Test',
|
||||||
|
'admin',
|
||||||
|
'admin@example.test',
|
||||||
|
true,
|
||||||
|
'',
|
||||||
|
'admin-password'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update_option('siteurl', TEST_SITE_URL);
|
||||||
|
update_option('home', TEST_SITE_URL);
|
||||||
|
|
||||||
|
pass('Fresh WordPress database installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate_and_configure_plugin(): void
|
||||||
|
{
|
||||||
|
require_once ABSPATH.'wp-admin/includes/plugin.php';
|
||||||
|
|
||||||
|
if (!is_plugin_active(TEST_PLUGIN)) {
|
||||||
|
ob_start();
|
||||||
|
$result = activate_plugin(TEST_PLUGIN);
|
||||||
|
$activationOutput = trim((string) ob_get_clean());
|
||||||
|
|
||||||
|
if (is_wp_error($result)) {
|
||||||
|
fail('Plugin activation failed: '.$result->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";
|
||||||
Executable
+81
@@ -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
|
||||||
Reference in New Issue
Block a user