feat: add connection diagnostics
This commit is contained in:
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Handles admin REST connection diagnostics.
|
||||||
|
*
|
||||||
|
* @package WPContentSync
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPContentSync\Admin;
|
||||||
|
|
||||||
|
use WPContentSync\Logging\LoggerInterface;
|
||||||
|
use WPContentSync\Settings\SettingsRepository;
|
||||||
|
use WPContentSync\Transport\RestTransportClient;
|
||||||
|
use WPContentSync\Transport\RestTransportException;
|
||||||
|
|
||||||
|
final class ConnectionTestController {
|
||||||
|
private SettingsRepository $settings_repository;
|
||||||
|
|
||||||
|
private RestTransportClient $transport_client;
|
||||||
|
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
SettingsRepository $settings_repository,
|
||||||
|
RestTransportClient $transport_client,
|
||||||
|
LoggerInterface $logger
|
||||||
|
) {
|
||||||
|
$this->settings_repository = $settings_repository;
|
||||||
|
$this->transport_client = $transport_client;
|
||||||
|
$this->logger = $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(): void {
|
||||||
|
add_action( 'admin_post_wpcs_test_connection', array( $this, 'handleTest' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleTest(): void {
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
throw new \RuntimeException( 'You do not have permission to test WP Content Sync connections.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! check_admin_referer( 'wpcs_test_connection', 'wpcs_connection_nonce' ) ) {
|
||||||
|
throw new \RuntimeException( 'The connection test request could not be verified.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above.
|
||||||
|
$pair_index = isset( $_POST['pair_index'] ) ? max( 0, (int) sanitize_text_field( wp_unslash( $_POST['pair_index'] ) ) ) : 0;
|
||||||
|
$pairs = $this->settings_repository->get()->syncPairs();
|
||||||
|
$pair = $pairs[ $pair_index ] ?? null;
|
||||||
|
|
||||||
|
if ( ! is_array( $pair ) ) {
|
||||||
|
$this->redirectWithError( 'The selected sync pair could not be found.' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->transport_client->testConnection(
|
||||||
|
(string) $pair['destination_url'],
|
||||||
|
(string) $pair['username'],
|
||||||
|
(string) $pair['application_password']
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->logger->info(
|
||||||
|
'REST connection test succeeded.',
|
||||||
|
array(
|
||||||
|
'pair_index' => $pair_index,
|
||||||
|
'destination_url' => (string) $pair['destination_url'],
|
||||||
|
'username' => (string) $pair['username'],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_safe_redirect(
|
||||||
|
add_query_arg(
|
||||||
|
array( 'wpcs_connection_ok' => '1' ),
|
||||||
|
admin_url( 'admin.php?page=wp-content-sync' )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch ( RestTransportException $exception ) {
|
||||||
|
$this->logger->warning(
|
||||||
|
'REST connection test failed.',
|
||||||
|
array(
|
||||||
|
'pair_index' => $pair_index,
|
||||||
|
'destination_url' => (string) $pair['destination_url'],
|
||||||
|
'username' => (string) $pair['username'],
|
||||||
|
'failure_code' => $exception->failureCode(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->redirectWithError( $exception->getMessage() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function redirectWithError( string $message ): void {
|
||||||
|
wp_safe_redirect(
|
||||||
|
add_query_arg(
|
||||||
|
array( 'wpcs_connection_error' => sanitize_text_field( $message ) ),
|
||||||
|
admin_url( 'admin.php?page=wp-content-sync' )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
namespace WPContentSync;
|
namespace WPContentSync;
|
||||||
|
|
||||||
use WPContentSync\Admin\AdminPage;
|
use WPContentSync\Admin\AdminPage;
|
||||||
|
use WPContentSync\Admin\ConnectionTestController;
|
||||||
use WPContentSync\Admin\FileImportController;
|
use WPContentSync\Admin\FileImportController;
|
||||||
use WPContentSync\Admin\SettingsController;
|
use WPContentSync\Admin\SettingsController;
|
||||||
use WPContentSync\Content\ContentHandlerRegistry;
|
use WPContentSync\Content\ContentHandlerRegistry;
|
||||||
@@ -186,6 +187,17 @@ final class Plugin {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$container->factory(
|
||||||
|
ConnectionTestController::class,
|
||||||
|
static function () use ( $container ): ConnectionTestController {
|
||||||
|
return new ConnectionTestController(
|
||||||
|
$container->get( SettingsRepository::class ),
|
||||||
|
$container->get( RestTransportClient::class ),
|
||||||
|
$container->get( LoggerInterface::class )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
$container->factory(
|
$container->factory(
|
||||||
RestPackageController::class,
|
RestPackageController::class,
|
||||||
static function () use ( $container ): RestPackageController {
|
static function () use ( $container ): RestPackageController {
|
||||||
@@ -222,9 +234,13 @@ final class Plugin {
|
|||||||
/** @var SettingsController $settings_controller */
|
/** @var SettingsController $settings_controller */
|
||||||
$settings_controller = $this->container->get( SettingsController::class );
|
$settings_controller = $this->container->get( SettingsController::class );
|
||||||
|
|
||||||
|
/** @var ConnectionTestController $connection_test_controller */
|
||||||
|
$connection_test_controller = $this->container->get( ConnectionTestController::class );
|
||||||
|
|
||||||
$admin_page->register();
|
$admin_page->register();
|
||||||
$file_import_controller->register();
|
$file_import_controller->register();
|
||||||
$rest_package_controller->register();
|
$rest_package_controller->register();
|
||||||
$settings_controller->register();
|
$settings_controller->register();
|
||||||
|
$connection_test_controller->register();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,20 @@ if ( array() === $sync_pairs ) {
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Displays redirect status only. ?>
|
||||||
|
<?php if ( isset( $_GET['wpcs_connection_ok'] ) ) : ?>
|
||||||
|
<div class="notice notice-success">
|
||||||
|
<p><?php echo esc_html__( 'REST connection succeeded.', 'wp-content-sync' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Displays redirect status only. ?>
|
||||||
|
<?php if ( isset( $_GET['wpcs_connection_error'] ) ) : ?>
|
||||||
|
<div class="notice notice-error">
|
||||||
|
<p><?php echo esc_html( sanitize_text_field( wp_unslash( $_GET['wpcs_connection_error'] ) ) ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<h2><?php echo esc_html__( 'Configuration', 'wp-content-sync' ); ?></h2>
|
<h2><?php echo esc_html__( 'Configuration', 'wp-content-sync' ); ?></h2>
|
||||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
||||||
<input type="hidden" name="action" value="wpcs_save_settings" />
|
<input type="hidden" name="action" value="wpcs_save_settings" />
|
||||||
@@ -176,6 +190,26 @@ if ( array() === $sync_pairs ) {
|
|||||||
<?php submit_button( __( 'Save Settings', 'wp-content-sync' ) ); ?>
|
<?php submit_button( __( 'Save Settings', 'wp-content-sync' ) ); ?>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h2><?php echo esc_html__( 'Connection Diagnostics', 'wp-content-sync' ); ?></h2>
|
||||||
|
<p><?php echo esc_html__( 'Test whether each configured destination site accepts REST requests with the saved application password.', 'wp-content-sync' ); ?></p>
|
||||||
|
<?php foreach ( $sync_pairs as $index => $pair ) : ?>
|
||||||
|
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
||||||
|
<input type="hidden" name="action" value="wpcs_test_connection" />
|
||||||
|
<input type="hidden" name="pair_index" value="<?php echo esc_attr( (string) $index ); ?>" />
|
||||||
|
<?php wp_nonce_field( 'wpcs_test_connection', 'wpcs_connection_nonce' ); ?>
|
||||||
|
<p>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: sync pair name. */
|
||||||
|
esc_html__( 'Pair: %s', 'wp-content-sync' ),
|
||||||
|
esc_html( '' !== $pair['name'] ? $pair['name'] : __( 'Unnamed sync pair', 'wp-content-sync' ) )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
<?php submit_button( __( 'Test REST Connection', 'wp-content-sync' ), 'secondary' ); ?>
|
||||||
|
</form>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<h2><?php echo esc_html__( 'File Package Import', 'wp-content-sync' ); ?></h2>
|
<h2><?php echo esc_html__( 'File Package Import', 'wp-content-sync' ); ?></h2>
|
||||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
|
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="action" value="wpcs_import_package" />
|
<input type="hidden" name="action" value="wpcs_import_package" />
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Tests for admin REST connection diagnostics.
|
||||||
|
*
|
||||||
|
* @package WPContentSync
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPContentSync\Tests\Unit\Admin;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use WPContentSync\Admin\ConnectionTestController;
|
||||||
|
use WPContentSync\Logging\OptionLogger;
|
||||||
|
use WPContentSync\Settings\SettingsRepository;
|
||||||
|
use WPContentSync\Transport\RestTransportClient;
|
||||||
|
|
||||||
|
class ConnectionTestControllerTest extends TestCase {
|
||||||
|
protected function tearDown(): void {
|
||||||
|
unset(
|
||||||
|
$GLOBALS['wpcs_current_user_can'],
|
||||||
|
$GLOBALS['wpcs_nonce_valid'],
|
||||||
|
$GLOBALS['wpcs_redirect_location'],
|
||||||
|
$GLOBALS['wpcs_test_options'],
|
||||||
|
$GLOBALS['wpcs_test_option_autoloads'],
|
||||||
|
$GLOBALS['wpcs_http_response'],
|
||||||
|
$GLOBALS['wpcs_last_http_request']
|
||||||
|
);
|
||||||
|
|
||||||
|
$_POST = array();
|
||||||
|
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_tests_a_configured_pair_with_nonce_and_capability(): void {
|
||||||
|
$this->storePair();
|
||||||
|
$GLOBALS['wpcs_current_user_can']['manage_options'] = true;
|
||||||
|
$GLOBALS['wpcs_nonce_valid']['wpcs_test_connection']['wpcs_connection_nonce'] = true;
|
||||||
|
$_POST['pair_index'] = '0';
|
||||||
|
|
||||||
|
$this->controller()->handleTest();
|
||||||
|
|
||||||
|
self::assertStringContainsString( 'wpcs_connection_ok=1', $GLOBALS['wpcs_redirect_location'] );
|
||||||
|
self::assertSame( 'GET', $GLOBALS['wpcs_last_http_request']['method'] );
|
||||||
|
self::assertSame( 'https://destination.test/wp-json/wp-content-sync/v1/status', $GLOBALS['wpcs_last_http_request']['url'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_users_without_manage_options(): void {
|
||||||
|
$GLOBALS['wpcs_current_user_can']['manage_options'] = false;
|
||||||
|
|
||||||
|
$this->expectException( \RuntimeException::class );
|
||||||
|
$this->expectExceptionMessage( 'You do not have permission to test WP Content Sync connections.' );
|
||||||
|
|
||||||
|
$this->controller()->handleTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_invalid_nonces(): void {
|
||||||
|
$GLOBALS['wpcs_nonce_valid']['wpcs_test_connection']['wpcs_connection_nonce'] = false;
|
||||||
|
|
||||||
|
$this->expectException( \RuntimeException::class );
|
||||||
|
$this->expectExceptionMessage( 'The connection test request could not be verified.' );
|
||||||
|
|
||||||
|
$this->controller()->handleTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_redirects_failures_without_leaking_application_passwords(): void {
|
||||||
|
$this->storePair();
|
||||||
|
$GLOBALS['wpcs_current_user_can']['manage_options'] = true;
|
||||||
|
$GLOBALS['wpcs_nonce_valid']['wpcs_test_connection']['wpcs_connection_nonce'] = true;
|
||||||
|
$GLOBALS['wpcs_http_response'] = array(
|
||||||
|
'response' => array( 'code' => 401 ),
|
||||||
|
'body' => '{"message":"Unauthorized"}',
|
||||||
|
);
|
||||||
|
$_POST['pair_index'] = '0';
|
||||||
|
|
||||||
|
$this->controller()->handleTest();
|
||||||
|
|
||||||
|
self::assertStringContainsString( 'wpcs_connection_error=', $GLOBALS['wpcs_redirect_location'] );
|
||||||
|
self::assertStringNotContainsString( 'app-pass', $GLOBALS['wpcs_redirect_location'] );
|
||||||
|
self::assertStringNotContainsString( 'app-pass', wp_json_encode( $GLOBALS['wpcs_test_options'][ OptionLogger::OPTION_NAME ] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function controller(): ConnectionTestController {
|
||||||
|
return new ConnectionTestController(
|
||||||
|
new SettingsRepository(),
|
||||||
|
new RestTransportClient(),
|
||||||
|
new OptionLogger()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storePair(): void {
|
||||||
|
update_option(
|
||||||
|
SettingsRepository::OPTION_NAME,
|
||||||
|
array(
|
||||||
|
'sync_pairs' => array(
|
||||||
|
array(
|
||||||
|
'name' => 'Staging',
|
||||||
|
'source_url' => 'https://source.test',
|
||||||
|
'destination_url' => 'https://destination.test',
|
||||||
|
'username' => 'codex',
|
||||||
|
'application_password' => 'app-pass',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,27 @@ class DashboardTemplateTest extends TestCase {
|
|||||||
self::assertStringContainsString( 'name="wpcs_settings[sync_pairs][0][url_mappings][0][source]"', $output );
|
self::assertStringContainsString( 'name="wpcs_settings[sync_pairs][0][url_mappings][0][source]"', $output );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_it_renders_connection_diagnostics_for_each_pair(): void {
|
||||||
|
$settings = Settings::fromArray(
|
||||||
|
array(
|
||||||
|
'sync_pairs' => array(
|
||||||
|
array(
|
||||||
|
'name' => 'Staging',
|
||||||
|
'destination_url' => 'https://staging.example.test',
|
||||||
|
'username' => 'codex',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$output = $this->renderDashboard( $settings );
|
||||||
|
|
||||||
|
self::assertStringContainsString( 'name="action" value="wpcs_test_connection"', $output );
|
||||||
|
self::assertStringContainsString( 'name="wpcs_connection_nonce"', $output );
|
||||||
|
self::assertStringContainsString( 'name="pair_index" value="0"', $output );
|
||||||
|
self::assertStringContainsString( 'Test REST Connection', $output );
|
||||||
|
}
|
||||||
|
|
||||||
private function renderDashboard( ?Settings $settings = null ): string {
|
private function renderDashboard( ?Settings $settings = null ): string {
|
||||||
$settings = $settings ?? Settings::fromArray( array() );
|
$settings = $settings ?? Settings::fromArray( array() );
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace WPContentSync\Tests\Unit;
|
|||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use WPContentSync\Admin\FileImportController;
|
use WPContentSync\Admin\FileImportController;
|
||||||
|
use WPContentSync\Admin\ConnectionTestController;
|
||||||
use WPContentSync\Admin\SettingsController;
|
use WPContentSync\Admin\SettingsController;
|
||||||
use WPContentSync\Content\ContentHandlerRegistry;
|
use WPContentSync\Content\ContentHandlerRegistry;
|
||||||
use WPContentSync\Content\ContentRecordNormalizer;
|
use WPContentSync\Content\ContentRecordNormalizer;
|
||||||
@@ -61,6 +62,15 @@ class PluginTest extends TestCase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_it_registers_connection_test_controller(): void {
|
||||||
|
$container = $this->getPluginContainer( Plugin::create() );
|
||||||
|
|
||||||
|
self::assertInstanceOf(
|
||||||
|
ConnectionTestController::class,
|
||||||
|
$container->get( ConnectionTestController::class )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_it_registers_rest_transport_services(): void {
|
public function test_it_registers_rest_transport_services(): void {
|
||||||
$container = $this->getPluginContainer( Plugin::create() );
|
$container = $this->getPluginContainer( Plugin::create() );
|
||||||
|
|
||||||
@@ -104,6 +114,15 @@ class PluginTest extends TestCase {
|
|||||||
self::assertArrayHasKey( 'admin_post_wpcs_save_settings', $GLOBALS['wpcs_test_actions'] );
|
self::assertArrayHasKey( 'admin_post_wpcs_save_settings', $GLOBALS['wpcs_test_actions'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_it_hooks_connection_test_controller_on_register(): void {
|
||||||
|
unset( $GLOBALS['wpcs_test_actions'] );
|
||||||
|
|
||||||
|
$plugin = Plugin::create();
|
||||||
|
$plugin->register();
|
||||||
|
|
||||||
|
self::assertArrayHasKey( 'admin_post_wpcs_test_connection', $GLOBALS['wpcs_test_actions'] );
|
||||||
|
}
|
||||||
|
|
||||||
private function getPluginContainer( Plugin $plugin ): Container {
|
private function getPluginContainer( Plugin $plugin ): Container {
|
||||||
$reflection = new \ReflectionClass( $plugin );
|
$reflection = new \ReflectionClass( $plugin );
|
||||||
$property = $reflection->getProperty( 'container' );
|
$property = $reflection->getProperty( 'container' );
|
||||||
|
|||||||
Reference in New Issue
Block a user