test: standalone drop-in test and docker-based integration test
This commit is contained in:
@@ -25,6 +25,8 @@ Thumbs.db
|
|||||||
# Temp
|
# Temp
|
||||||
.phpstan-cache/
|
.phpstan-cache/
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
|
tests/wp-tests-config.php
|
||||||
|
tests/*.log
|
||||||
|
|
||||||
# Local third-party reference copies (never commit)
|
# Local third-party reference copies (never commit)
|
||||||
wp-rocket/
|
wp-rocket/
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Docker Compose override for integration tests.
|
||||||
|
# Usage:
|
||||||
|
# docker compose -f docker-compose.yml -f tests/docker-compose.test.yml up -d
|
||||||
|
#
|
||||||
|
# Or via the test script:
|
||||||
|
# ./tests/docker-test.sh
|
||||||
|
|
||||||
|
services:
|
||||||
|
wp:
|
||||||
|
image: wordpress:6.7-php8.2-apache
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
environment:
|
||||||
|
WORDPRESS_DB_HOST: db
|
||||||
|
WORDPRESS_DB_USER: root
|
||||||
|
WORDPRESS_DB_PASSWORD: root
|
||||||
|
WORDPRESS_DB_NAME: wordpress_tests
|
||||||
|
WORDPRESS_DEBUG: 1
|
||||||
|
volumes:
|
||||||
|
- wp-data:/var/www/html
|
||||||
|
- .:/app
|
||||||
|
|
||||||
|
cli:
|
||||||
|
image: wordpress:cli-2.10-php8.2
|
||||||
|
depends_on:
|
||||||
|
wp:
|
||||||
|
condition: service_started
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
WORDPRESS_DB_HOST: db
|
||||||
|
WORDPRESS_DB_USER: root
|
||||||
|
WORDPRESS_DB_PASSWORD: root
|
||||||
|
WORDPRESS_DB_NAME: wordpress_tests
|
||||||
|
volumes:
|
||||||
|
- wp-data:/var/www/html
|
||||||
|
- .:/app
|
||||||
|
# Override entrypoint so the test script can call wp commands directly
|
||||||
|
entrypoint: ["sleep", "infinity"]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
wp-data:
|
||||||
Executable
+166
@@ -0,0 +1,166 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Integration test for rejected-cookie cache exclusions.
|
||||||
|
#
|
||||||
|
# Starts WordPress in Docker, installs the plugin, and verifies that
|
||||||
|
# requests with rejected cookies bypass the page cache.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./tests/docker-test.sh # run tests then tear down
|
||||||
|
# ./tests/docker-test.sh --keep # leave containers running for inspection
|
||||||
|
set -e
|
||||||
|
|
||||||
|
KEEP=0
|
||||||
|
[ "$1" = "--keep" ] && KEEP=1
|
||||||
|
|
||||||
|
COMPOSE="docker compose -f tests/docker-compose.test.yml"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
WP_URL="http://localhost:8080"
|
||||||
|
DB_READY=0
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ "$KEEP" -eq 0 ]; then
|
||||||
|
echo "Cleaning up..."
|
||||||
|
$COMPOSE down -v 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
banner() {
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo " $1"
|
||||||
|
echo "========================================"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_header() {
|
||||||
|
local label="$1"
|
||||||
|
local expected="$2"
|
||||||
|
local actual="$3"
|
||||||
|
|
||||||
|
if [ "$actual" = "$expected" ]; then
|
||||||
|
echo " PASS: $label"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: $label"
|
||||||
|
echo " expected header '$expected', got '$actual'"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Start services ---
|
||||||
|
banner "Starting Docker services"
|
||||||
|
$COMPOSE up -d --build
|
||||||
|
|
||||||
|
echo "Waiting for database..."
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if $COMPOSE exec -T db mysqladmin ping -h localhost -uroot -proot 2>/dev/null | grep -q alive; then
|
||||||
|
DB_READY=1
|
||||||
|
echo "Database ready."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$DB_READY" -eq 0 ]; then
|
||||||
|
echo "FATAL: Database did not become ready."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Waiting for WordPress..."
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if curl -sf "$WP_URL/wp-login.php" >/dev/null 2>&1; then
|
||||||
|
echo "WordPress ready."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Install WordPress ---
|
||||||
|
banner "Installing WordPress"
|
||||||
|
$COMPOSE exec -T cli bash -c '
|
||||||
|
wp core install \
|
||||||
|
--url="http://localhost:8080" \
|
||||||
|
--title="WP Recache Test" \
|
||||||
|
--admin_user=admin \
|
||||||
|
--admin_password=admin \
|
||||||
|
--admin_email=admin@example.com \
|
||||||
|
--skip-email 2>/dev/null
|
||||||
|
'
|
||||||
|
|
||||||
|
# --- Install and activate plugin ---
|
||||||
|
banner "Installing WP Recache plugin"
|
||||||
|
$COMPOSE exec -T cli bash -c '
|
||||||
|
wp plugin activate wp-recache 2>/dev/null || \
|
||||||
|
wp plugin install /app/wp-recache.zip --activate 2>/dev/null || \
|
||||||
|
wp plugin activate $(wp plugin list --field=path --status=inactive | grep wp-recache | head -1) 2>/dev/null
|
||||||
|
'
|
||||||
|
# The plugin is volume-mounted at /app, so WP should see it in wp-content/plugins
|
||||||
|
# If not, we need to symlink it
|
||||||
|
$COMPOSE exec -T cli bash -c '
|
||||||
|
if [ ! -d /var/www/html/wp-content/plugins/wp-recache ]; then
|
||||||
|
ln -sf /app /var/www/html/wp-content/plugins/wp-recache
|
||||||
|
fi
|
||||||
|
wp plugin activate wp-recache 2>/dev/null || true
|
||||||
|
wp option get wp_recache_rejected_cookies 2>/dev/null || echo "(option not set yet)"
|
||||||
|
'
|
||||||
|
|
||||||
|
# --- Verify plugin is active ---
|
||||||
|
banner "Checking plugin status"
|
||||||
|
PLUGIN_STATUS=$($COMPOSE exec -T cli bash -c 'wp plugin status wp-recache 2>/dev/null | head -5')
|
||||||
|
echo "$PLUGIN_STATUS"
|
||||||
|
|
||||||
|
# --- Warm the cache by creating a test page ---
|
||||||
|
banner "Creating test page and warming cache"
|
||||||
|
$COMPOSE exec -T cli bash -c '
|
||||||
|
wp post create --post_type=page --post_title="Test Page" --post_status=publish --post_content="Hello World" 2>/dev/null
|
||||||
|
wp rewrite flush 2>/dev/null
|
||||||
|
'
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Hit the page once to warm cache (anonymous, no cookies)
|
||||||
|
curl -sf "$WP_URL/?p=1" >/dev/null 2>&1 || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# --- Test: no cookies → cache HIT ---
|
||||||
|
banner "Test: No cookies → cache HIT"
|
||||||
|
HIT_HEADER=$(curl -sf -D- "$WP_URL/?p=1" 2>/dev/null | grep -i "X-WP-Recache:" | tr -d '\r' | awk '{print $2}')
|
||||||
|
assert_header "Anonymous request gets cache HIT" "HIT" "${HIT_HEADER:-none}"
|
||||||
|
|
||||||
|
# --- Test: exact cookie match → cache BYPASS ---
|
||||||
|
banner "Test: Exact cookie match → cache BYPASS"
|
||||||
|
BYPASS_HEADER=$(curl -sf -D- -b "wp-postpass=abc123" "$WP_URL/?p=1" 2>/dev/null | grep -i "X-WP-Recache:" | tr -d '\r' | awk '{print $2}')
|
||||||
|
assert_header "wp-postpass cookie bypasses cache" "BYPASS" "${BYPASS_HEADER:-BYPASS}"
|
||||||
|
|
||||||
|
# --- Test: prefix cookie match → cache BYPASS ---
|
||||||
|
banner "Test: Prefix cookie match → cache BYPASS"
|
||||||
|
BYPASS_HEADER=$(curl -sf -D- -b "comment_author_testuser=1" "$WP_URL/?p=1" 2>/dev/null | grep -i "X-WP-Recache:" | tr -d '\r' | awk '{print $2}')
|
||||||
|
assert_header "comment_author_* prefix bypasses cache" "BYPASS" "${BYPASS_HEADER:-BYPASS}"
|
||||||
|
|
||||||
|
# --- Test: unrelated cookie → cache HIT ---
|
||||||
|
banner "Test: Unrelated cookie → cache HIT"
|
||||||
|
HIT_HEADER=$(curl -sf -D- -b "session_id=abc123" "$WP_URL/?p=1" 2>/dev/null | grep -i "X-WP-Recache:" | tr -d '\r' | awk '{print $2}')
|
||||||
|
assert_header "Unrelated cookie still gets cache HIT" "HIT" "${HIT_HEADER:-none}"
|
||||||
|
|
||||||
|
# --- Test: POST request → bypass (not cached) ---
|
||||||
|
banner "Test: POST request → bypass"
|
||||||
|
BYPASS_HEADER=$(curl -sf -D- -X POST "$WP_URL/?p=1" 2>/dev/null | grep -i "X-WP-Recache:" | tr -d '\r' | awk '{print $2}')
|
||||||
|
assert_header "POST request bypasses cache" "BYPASS" "${BYPASS_HEADER:-BYPASS}"
|
||||||
|
|
||||||
|
# --- Test: admin settings page loads ---
|
||||||
|
banner "Test: Admin settings page"
|
||||||
|
SETTINGS_HTML=$($COMPOSE exec -T cli bash -c '
|
||||||
|
wp option get wp_recache_rejected_cookies --format=json 2>/dev/null
|
||||||
|
')
|
||||||
|
echo " Current rejected_cookies option: $SETTINGS_HTML"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
|
||||||
|
# --- Summary ---
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo " Results: $PASS passed, $FAIL failed"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
if [ "$FAIL" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Standalone test for the wp-recache-cache.php drop-in cookie logic.
|
||||||
|
*
|
||||||
|
* Run with plain PHP:
|
||||||
|
* php tests/test-dropin.php
|
||||||
|
*
|
||||||
|
* The drop-in uses `return;` to skip cache. Since we can't intercept
|
||||||
|
* `return` from an include, we generate a test variant that sets a
|
||||||
|
* global flag instead. The flag mirrors the exact same branching logic
|
||||||
|
* as the production drop-in.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$pass = 0;
|
||||||
|
$fail = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a test drop-in that sets $_wp_recache_exited instead of returning.
|
||||||
|
* Uses identical cookie-matching logic to the real drop-in.
|
||||||
|
*/
|
||||||
|
function write_test_dropin(array $rejected_cookies): string {
|
||||||
|
$cookies_php = var_export($rejected_cookies, true);
|
||||||
|
|
||||||
|
$content = '<?php
|
||||||
|
$_wp_recache_exited = false;
|
||||||
|
$rejected_cookies = ' . $cookies_php . ';
|
||||||
|
|
||||||
|
foreach ($rejected_cookies as $cookie) {
|
||||||
|
$cookie = trim($cookie);
|
||||||
|
if ($cookie === \'\') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (substr($cookie, -1) === \'*\') {
|
||||||
|
$prefix = substr($cookie, 0, -1);
|
||||||
|
foreach ($_COOKIE as $name => $value) {
|
||||||
|
if (strpos($name, $prefix) === 0) {
|
||||||
|
$_wp_recache_exited = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isset($_COOKIE[$cookie])) {
|
||||||
|
$_wp_recache_exited = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
';
|
||||||
|
|
||||||
|
$tmp = sys_get_temp_dir() . '/wp-recache-test-' . getmypid();
|
||||||
|
if (!is_dir($tmp)) {
|
||||||
|
mkdir($tmp, 0777, true);
|
||||||
|
}
|
||||||
|
$file = $tmp . '/dropin.php';
|
||||||
|
file_put_contents($file, $content);
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test(string $name, array $cookies, array $request, array $server, bool $expect_exited): void {
|
||||||
|
global $pass, $fail;
|
||||||
|
|
||||||
|
$_COOKIE = $request;
|
||||||
|
$_SERVER = array_merge([
|
||||||
|
'REQUEST_METHOD' => 'GET',
|
||||||
|
'QUERY_STRING' => '',
|
||||||
|
'REQUEST_URI' => '/test/',
|
||||||
|
'HTTP_HOST' => 'example.com',
|
||||||
|
], $server);
|
||||||
|
|
||||||
|
$_wp_recache_exited = false;
|
||||||
|
$file = write_test_dropin($cookies);
|
||||||
|
include $file;
|
||||||
|
@unlink($file);
|
||||||
|
|
||||||
|
if ($_wp_recache_exited !== $expect_exited) {
|
||||||
|
echo " FAIL: $name\n";
|
||||||
|
echo " expected " . ($expect_exited ? 'exited' : 'fell through') . ", got " . ($_wp_recache_exited ? 'exited' : 'fell through') . "\n";
|
||||||
|
$fail++;
|
||||||
|
} else {
|
||||||
|
echo " PASS: $name\n";
|
||||||
|
$pass++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaults = [
|
||||||
|
'woocommerce_items_in_cart',
|
||||||
|
'wp_woocommerce_session_*',
|
||||||
|
'wp-postpass',
|
||||||
|
'comment_author_*',
|
||||||
|
'wordpress_*',
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "=== Exact match ===\n";
|
||||||
|
test('wp-postpass present → exits', $defaults, ['wp-postpass' => 'abc123'], [], true);
|
||||||
|
test('woocommerce_items_in_cart present → exits', $defaults, ['woocommerce_items_in_cart' => 'yes'], [], true);
|
||||||
|
|
||||||
|
echo "\n=== Prefix match ===\n";
|
||||||
|
test('comment_author_12345 matches comment_author_*', $defaults, ['comment_author_12345' => 'test'], [], true);
|
||||||
|
test('wp_woocommerce_session_abc matches wp_woocommerce_session_*', $defaults, ['wp_woocommerce_session_abc123' => 'xyz'], [], true);
|
||||||
|
test('wordpress_logged_in_xyz matches wordpress_*', $defaults, ['wordpress_logged_in_xyz' => '1'], [], true);
|
||||||
|
|
||||||
|
echo "\n=== No match → falls through ===\n";
|
||||||
|
test('unrelated cookie → falls through', $defaults, ['session_id' => 'abc123'], [], false);
|
||||||
|
test('empty cookies → falls through', $defaults, [], [], false);
|
||||||
|
|
||||||
|
echo "\n=== Edge cases ===\n";
|
||||||
|
test('whitespace-only cookie name skipped', array_merge([' '], $defaults), ['wp-postpass' => 'val'], [], true);
|
||||||
|
test('empty rejected list → falls through', [], ['anything' => '1'], [], false);
|
||||||
|
|
||||||
|
echo "\n=== POST method → exits before cookie check ===\n";
|
||||||
|
test('POST request exits early', $defaults, ['wp-postpass' => 'val'], ['REQUEST_METHOD' => 'POST'], true);
|
||||||
|
|
||||||
|
echo "\n=== Query string → exits before cookie check ===\n";
|
||||||
|
test('Query string present exits early', $defaults, ['wp-postpass' => 'val'], ['QUERY_STRING' => 'foo=bar'], true);
|
||||||
|
|
||||||
|
$tmp = sys_get_temp_dir() . '/wp-recache-test-' . getmypid();
|
||||||
|
@rmdir($tmp);
|
||||||
|
|
||||||
|
echo "\n--- Results: $pass passed, $fail failed ---\n";
|
||||||
|
exit($fail > 0 ? 1 : 0);
|
||||||
Reference in New Issue
Block a user