From efbbda4cc44500364ee31cc9d3c159820ef8c4ca Mon Sep 17 00:00:00 2001 From: omikheev Date: Thu, 3 Sep 2026 16:33:51 -0400 Subject: [PATCH] architecture: extract Cardea_Comment_Gate, single version source, unified excludes, shared e2e fixtures --- Makefile | 58 +-- cardea.php | 3 + composer.json | 5 - includes/class-cardea-comment-gate.php | 209 ++++++++++ includes/class-cardea-core.php | 154 +------- tests/e2e/api-comments.spec.js | 40 +- tests/e2e/pow-comment.spec.js | 96 +---- tests/e2e/rest-challenge.spec.js | 40 +- tests/e2e/support/playground.js | 93 +++++ tests/e2e/trackback-comments.spec.js | 40 +- tests/e2e/xmlrpc-comments.spec.js | 40 +- tests/phpunit/Cardea_Comment_Gate_Test.php | 425 +++++++++++++++++++++ tests/phpunit/Cardea_Core_Test.php | 375 +----------------- tests/phpunit/bootstrap.php | 13 +- 14 files changed, 797 insertions(+), 794 deletions(-) create mode 100644 includes/class-cardea-comment-gate.php create mode 100644 tests/e2e/support/playground.js create mode 100644 tests/phpunit/Cardea_Comment_Gate_Test.php diff --git a/Makefile b/Makefile index f6a6b34..2c5fd6f 100644 --- a/Makefile +++ b/Makefile @@ -47,32 +47,22 @@ test-e2e: # ========================================== # PACKAGING # ========================================== +# Paths excluded from the distributed plugin. Single source of truth, shared +# by `make package` (zip) and `make sync-svn` (rsync) so the two can never +# drift apart. +EXCLUDE_DIRS := node_modules vendor tests dist .github .playwright-browsers playwright-report test-results wp-assets design +EXCLUDE_FILES := .phpunit.result.cache phpunit.xml playwright.config.js Dockerfile .dockerignore Makefile README.md +EXCLUDE_GLOBS := *.git* composer.* package*.json + +ZIP_EXCLUDES := $(foreach entry,$(EXCLUDE_DIRS),\"$(entry)/*\") $(foreach entry,$(EXCLUDE_FILES),\"$(entry)\") $(foreach entry,$(EXCLUDE_GLOBS),\"$(entry)\") +RSYNC_EXCLUDES := $(foreach entry,$(EXCLUDE_DIRS),--exclude=$(entry)/) $(foreach entry,$(EXCLUDE_FILES),--exclude=$(entry)) $(foreach entry,$(EXCLUDE_GLOBS),--exclude=$(entry)) + package: @echo "Packaging $(PLUGIN_SLUG) version $(VERSION)..." @mkdir -p dist @rm -f dist/$(PLUGIN_SLUG).zip - @# Use a temporary directory to ensure only the necessary files are zipped - @zip -r dist/$(PLUGIN_SLUG).zip . \ - -x "*.git*" \ - -x "node_modules/*" \ - -x "vendor/*" \ - -x "tests/*" \ - -x "dist/*" \ - -x ".github/*" \ - -x ".playwright-browsers/*" \ - -x "playwright-report/*" \ - -x "test-results/*" \ - -x ".phpunit.result.cache" \ - -x "phpunit.xml" \ - -x "playwright.config.js" \ - -x "composer.*" \ - -x "package*.json" \ - -x "Dockerfile" \ - -x ".dockerignore" \ - -x "Makefile" \ - -x "README.md" \ - -x "wp-assets/*" \ - -x "design/*" + @# Zip only the plugin files (exclusion list above keeps dev artifacts out) + @zip -r dist/$(PLUGIN_SLUG).zip . -x $(ZIP_EXCLUDES) @echo "Package created at dist/$(PLUGIN_SLUG).zip" # ========================================== @@ -86,29 +76,7 @@ sync-svn: @if [ ! -d "$(SVN_DIR)" ]; then echo "Error: SVN directory $(SVN_DIR) does not exist."; exit 1; fi @echo "--> Mirroring production files to $(SVN_DIR)/trunk/" - @rsync -av --delete \ - --exclude=".*" \ - --exclude="*.git*" \ - --exclude="node_modules/" \ - --exclude="vendor/" \ - --exclude="tests/" \ - --exclude="dist/" \ - --exclude=".github/" \ - --exclude=".playwright-browsers/" \ - --exclude="playwright-report/" \ - --exclude="test-results/" \ - --exclude=".phpunit.result.cache" \ - --exclude="phpunit.xml" \ - --exclude="playwright.config.js" \ - --exclude="composer.*" \ - --exclude="package*.json" \ - --exclude="Dockerfile" \ - --exclude=".dockerignore" \ - --exclude="Makefile" \ - --exclude="README.md" \ - --exclude="wp-assets/" \ - --exclude="design/" \ - ./ $(SVN_DIR)/trunk/ + @rsync -av --delete $(RSYNC_EXCLUDES) ./ $(SVN_DIR)/trunk/ @echo "--> Mirroring repository assets to $(SVN_DIR)/assets/" @rsync -av --delete ./wp-assets/ $(SVN_DIR)/assets/ diff --git a/cardea.php b/cardea.php index 6c91fc4..1bccd55 100644 --- a/cardea.php +++ b/cardea.php @@ -26,6 +26,7 @@ define( 'CARDEA_DEFAULT_WINDOW', 30 ); require_once CARDEA_PLUGIN_DIR . 'includes/class-cardea-core.php'; +require_once CARDEA_PLUGIN_DIR . 'includes/class-cardea-comment-gate.php'; require_once CARDEA_PLUGIN_DIR . 'includes/class-cardea-frontend.php'; require_once CARDEA_PLUGIN_DIR . 'includes/class-cardea-admin.php'; @@ -36,10 +37,12 @@ */ function cardea_init() { $core = new Cardea_Core(); + $gate = new Cardea_Comment_Gate( $core ); $frontend = new Cardea_Frontend( $core ); $admin = new Cardea_Admin( $core ); $core->init(); + $gate->init(); $frontend->init(); $admin->init(); } diff --git a/composer.json b/composer.json index 5bc929f..963de95 100644 --- a/composer.json +++ b/composer.json @@ -12,11 +12,6 @@ "phpcompatibility/php-compatibility": "^9.3", "wp-coding-standards/wpcs": "^3.0" }, - "autoload": { - "psr-4": { - "Cardea\\": "includes/" - } - }, "scripts": { "test": "phpunit", "phpcs": "phpcs --standard=WordPress --ignore=vendor/,node_modules/,tests/ --extensions=php .", diff --git a/includes/class-cardea-comment-gate.php b/includes/class-cardea-comment-gate.php new file mode 100644 index 0000000..492cdfb --- /dev/null +++ b/includes/class-cardea-comment-gate.php @@ -0,0 +1,209 @@ +core = $core; + } + + /** + * Initialize hooks. + */ + public function init() { + add_filter( 'preprocess_comment', array( $this, 'verify_form_submission' ) ); + add_filter( 'rest_pre_insert_comment', array( $this, 'verify_rest_submission' ), 10, 2 ); + } + + /** + * Verify a PoW on comment form submission. + * + * @param array $commentdata Comment data. + * @return array|WP_Error + */ + public function verify_form_submission( $commentdata ) { + $comment_type = isset( $commentdata['comment_type'] ) ? $commentdata['comment_type'] : ''; + + if ( $this->submission_is_exempt( $comment_type ) ) { + return $commentdata; + } + + /* phpcs:disable WordPress.Security.NonceVerification.Missing -- values are verified by wp_verify_nonce() in run_verification(). */ + $nonce = isset( $_POST['cardea_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['cardea_nonce'] ) ) : ''; + $timestamp = isset( $_POST['cardea_timestamp'] ) ? sanitize_text_field( wp_unslash( $_POST['cardea_timestamp'] ) ) : ''; + $salt = isset( $_POST['cardea_salt'] ) ? sanitize_text_field( wp_unslash( $_POST['cardea_salt'] ) ) : ''; + $solution = isset( $_POST['cardea_solution'] ) ? sanitize_text_field( wp_unslash( $_POST['cardea_solution'] ) ) : ''; + $signature = isset( $_POST['cardea_signature'] ) ? sanitize_text_field( wp_unslash( $_POST['cardea_signature'] ) ) : ''; + /* phpcs:enable */ + + if ( $this->run_verification( $nonce, $timestamp, $salt, $solution, $signature ) ) { + return $commentdata; + } + + $this->die_on_verification_failure(); + + return $commentdata; + } + + /** + * Verify a PoW on REST API comment submission. + * + * Anonymous submissions must provide the same challenge fields as the + * comment form and are verified by the identical pipeline. + * + * @param array $prepared_comment Prepared comment data. + * @param WP_REST_Request $request The request object. + * @return array|WP_Error + */ + public function verify_rest_submission( $prepared_comment, $request ) { + $comment_type = $request->get_param( 'comment_type' ) ? $request->get_param( 'comment_type' ) : ''; + + if ( $this->submission_is_exempt( $comment_type ) ) { + return $prepared_comment; + } + + $nonce = $this->get_rest_param( $request, 'cardea_nonce' ); + $timestamp = $this->get_rest_param( $request, 'cardea_timestamp' ); + $salt = $this->get_rest_param( $request, 'cardea_salt' ); + $solution = $this->get_rest_param( $request, 'cardea_solution' ); + $signature = $this->get_rest_param( $request, 'cardea_signature' ); + + if ( $this->run_verification( $nonce, $timestamp, $salt, $solution, $signature ) ) { + return $prepared_comment; + } + + /* + * One generic error for every failure mode: the client learns only that + * verification failed, never which check failed. + */ + return new WP_Error( + 'cardea_verification_failed', + Cardea_Core::failure_message(), + array( 'status' => 403 ) + ); + } + + /** + * Whether this submission is exempt from Proof-of-Work verification. + * + * Moderators, logged-in users, and non-comment types (pingbacks, + * trackbacks) are exempt. + * + * @param string $comment_type Comment type of the submission. + * @return bool + */ + private function submission_is_exempt( $comment_type ) { + if ( current_user_can( 'moderate_comments' ) ) { + return true; + } + + if ( in_array( $comment_type, array( 'pingback', 'trackback' ), true ) ) { + return true; + } + + return is_user_logged_in(); + } + + /** + * Run the shared verification pipeline over parsed submission fields. + * + * @param string $nonce Challenge nonce. + * @param string $timestamp Challenge timestamp. + * @param string $salt Challenge salt. + * @param string $solution Client solution. + * @param string $signature Challenge signature. + * @return bool True when the submission verifies. + */ + private function run_verification( $nonce, $timestamp, $salt, $solution, $signature ) { + if ( empty( $nonce ) || empty( $timestamp ) || empty( $salt ) || empty( $solution ) ) { + return false; + } + + if ( ! wp_verify_nonce( $nonce, 'cardea_challenge' ) ) { + return false; + } + + $challenge = array( + 'nonce' => $nonce, + 'timestamp' => $timestamp, + 'salt' => $salt, + 'signature' => $signature, + 'difficulty' => $this->core->get_difficulty(), + ); + + return ! is_wp_error( $this->core->verify_solution( $challenge, $solution ) ); + } + + /** + * Read and sanitize a plugin parameter from a REST request. + * + * @param object $request The request object. + * @param string $param Parameter name. + * @return string + */ + private function get_rest_param( $request, $param ) { + $value = $request->get_param( $param ); + return sanitize_text_field( wp_unslash( is_scalar( $value ) ? (string) $value : '' ) ); + } + + /** + * Terminate the request with the generic verification failure message. + * + * @codeCoverageIgnore + */ + private function die_on_verification_failure() { + wp_die( + esc_html( Cardea_Core::failure_message() ), + esc_html__( 'PoW Verification Failed', 'cardea' ), + array( 'response' => 403 ) + ); + } +} diff --git a/includes/class-cardea-core.php b/includes/class-cardea-core.php index a7bef3f..d4740d3 100644 --- a/includes/class-cardea-core.php +++ b/includes/class-cardea-core.php @@ -212,10 +212,10 @@ public function hash_meets_difficulty( $hash, $difficulty ) { /** * Initialize the plugin. + * + * Comment verification hooks are owned by Cardea_Comment_Gate. */ public function init() { - add_action( 'preprocess_comment', array( $this, 'verify_comment_pow' ) ); - add_filter( 'rest_pre_insert_comment', array( $this, 'verify_rest_comment' ), 10, 2 ); add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); } @@ -244,154 +244,4 @@ public function rest_get_challenge( $request ) { $post_id = $request->get_param( 'post_id' ) ? (int) $request->get_param( 'post_id' ) : 0; return $this->generate_challenge( $post_id ); } - - /** - * Verify PoW on REST API comment submission. - * - * @param array $prepared_comment Prepared comment data. - * @param WP_REST_Request $request The request object. - * @return array|WP_Error - */ - /** - * Verify PoW on REST API comment submission. - * - * Anonymous submissions must provide the same challenge fields as the - * comment form; they are verified by the identical pipeline. - * - * @param array $prepared_comment Prepared comment data. - * @param WP_REST_Request $request The request object. - * @return array|WP_Error - */ - public function verify_rest_comment( $prepared_comment, $request ) { - if ( current_user_can( 'moderate_comments' ) ) { - return $prepared_comment; - } - - $comment_type = $request->get_param( 'comment_type' ) ? $request->get_param( 'comment_type' ) : ''; - if ( in_array( $comment_type, array( 'pingback', 'trackback' ), true ) ) { - return $prepared_comment; - } - - if ( is_user_logged_in() ) { - return $prepared_comment; - } - - $nonce = $this->get_sanitized_rest_param( $request, 'cardea_nonce' ); - $timestamp = $this->get_sanitized_rest_param( $request, 'cardea_timestamp' ); - $salt = $this->get_sanitized_rest_param( $request, 'cardea_salt' ); - $solution = $this->get_sanitized_rest_param( $request, 'cardea_solution' ); - $signature = $this->get_sanitized_rest_param( $request, 'cardea_signature' ); - - if ( empty( $nonce ) || empty( $timestamp ) || empty( $salt ) || empty( $solution ) ) { - return new WP_Error( - 'cardea_missing_fields', - self::failure_message(), - array( 'status' => 403 ) - ); - } - - if ( ! wp_verify_nonce( $nonce, 'cardea_challenge' ) ) { - return new WP_Error( - 'cardea_security_check', - self::failure_message(), - array( 'status' => 403 ) - ); - } - - $challenge = array( - 'nonce' => $nonce, - 'timestamp' => $timestamp, - 'salt' => $salt, - 'signature' => $signature, - 'difficulty' => $this->get_difficulty(), - ); - - $result = $this->verify_solution( $challenge, $solution ); - - if ( is_wp_error( $result ) ) { - return new WP_Error( - $result->get_error_code(), - $result->get_error_message(), - array( 'status' => 403 ) - ); - } - - return $prepared_comment; - } - - /** - * Read and sanitize a plugin parameter from a REST request. - * - * @param object $request The request object. - * @param string $param Parameter name. - * @return string - */ - private function get_sanitized_rest_param( $request, $param ) { - $value = $request->get_param( $param ); - return sanitize_text_field( wp_unslash( is_scalar( $value ) ? (string) $value : '' ) ); - } - - /** - * Verify PoW on comment submission. - * - * @param array $commentdata Comment data. - * @return array|WP_Error - */ - public function verify_comment_pow( $commentdata ) { - if ( current_user_can( 'moderate_comments' ) ) { - return $commentdata; - } - - $comment_type = isset( $commentdata['comment_type'] ) ? $commentdata['comment_type'] : ''; - if ( in_array( $comment_type, array( 'pingback', 'trackback' ), true ) ) { - return $commentdata; - } - - if ( is_user_logged_in() ) { - return $commentdata; - } - - $nonce = isset( $_POST['cardea_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['cardea_nonce'] ) ) : ''; - $timestamp = isset( $_POST['cardea_timestamp'] ) ? sanitize_text_field( wp_unslash( $_POST['cardea_timestamp'] ) ) : ''; - $salt = isset( $_POST['cardea_salt'] ) ? sanitize_text_field( wp_unslash( $_POST['cardea_salt'] ) ) : ''; - $solution = isset( $_POST['cardea_solution'] ) ? sanitize_text_field( wp_unslash( $_POST['cardea_solution'] ) ) : ''; - $signature = isset( $_POST['cardea_signature'] ) ? sanitize_text_field( wp_unslash( $_POST['cardea_signature'] ) ) : ''; - - if ( empty( $nonce ) || empty( $timestamp ) || empty( $salt ) || empty( $solution ) ) { - $this->die_on_verification_failure(); - } - - if ( ! wp_verify_nonce( $nonce, 'cardea_challenge' ) ) { - $this->die_on_verification_failure(); - } - - $challenge = array( - 'nonce' => $nonce, - 'timestamp' => $timestamp, - 'salt' => $salt, - 'signature' => $signature, - 'difficulty' => $this->get_difficulty(), - ); - - $result = $this->verify_solution( $challenge, $solution ); - - if ( is_wp_error( $result ) ) { - $this->die_on_verification_failure(); - } - - return $commentdata; - } - - /** - * Terminate the request with the generic verification failure message. - * - * @codeCoverageIgnore - */ - private function die_on_verification_failure() { - wp_die( - esc_html( self::failure_message() ), - esc_html__( 'PoW Verification Failed', 'cardea' ), - array( 'response' => 403 ) - ); - } } diff --git a/tests/e2e/api-comments.spec.js b/tests/e2e/api-comments.spec.js index e5f06d3..7cc28f5 100644 --- a/tests/e2e/api-comments.spec.js +++ b/tests/e2e/api-comments.spec.js @@ -22,7 +22,7 @@ */ const { test, expect } = require('@playwright/test'); -const { runCLI } = require('@wp-playground/cli'); +const { startPlayground } = require('./support/playground'); const crypto = require('crypto'); /** @@ -50,43 +50,7 @@ function solveChallenge(challenge) { let cli; test.beforeAll(async () => { - cli = await runCLI({ - command: 'server', - php: '8.3', - wp: 'latest', - login: false, - mount: [ - { - hostPath: './', - vfsPath: '/wordpress/wp-content/plugins/cardea', - }, - ], - blueprint: { - steps: [ - { - step: 'activatePlugin', - pluginPath: '/wordpress/wp-content/plugins/cardea/cardea.php', - }, - { - step: 'writeFile', - path: '/wordpress/wp-content/mu-plugins/disable-flood-check.php', - data: ' 'Test Post for REST API', - 'post_content' => 'This is a test post to verify REST API comments.', - 'post_status' => 'publish', - 'comment_status' => 'open', - ]); - `, - }, - ], - }, - }); + cli = await startPlayground( { postTitle: 'Test Post for REST API' }); }); test.afterAll(async () => { diff --git a/tests/e2e/pow-comment.spec.js b/tests/e2e/pow-comment.spec.js index f461dc9..5933408 100644 --- a/tests/e2e/pow-comment.spec.js +++ b/tests/e2e/pow-comment.spec.js @@ -22,48 +22,12 @@ */ const { test, expect } = require('@playwright/test'); -const { runCLI } = require('@wp-playground/cli'); +const { startPlayground } = require('./support/playground'); let cli; test.beforeAll(async () => { - cli = await runCLI({ - command: 'server', - php: '8.3', - wp: 'latest', - login: false, - mount: [ - { - hostPath: './', - vfsPath: '/wordpress/wp-content/plugins/cardea', - }, - ], - blueprint: { - steps: [ - { - step: 'activatePlugin', - pluginPath: '/wordpress/wp-content/plugins/cardea/cardea.php', - }, - { - step: 'writeFile', - path: '/wordpress/wp-content/mu-plugins/disable-flood-check.php', - data: ' 'Test Post for Comments', - 'post_content' => 'This is a test post to verify comment form.', - 'post_status' => 'publish', - 'comment_status' => 'open', - ]); - `, - }, - ], - }, - }); + cli = await startPlayground( { postTitle: 'Test Post for Comments' }); }); test.afterAll(async () => { @@ -278,53 +242,15 @@ test.describe('Cardea - Admin Dashboard Reply', () => { let adminCli; test.beforeAll(async () => { - adminCli = await runCLI({ - command: 'server', - php: '8.3', - wp: 'latest', - login: true, - adminLogin: true, - mount: [ - { - hostPath: './', - vfsPath: '/wordpress/wp-content/plugins/cardea', - }, - ], - blueprint: { - steps: [ - { - step: 'activatePlugin', - pluginPath: '/wordpress/wp-content/plugins/cardea/cardea.php', - }, - { - step: 'writeFile', - path: '/wordpress/wp-content/mu-plugins/disable-flood-check.php', - data: ' 'Test Post for Admin Reply', - 'post_content' => 'This is a test post for admin reply test.', - 'post_status' => 'publish', - 'comment_status' => 'open', - ]); - // Create a test comment - wp_insert_comment([ - 'comment_post_ID' => $post_id, - 'comment_content' => 'Test comment for admin reply test', - 'comment_author' => 'Test Commenter', - 'comment_author_email' => 'tester@test.com', - 'comment_approved' => 1, - ]); - `, - }, - ], - }, - }); - }); + adminCli = await startPlayground( { login: true, postTitle: 'Test Post for Admin Reply', extraRunPHP: ` wp_insert_comment([ + 'comment_post_ID' => $post_id, + 'comment_content' => 'Test comment for admin reply test', + 'comment_author' => 'Test Commenter', + 'comment_author_email' => 'tester@test.com', + 'comment_approved' => 1, + ]);` }); +}); + test.afterAll(async () => { if (adminCli) { diff --git a/tests/e2e/rest-challenge.spec.js b/tests/e2e/rest-challenge.spec.js index 2014590..19ddf5e 100644 --- a/tests/e2e/rest-challenge.spec.js +++ b/tests/e2e/rest-challenge.spec.js @@ -21,48 +21,12 @@ */ const { test, expect } = require('@playwright/test'); -const { runCLI } = require('@wp-playground/cli'); +const { startPlayground } = require('./support/playground'); let cli; test.beforeAll(async () => { - cli = await runCLI({ - command: 'server', - php: '8.3', - wp: 'latest', - login: false, - mount: [ - { - hostPath: './', - vfsPath: '/wordpress/wp-content/plugins/cardea', - }, - ], - blueprint: { - steps: [ - { - step: 'activatePlugin', - pluginPath: '/wordpress/wp-content/plugins/cardea/cardea.php', - }, - { - step: 'writeFile', - path: '/wordpress/wp-content/mu-plugins/disable-flood-check.php', - data: ' 'Test Post for REST Challenge', - 'post_content' => 'This is a test post.', - 'post_status' => 'publish', - 'comment_status' => 'open', - ]); - `, - }, - ], - }, - }); + cli = await startPlayground( { postTitle: 'Test Post for REST Challenge' }); }); test.afterAll(async () => { diff --git a/tests/e2e/support/playground.js b/tests/e2e/support/playground.js new file mode 100644 index 0000000..b96572f --- /dev/null +++ b/tests/e2e/support/playground.js @@ -0,0 +1,93 @@ +/** + * Cardea - Proof-of-Work Comment Spam Protection + * + * Copyright (C) 2024 Oleg Mikheev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * @package Cardea + */ + +/** + * Shared Playwright / WordPress Playground fixtures for the Cardea e2e suite. + * + * Every spec boots WordPress through @wp-playground/cli with the plugin + * mounted and activated. This helper is the single place that controls + * the WordPress version under test, so any release line can be exercised: + * + * WP_VERSION=7.0 make test-e2e + */ + +const { runCLI } = require('@wp-playground/cli'); + +/** WordPress version under test (defaults to the latest stable release). */ +const WP_VERSION = process.env.WP_VERSION || 'latest'; + +/** Disables the comment flood checks so rapid e2e submissions are not rate-limited. */ +const FLOOD_CHECK_BYPASS = '} The running CLI handle ({ serverUrl, [Symbol.asyncDispose] }). + */ +async function startPlayground({ login = false, postTitle = 'Test Post', extraRunPHP = '' } = {}) { + const options = { + command: 'server', + php: '8.3', + wp: WP_VERSION, + login: login, + mount: [ + { + hostPath: './', + vfsPath: '/wordpress/wp-content/plugins/cardea', + }, + ], + blueprint: { + steps: [ + { + step: 'activatePlugin', + pluginPath: '/wordpress/wp-content/plugins/cardea/cardea.php', + }, + { + step: 'writeFile', + path: '/wordpress/wp-content/mu-plugins/disable-flood-check.php', + data: FLOOD_CHECK_BYPASS, + }, + { + step: 'runPHP', + code: ` '${postTitle}', + 'post_content' => 'This is a test post.', + 'post_status' => 'publish', + 'comment_status' => 'open', + ]); + ${extraRunPHP} + `, + }, + ], + }, + }; + if (login) { + options.adminLogin = true; + } + + return runCLI(options); +} + +module.exports = { startPlayground, WP_VERSION }; diff --git a/tests/e2e/trackback-comments.spec.js b/tests/e2e/trackback-comments.spec.js index b5729c3..cf3af14 100644 --- a/tests/e2e/trackback-comments.spec.js +++ b/tests/e2e/trackback-comments.spec.js @@ -22,48 +22,12 @@ */ const { test, expect } = require('@playwright/test'); -const { runCLI } = require('@wp-playground/cli'); +const { startPlayground } = require('./support/playground'); let cli; test.beforeAll(async () => { - cli = await runCLI({ - command: 'server', - php: '8.3', - wp: 'latest', - login: false, - mount: [ - { - hostPath: './', - vfsPath: '/wordpress/wp-content/plugins/cardea', - }, - ], - blueprint: { - steps: [ - { - step: 'activatePlugin', - pluginPath: '/wordpress/wp-content/plugins/cardea/cardea.php', - }, - { - step: 'writeFile', - path: '/wordpress/wp-content/mu-plugins/disable-flood-check.php', - data: ' 'Test Post for Trackbacks', - 'post_content' => 'This is a test post to verify trackbacks.', - 'post_status' => 'publish', - 'comment_status' => 'open', - ]); - `, - }, - ], - }, - }); + cli = await startPlayground( { postTitle: 'Test Post for Trackbacks' }); }); test.afterAll(async () => { diff --git a/tests/e2e/xmlrpc-comments.spec.js b/tests/e2e/xmlrpc-comments.spec.js index 54cae42..3e613cc 100644 --- a/tests/e2e/xmlrpc-comments.spec.js +++ b/tests/e2e/xmlrpc-comments.spec.js @@ -22,48 +22,12 @@ */ const { test, expect } = require('@playwright/test'); -const { runCLI } = require('@wp-playground/cli'); +const { startPlayground } = require('./support/playground'); let cli; test.beforeAll(async () => { - cli = await runCLI({ - command: 'server', - php: '8.3', - wp: 'latest', - login: false, - mount: [ - { - hostPath: './', - vfsPath: '/wordpress/wp-content/plugins/cardea', - }, - ], - blueprint: { - steps: [ - { - step: 'activatePlugin', - pluginPath: '/wordpress/wp-content/plugins/cardea/cardea.php', - }, - { - step: 'writeFile', - path: '/wordpress/wp-content/mu-plugins/disable-flood-check.php', - data: ' 'Test Post for XML-RPC', - 'post_content' => 'This is a test post to verify XML-RPC comments.', - 'post_status' => 'publish', - 'comment_status' => 'open', - ]); - `, - }, - ], - }, - }); + cli = await startPlayground( { postTitle: 'Test Post for XML-RPC' }); }); test.afterAll(async () => { diff --git a/tests/phpunit/Cardea_Comment_Gate_Test.php b/tests/phpunit/Cardea_Comment_Gate_Test.php new file mode 100644 index 0000000..3e410c3 --- /dev/null +++ b/tests/phpunit/Cardea_Comment_Gate_Test.php @@ -0,0 +1,425 @@ +core = new Cardea_Core(); + $this->gate = new Cardea_Comment_Gate( $this->core ); + } + + /** + * Test form submission with missing fields fails with the generic message. + */ + public function test_verify_form_submission_missing_fields() { + $_POST['cardea_nonce'] = ''; + $_POST['cardea_timestamp'] = '1234567890'; + $_POST['cardea_salt'] = 'testsalt'; + $_POST['cardea_solution'] = ''; + $_POST['cardea_signature'] = 'testsig'; + + $this->expectException( Exception::class ); + $this->expectExceptionMessage( Cardea_Core::failure_message() ); + + $this->gate->verify_form_submission( array() ); + } + + /** + * Test form submission with an invalid nonce fails with the generic message. + */ + public function test_verify_form_submission_invalid_nonce() { + $_POST['cardea_nonce'] = 'invalid_nonce'; + $_POST['cardea_timestamp'] = (string) time(); + $_POST['cardea_salt'] = 'testsalt'; + $_POST['cardea_solution'] = '12345'; + $_POST['cardea_signature'] = 'testsig'; + + $this->expectException( Exception::class ); + $this->expectExceptionMessage( Cardea_Core::failure_message() ); + + $this->gate->verify_form_submission( array() ); + } + + /** + * Test form submission with an invalid solution fails with the generic message. + */ + public function test_verify_form_submission_invalid_solution() { + $challenge = $this->core->generate_challenge( 1 ); + + $_POST['cardea_nonce'] = $challenge['nonce']; + $_POST['cardea_timestamp'] = (string) $challenge['timestamp']; + $_POST['cardea_salt'] = $challenge['salt']; + $_POST['cardea_solution'] = 'invalid'; + $_POST['cardea_signature'] = $challenge['signature']; + + $this->expectException( Exception::class ); + $this->expectExceptionMessage( Cardea_Core::failure_message() ); + + $this->gate->verify_form_submission( array() ); + } + + /** + * Test form submission accepts a valid submission. + */ + public function test_verify_form_submission_success() { + $challenge = $this->core->generate_challenge( 1 ); + $solution = $this->find_solution( $this->core->build_challenge_string( $challenge ), $challenge['difficulty'] ); + + $_POST['cardea_nonce'] = $challenge['nonce']; + $_POST['cardea_timestamp'] = (string) $challenge['timestamp']; + $_POST['cardea_salt'] = $challenge['salt']; + $_POST['cardea_solution'] = $solution; + $_POST['cardea_signature'] = $challenge['signature']; + + $commentdata = array( 'comment_post_ID' => 1 ); + $result = $this->gate->verify_form_submission( $commentdata ); + + $this->assertEquals( $commentdata, $result ); + } + + /** + * Test form submission bypasses for users with moderate_comments capability. + */ + public function test_verify_form_submission_bypasses_for_moderator() { + global $current_user; + $current_user = new WP_User(); + $current_user->caps = array( 'moderate_comments' => true ); + + $_POST['cardea_nonce'] = ''; + + $commentdata = array( 'comment_post_ID' => 1 ); + $result = $this->gate->verify_form_submission( $commentdata ); + + $this->assertEquals( $commentdata, $result ); + + $current_user = null; + $_POST = array(); + } + + /** + * Test form submission bypasses for pingback comment type. + */ + public function test_verify_form_submission_bypasses_for_pingback() { + $_POST['cardea_nonce'] = ''; + + $commentdata = array( + 'comment_post_ID' => 1, + 'comment_type' => 'pingback', + ); + $result = $this->gate->verify_form_submission( $commentdata ); + + $this->assertEquals( $commentdata, $result ); + + $_POST = array(); + } + + /** + * Test form submission bypasses for trackback comment type. + */ + public function test_verify_form_submission_bypasses_for_trackback() { + $_POST['cardea_nonce'] = ''; + + $commentdata = array( + 'comment_post_ID' => 1, + 'comment_type' => 'trackback', + ); + $result = $this->gate->verify_form_submission( $commentdata ); + + $this->assertEquals( $commentdata, $result ); + + $_POST = array(); + } + + /** + * Test form submission bypasses for logged-in users. + */ + public function test_verify_form_submission_bypasses_for_logged_in_user() { + global $current_user; + $current_user = new WP_User(); + + $_POST['cardea_nonce'] = ''; + + $commentdata = array( + 'comment_post_ID' => 1, + 'comment_type' => 'comment', + ); + $result = $this->gate->verify_form_submission( $commentdata ); + + $this->assertEquals( $commentdata, $result ); + + $current_user = null; + $_POST = array(); + } + + /** + * Test form submission validates for logged-out users with comment type 'comment'. + */ + public function test_verify_form_submission_validates_for_logged_out_comment_type() { + $_POST['cardea_nonce'] = ''; + $_POST['cardea_timestamp'] = '1234567890'; + $_POST['cardea_salt'] = 'testsalt'; + $_POST['cardea_solution'] = ''; + $_POST['cardea_signature'] = 'testsig'; + + $commentdata = array( + 'comment_post_ID' => 1, + 'comment_type' => 'comment', + ); + + $this->expectException( Exception::class ); + $this->expectExceptionMessage( Cardea_Core::failure_message() ); + + $this->gate->verify_form_submission( $commentdata ); + } + + /** + * Test REST submission bypasses for moderators. + */ + public function test_verify_rest_submission_bypasses_for_moderator() { + global $current_user; + $current_user = new WP_User(); + $current_user->caps = array( 'moderate_comments' => true ); + + $request = $this->createMockWP_REST_Request( 'comment', 1 ); + $result = $this->gate->verify_rest_submission( array(), $request ); + + $this->assertEquals( array(), $result ); + + $current_user = null; + } + + /** + * Test REST submission bypasses for pingbacks. + */ + public function test_verify_rest_submission_bypasses_for_pingback() { + $request = $this->createMockWP_REST_Request( 'pingback', 1 ); + $result = $this->gate->verify_rest_submission( array(), $request ); + + $this->assertEquals( array(), $result ); + } + + /** + * Test REST submission bypasses for trackbacks. + */ + public function test_verify_rest_submission_bypasses_for_trackback() { + $request = $this->createMockWP_REST_Request( 'trackback', 1 ); + $result = $this->gate->verify_rest_submission( array(), $request ); + + $this->assertEquals( array(), $result ); + } + + /** + * Test REST submission bypasses for logged-in users. + */ + public function test_verify_rest_submission_bypasses_for_logged_in_user() { + global $current_user; + $current_user = new WP_User(); + + $request = $this->createMockWP_REST_Request( 'comment', 1 ); + $result = $this->gate->verify_rest_submission( array(), $request ); + + $this->assertEquals( array(), $result ); + + $current_user = null; + } + + /** + * Test REST submission rejects anonymous users without challenge fields. + */ + public function test_verify_rest_submission_rejects_missing_fields() { + $request = $this->createMockWP_REST_Request( 'comment', 1 ); + $result = $this->gate->verify_rest_submission( array(), $request ); + + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertEquals( 'cardea_verification_failed', $result->get_error_code() ); + $this->assertEquals( 403, $this->error_status( $result ) ); + } + + /** + * Test REST submission rejects anonymous submissions with an invalid nonce. + */ + public function test_verify_rest_submission_rejects_invalid_nonce() { + $request = $this->createMockWP_REST_Request( + 'comment', + 1, + array( + 'cardea_nonce' => 'invalid_nonce', + 'cardea_timestamp' => (string) time(), + 'cardea_salt' => 'testsalt', + 'cardea_solution' => '12345', + 'cardea_signature' => 'testsig', + ) + ); + + $result = $this->gate->verify_rest_submission( array(), $request ); + + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertEquals( 'cardea_verification_failed', $result->get_error_code() ); + } + + /** + * Test REST submission rejects anonymous submissions with an invalid solution. + */ + public function test_verify_rest_submission_rejects_invalid_solution() { + $challenge = $this->core->generate_challenge( 1 ); + + $request = $this->createMockWP_REST_Request( + 'comment', + 1, + array( + 'cardea_nonce' => $challenge['nonce'], + 'cardea_timestamp' => (string) $challenge['timestamp'], + 'cardea_salt' => $challenge['salt'], + 'cardea_solution' => 'invalid', + 'cardea_signature' => $challenge['signature'], + ) + ); + + $result = $this->gate->verify_rest_submission( array(), $request ); + + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertEquals( 'cardea_verification_failed', $result->get_error_code() ); + } + + /** + * Test REST submission accepts anonymous submissions with a valid PoW solution. + */ + public function test_verify_rest_submission_accepts_valid_solution() { + $challenge = $this->core->generate_challenge( 1 ); + $solution = $this->find_solution( $this->core->build_challenge_string( $challenge ), $challenge['difficulty'] ); + + $request = $this->createMockWP_REST_Request( + 'comment', + 1, + array( + 'cardea_nonce' => $challenge['nonce'], + 'cardea_timestamp' => (string) $challenge['timestamp'], + 'cardea_salt' => $challenge['salt'], + 'cardea_solution' => $solution, + 'cardea_signature' => $challenge['signature'], + ) + ); + + $prepared = array( 'post_id' => 1 ); + $result = $this->gate->verify_rest_submission( $prepared, $request ); + + $this->assertEquals( $prepared, $result ); + } + + /** + * Read the status from a WP_Error data payload (helper for assertions). + * + * @param WP_Error $error The error. + * @return int + */ + private function error_status( $error ) { + $data = $error->get_error_data(); + return is_array( $data ) && isset( $data['status'] ) ? (int) $data['status'] : 0; + } + + /** + * Create a mock WP_REST_Request object. + * + * @param string $comment_type Optional comment type to configure. + * @param int $post_id Optional post ID to configure. + * @param array $params Optional map of additional request parameters. + * @return object + */ + private function createMockWP_REST_Request( $comment_type = null, $post_id = null, $params = array() ) { + return new class( $comment_type, $post_id, $params ) { + private $comment_type; + private $post_id; + private $params; + + public function __construct( $comment_type = null, $post_id = null, $params = array() ) { + $this->comment_type = $comment_type; + $this->post_id = $post_id; + $this->params = $params; + } + + public function get_param( $param ) { + if ( 'comment_type' === $param ) { + return $this->comment_type; + } + if ( 'post_id' === $param ) { + return $this->post_id; + } + return isset( $this->params[ $param ] ) ? $this->params[ $param ] : null; + } + }; + } + + /** + * Find a valid solution for the given challenge (for testing). + * + * @param string $challenge Challenge string. + * @param int $difficulty Difficulty level. + * @return string The solution. + */ + private function find_solution( $challenge, $difficulty ) { + $counter = 0; + while ( true ) { + $hash = hash( 'sha256', $challenge . $counter ); + $prefix = str_repeat( '0', $difficulty ); + if ( strpos( $hash, $prefix ) === 0 ) { + return (string) $counter; + } + $counter++; + if ( $counter > 1000000 ) { + $this->fail( 'Could not find solution within timeout' ); + } + } + } +} diff --git a/tests/phpunit/Cardea_Core_Test.php b/tests/phpunit/Cardea_Core_Test.php index e5180d3..0173657 100644 --- a/tests/phpunit/Cardea_Core_Test.php +++ b/tests/phpunit/Cardea_Core_Test.php @@ -207,353 +207,46 @@ public function test_replay_attack_prevention() { } /** - * Test verify_comment_pow triggers wp_die for missing fields. - */ - public function test_verify_comment_pow_missing_fields() { - $_POST['cardea_nonce'] = ''; - $_POST['cardea_timestamp'] = '1234567890'; - $_POST['cardea_salt'] = 'testsalt'; - $_POST['cardea_solution'] = ''; - $_POST['cardea_signature'] = 'testsig'; - - $this->expectException( Exception::class ); - $this->expectExceptionMessage( Cardea_Core::failure_message() ); - - $this->core->verify_comment_pow( array() ); - } - - /** - * Test verify_comment_pow triggers wp_die for invalid nonce. - */ - public function test_verify_comment_pow_invalid_nonce() { - $_POST['cardea_nonce'] = 'invalid_nonce'; - $_POST['cardea_timestamp'] = (string) time(); - $_POST['cardea_salt'] = 'testsalt'; - $_POST['cardea_solution'] = '12345'; - $_POST['cardea_signature'] = 'testsig'; - - $this->expectException( Exception::class ); - $this->expectExceptionMessage( Cardea_Core::failure_message() ); - - $this->core->verify_comment_pow( array() ); - } - - /** - * Test verify_comment_pow triggers wp_die for invalid solution. - */ - public function test_verify_comment_pow_invalid_solution() { - $challenge = $this->core->generate_challenge( 1 ); - - $_POST['cardea_nonce'] = $challenge['nonce']; - $_POST['cardea_timestamp'] = (string) $challenge['timestamp']; - $_POST['cardea_salt'] = $challenge['salt']; - $_POST['cardea_solution'] = 'invalid'; - $_POST['cardea_signature'] = $challenge['signature']; - - $this->expectException( Exception::class ); - $this->expectExceptionMessage( Cardea_Core::failure_message() ); - - $this->core->verify_comment_pow( array() ); - } - - /** - * Test verify_comment_pow accepts valid submission. - */ - public function test_verify_comment_pow_success() { - $challenge = $this->core->generate_challenge( 1 ); - $challenge_string = $this->core->build_challenge_string( $challenge ); - $solution = $this->find_solution( $challenge_string, $challenge['difficulty'] ); - - $_POST['cardea_nonce'] = $challenge['nonce']; - $_POST['cardea_timestamp'] = (string) $challenge['timestamp']; - $_POST['cardea_salt'] = $challenge['salt']; - $_POST['cardea_solution'] = $solution; - $_POST['cardea_signature'] = $challenge['signature']; - - $commentdata = array( 'comment_post_ID' => 1 ); - $result = $this->core->verify_comment_pow( $commentdata ); - - $this->assertEquals( $commentdata, $result ); - - // Clean up - $_POST = array(); - } - - /** - * Test verify_comment_pow bypasses for users with moderate_comments capability. - */ - public function test_verify_comment_pow_bypasses_for_moderator() { - global $current_user; - $current_user = new WP_User(); - $current_user->caps = array( 'moderate_comments' => true ); - - $_POST['cardea_nonce'] = ''; - - $commentdata = array( 'comment_post_ID' => 1 ); - $result = $this->core->verify_comment_pow( $commentdata ); - - $this->assertEquals( $commentdata, $result ); - - $_POST = array(); - $current_user = null; - } - - /** - * Test verify_comment_pow bypasses for pingback comment type. - */ - public function test_verify_comment_pow_bypasses_for_pingback() { - global $current_user; - $current_user = null; - - $_POST['cardea_nonce'] = ''; - - $commentdata = array( - 'comment_post_ID' => 1, - 'comment_type' => 'pingback', - ); - $result = $this->core->verify_comment_pow( $commentdata ); - - $this->assertEquals( $commentdata, $result ); - - $_POST = array(); - } - - /** - * Test verify_comment_pow bypasses for trackback comment type. - */ - public function test_verify_comment_pow_bypasses_for_trackback() { - global $current_user; - $current_user = null; - - $_POST['cardea_nonce'] = ''; - - $commentdata = array( - 'comment_post_ID' => 1, - 'comment_type' => 'trackback', - ); - $result = $this->core->verify_comment_pow( $commentdata ); - - $this->assertEquals( $commentdata, $result ); - - $_POST = array(); - } - - /** - * Test verify_comment_pow bypasses for logged-in user. - */ - public function test_verify_comment_pow_bypasses_for_logged_in_user() { - global $current_user; - $current_user = new WP_User(); - - $_POST['cardea_nonce'] = ''; - - $commentdata = array( - 'comment_post_ID' => 1, - 'comment_type' => 'comment', - ); - $result = $this->core->verify_comment_pow( $commentdata ); - - $this->assertEquals( $commentdata, $result ); - - $_POST = array(); - $current_user = null; - } - - /** - * Test verify_comment_pow validates for logged-out user with comment type 'comment'. - */ - public function test_verify_comment_pow_validates_for_logged_out_comment_type() { - global $current_user; - $current_user = null; - - $_POST['cardea_nonce'] = ''; - $_POST['cardea_timestamp'] = '1234567890'; - $_POST['cardea_salt'] = 'testsalt'; - $_POST['cardea_solution'] = ''; - $_POST['cardea_signature'] = 'testsig'; - - $commentdata = array( - 'comment_post_ID' => 1, - 'comment_type' => 'comment', - ); - - $this->expectException( Exception::class ); - $this->expectExceptionMessage( Cardea_Core::failure_message() ); - - $this->core->verify_comment_pow( $commentdata ); - - $_POST = array(); - } - - /** - * Test verify_rest_comment bypasses for moderators. - */ - public function test_verify_rest_comment_bypasses_for_moderator() { - global $current_user; - $current_user = new WP_User(); - $current_user->caps = array( 'moderate_comments' => true ); - - $request = $this->createMockWP_REST_Request(); - $result = $this->core->verify_rest_comment( array(), $request ); - - $this->assertEquals( array(), $result ); - - $current_user = null; - } - - /** - * Test verify_rest_comment bypasses for pingbacks. - */ - public function test_verify_rest_comment_bypasses_for_pingback() { - global $current_user; - $current_user = null; - - $request = $this->createMockWP_REST_Request( 'pingback' ); - $result = $this->core->verify_rest_comment( array(), $request ); - - $this->assertEquals( array(), $result ); - } - - /** - * Test verify_rest_comment bypasses for trackbacks. + * Test rest_get_challenge returns valid challenge data. */ - public function test_verify_rest_comment_bypasses_for_trackback() { - global $current_user; - $current_user = null; - - $request = $this->createMockWP_REST_Request( 'trackback' ); - $result = $this->core->verify_rest_comment( array(), $request ); + public function test_rest_get_challenge() { + $request = $this->createMockWP_REST_Request( null, 1 ); + $result = $this->core->rest_get_challenge( $request ); - $this->assertEquals( array(), $result ); + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'nonce', $result ); + $this->assertArrayHasKey( 'timestamp', $result ); + $this->assertArrayHasKey( 'salt', $result ); + $this->assertArrayHasKey( 'signature', $result ); + $this->assertArrayHasKey( 'difficulty', $result ); } /** - * Test verify_rest_comment bypasses for logged-in users. + * Test rest_get_challenge works without post_id. */ - public function test_verify_rest_comment_bypasses_for_logged_in_user() { - global $current_user; - $current_user = new WP_User(); - + public function test_rest_get_challenge_without_post_id() { $request = $this->createMockWP_REST_Request(); - $result = $this->core->verify_rest_comment( array(), $request ); - - $this->assertEquals( array(), $result ); - - $current_user = null; - } - - /** - * Test verify_rest_comment blocks anonymous users without challenge fields. - */ - public function test_verify_rest_comment_blocks_unauthenticated() { - global $current_user; - $current_user = null; - - $request = $this->createMockWP_REST_Request( 'comment' ); - $result = $this->core->verify_rest_comment( array(), $request ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'cardea_missing_fields', $result->get_error_code() ); - } - - /** - * Test verify_rest_comment accepts anonymous submissions with a valid PoW solution. - */ - public function test_verify_rest_comment_accepts_valid_solution() { - global $current_user; - $current_user = null; - - $challenge = $this->core->generate_challenge( 1 ); - $solution = $this->find_solution( $this->core->build_challenge_string( $challenge ), $challenge['difficulty'] ); - - $request = $this->createMockWP_REST_Request( - 'comment', - 1, - array( - 'cardea_nonce' => $challenge['nonce'], - 'cardea_timestamp' => (string) $challenge['timestamp'], - 'cardea_salt' => $challenge['salt'], - 'cardea_solution' => $solution, - 'cardea_signature' => $challenge['signature'], - ) - ); - - $prepared = array( 'post_id' => 1 ); - $result = $this->core->verify_rest_comment( $prepared, $request ); - - $this->assertEquals( $prepared, $result ); - } - - /** - * Test verify_rest_comment rejects anonymous submissions with an invalid solution. - */ - public function test_verify_rest_comment_rejects_invalid_solution() { - global $current_user; - $current_user = null; - - $challenge = $this->core->generate_challenge( 1 ); - - $request = $this->createMockWP_REST_Request( - 'comment', - 1, - array( - 'cardea_nonce' => $challenge['nonce'], - 'cardea_timestamp' => (string) $challenge['timestamp'], - 'cardea_salt' => $challenge['salt'], - 'cardea_solution' => 'invalid', - 'cardea_signature' => $challenge['signature'], - ) - ); - - $result = $this->core->verify_rest_comment( array(), $request ); - - $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'cardea_invalid', $result->get_error_code() ); - } - - /** - * Test verify_rest_comment rejects anonymous submissions with an invalid nonce. - */ - public function test_verify_rest_comment_rejects_invalid_nonce() { - global $current_user; - $current_user = null; - - $request = $this->createMockWP_REST_Request( - 'comment', - 1, - array( - 'cardea_nonce' => 'invalid_nonce', - 'cardea_timestamp' => (string) time(), - 'cardea_salt' => 'testsalt', - 'cardea_solution' => '12345', - 'cardea_signature' => 'testsig', - ) - ); - - $result = $this->core->verify_rest_comment( array(), $request ); + $result = $this->core->rest_get_challenge( $request ); - $this->assertInstanceOf( WP_Error::class, $result ); - $this->assertEquals( 'cardea_security_check', $result->get_error_code() ); + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'nonce', $result ); } /** - * Create a mock WP_REST_Request object. + * Create a minimal mock request object (for the REST challenge endpoint). * * @param string $comment_type Optional comment type to configure. * @param int $post_id Optional post ID to configure. - * @param array $params Optional map of additional request parameters. * @return object */ - private function createMockWP_REST_Request( $comment_type = null, $post_id = null, $params = array() ) { - return new class( $comment_type, $post_id, $params ) { + private function createMockWP_REST_Request( $comment_type = null, $post_id = null ) { + return new class( $comment_type, $post_id ) { private $comment_type; private $post_id; - private $params; - public function __construct( $comment_type = null, $post_id = null, $params = array() ) { + public function __construct( $comment_type = null, $post_id = null ) { $this->comment_type = $comment_type; - $this->post_id = $post_id; - $this->params = $params; + $this->post_id = $post_id; } public function get_param( $param ) { @@ -563,37 +256,11 @@ public function get_param( $param ) { if ( $param === 'post_id' ) { return $this->post_id; } - return isset( $this->params[ $param ] ) ? $this->params[ $param ] : null; + return null; } }; } - /** - * Test rest_get_challenge returns valid challenge data. - */ - public function test_rest_get_challenge() { - $request = $this->createMockWP_REST_Request( null, 1 ); - $result = $this->core->rest_get_challenge( $request ); - - $this->assertIsArray( $result ); - $this->assertArrayHasKey( 'nonce', $result ); - $this->assertArrayHasKey( 'timestamp', $result ); - $this->assertArrayHasKey( 'salt', $result ); - $this->assertArrayHasKey( 'signature', $result ); - $this->assertArrayHasKey( 'difficulty', $result ); - } - - /** - * Test rest_get_challenge works without post_id. - */ - public function test_rest_get_challenge_without_post_id() { - $request = $this->createMockWP_REST_Request(); - $result = $this->core->rest_get_challenge( $request ); - - $this->assertIsArray( $result ); - $this->assertArrayHasKey( 'nonce', $result ); - } - /** * Find a valid solution for the given challenge (for testing). * diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index 9b91012..d9db20f 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -24,13 +24,24 @@ define( 'ABSPATH', '/var/www/html/' ); define( 'CARDEA_PLUGIN_DIR', __DIR__ . '/../../' ); define( 'CARDEA_PLUGIN_URL', 'https://example.com/wp-content/plugins/cardea/' ); -define( 'CARDEA_VERSION', '1.0.1' ); define( 'CARDEA_DEFAULT_DIFFICULTY', 4 ); define( 'CARDEA_DEFAULT_WINDOW', 30 ); +/* + * Single source of truth: the plugin version is parsed from the cardea.php + * header instead of being re-typed here. + */ +$cardea_header = file_get_contents( CARDEA_PLUGIN_DIR . 'cardea.php' ); +if ( preg_match( '/^\s*\*\s*Version:\s*(\S+)/m', $cardea_header, $cardea_matches ) ) { + define( 'CARDEA_VERSION', $cardea_matches[1] ); +} else { + define( 'CARDEA_VERSION', '0.0.0' ); +} + require_once __DIR__ . '/../../vendor/autoload.php'; require_once CARDEA_PLUGIN_DIR . 'includes/class-cardea-core.php'; +require_once CARDEA_PLUGIN_DIR . 'includes/class-cardea-comment-gate.php'; function get_option( $option, $default = false ) { global $wp_options;