From b28054f6b329af54100ceccaa71cada1fc667fc3 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 24 Sep 2025 18:33:51 +0100 Subject: [PATCH 01/18] Refactoring Admin Tabs to make to reduce code duplication. --- .../Admin/Settings/ConfigurationHelper.php | 22 ++------- .../Fields/SettingsFieldCollection.php | 2 +- .../Fields/Tab/BasicConfigurationTab.php | 46 +++++++++---------- .../Settings/Fields/Tab/DataManagementTab.php | 46 +++++++++---------- .../Fields/Tab/SettingsTabInterface.php | 18 ++++---- .../Admin/Settings/SettingsFormManager.php | 2 +- .../src/Admin/Settings/Templates/admin.php | 6 ++- .../src/Admin/SettingsPage.php | 20 ++++++-- 8 files changed, 81 insertions(+), 81 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php b/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php index 7cea4f49..0dc6850d 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php @@ -4,6 +4,9 @@ namespace WPGraphQL\Logging\Admin\Settings; +use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; +use WPGraphQL\Logging\Admin\Settings\Fields\Tab\DataManagementTab; + /** * Configuration Helper class * @@ -11,21 +14,6 @@ * It implements a singleton pattern to ensure configuration is only loaded once per request * and provides convenient methods for accessing different configuration sections. * - * Usage Examples: - * ```php - * // Get the helper instance - * $config = ConfigurationHelper::get_instance(); - * - * // Get a specific setting - * $log_level = $config->get_setting('basic_configuration', 'log_level', 'info'); - * - * // Check if a feature is enabled - * $is_enabled = $config->is_enabled('data_management', 'data_sanitization_enabled'); - * - * // Get an entire configuration section - * $basic_config = $config->get_basic_config(); - * ``` - * * @package WPGraphQL\Logging * * @since 0.0.1 @@ -114,7 +102,7 @@ public function get_setting( string $section, string $setting_key, $default_valu * @return array */ public function get_basic_config(): array { - return $this->get_section_config( 'basic_configuration' ); + return $this->get_section_config( BasicConfigurationTab::get_name() ); } /** @@ -123,7 +111,7 @@ public function get_basic_config(): array { * @return array */ public function get_data_management_config(): array { - return $this->get_section_config( 'data_management' ); + return $this->get_section_config( DataManagementTab::get_name() ); } /** diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldCollection.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldCollection.php index eab07113..1a8bb5f1 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldCollection.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/SettingsFieldCollection.php @@ -82,7 +82,7 @@ public function remove_field( string $key ): void { * @param \WPGraphQL\Logging\Admin\Settings\Fields\Tab\SettingsTabInterface $tab The tab to add. */ public function add_tab( SettingsTabInterface $tab ): void { - $this->tabs[ $tab->get_name() ] = $tab; + $this->tabs[ $tab::get_name() ] = $tab; foreach ( $tab->get_fields() as $field_key => $field ) { $this->add_field( $field_key, $field ); diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/BasicConfigurationTab.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/BasicConfigurationTab.php index 7d9c66b5..849e74cc 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/BasicConfigurationTab.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/BasicConfigurationTab.php @@ -66,22 +66,6 @@ class BasicConfigurationTab implements SettingsTabInterface { */ public const LOG_RESPONSE = 'log_response'; - /** - * Get the name/identifier of the tab. - */ - public function get_name(): string { - return 'basic_configuration'; - } - - /** - * Get the label of the tab. - * - * @return string The tab label. - */ - public function get_label(): string { - return 'Basic Configuration'; - } - /** * Get the fields for this tab. * @@ -92,7 +76,7 @@ public function get_fields(): array { $fields[ self::ENABLED ] = new CheckboxField( self::ENABLED, - $this->get_name(), + self::get_name(), __( 'Enabled', 'wpgraphql-logging' ), '', __( 'Enable or disable WPGraphQL logging.', 'wpgraphql-logging' ), @@ -100,7 +84,7 @@ public function get_fields(): array { $fields[ self::IP_RESTRICTIONS ] = new TextInputField( self::IP_RESTRICTIONS, - $this->get_name(), + self::get_name(), __( 'IP Restrictions', 'wpgraphql-logging' ), '', __( 'Comma-separated list of IPv4/IPv6 addresses to restrict logging to. Leave empty to log from all IPs.', 'wpgraphql-logging' ), @@ -109,7 +93,7 @@ public function get_fields(): array { $fields[ self::EXCLUDE_QUERY ] = new TextInputField( self::EXCLUDE_QUERY, - $this->get_name(), + self::get_name(), __( 'Exclude Queries', 'wpgraphql-logging' ), '', __( 'Comma-separated list of GraphQL query names to exclude from logging.', 'wpgraphql-logging' ), @@ -118,7 +102,7 @@ public function get_fields(): array { $fields[ self::ADMIN_USER_LOGGING ] = new CheckboxField( self::ADMIN_USER_LOGGING, - $this->get_name(), + self::get_name(), __( 'Admin User Logging', 'wpgraphql-logging' ), '', __( 'Log only for admin users.', 'wpgraphql-logging' ) @@ -126,7 +110,7 @@ public function get_fields(): array { $fields[ self::DATA_SAMPLING ] = new SelectField( self::DATA_SAMPLING, - $this->get_name(), + self::get_name(), __( 'Data Sampling Rate', 'wpgraphql-logging' ), [ '10' => __( '10% (Every 10th request)', 'wpgraphql-logging' ), @@ -142,7 +126,7 @@ public function get_fields(): array { $fields[ self::EVENT_LOG_SELECTION ] = new SelectField( self::EVENT_LOG_SELECTION, - $this->get_name(), + self::get_name(), __( 'Log Points', 'wpgraphql-logging' ), [ Events::PRE_REQUEST => __( 'Pre Request', 'wpgraphql-logging' ), @@ -159,7 +143,7 @@ public function get_fields(): array { $fields[ self::LOG_RESPONSE ] = new CheckboxField( self::LOG_RESPONSE, - $this->get_name(), + self::get_name(), __( 'Log Response', 'wpgraphql-logging' ), '', __( 'Whether or not to log the response from the WPGraphQL query into the context object.', 'wpgraphql-logging' ), @@ -167,4 +151,20 @@ public function get_fields(): array { return apply_filters( 'wpgraphql_logging_basic_configuration_fields', $fields ); } + + /** + * Get the name/identifier of the tab. + */ + public static function get_name(): string { + return 'basic_configuration'; + } + + /** + * Get the label of the tab. + * + * @return string The tab label. + */ + public static function get_label(): string { + return 'Basic Configuration'; + } } diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/DataManagementTab.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/DataManagementTab.php index 94cd5625..6f0d9a1d 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/DataManagementTab.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/DataManagementTab.php @@ -67,22 +67,6 @@ class DataManagementTab implements SettingsTabInterface { */ public const DATA_SANITIZATION_CUSTOM_FIELD_TRUNCATE = 'data_sanitization_custom_field_truncate'; - /** - * Get the name/identifier of the tab. - */ - public function get_name(): string { - return 'data_management'; - } - - /** - * Get the label of the tab. - * - * @return string The tab label. - */ - public function get_label(): string { - return 'Data Management'; - } - /** * Get the fields for this tab. * @@ -93,7 +77,7 @@ public function get_fields(): array { $fields[ self::DATA_DELETION_ENABLED ] = new CheckboxField( self::DATA_DELETION_ENABLED, - $this->get_name(), + self::get_name(), __( 'Data Deletion Enabled', 'wpgraphql-logging' ), '', __( 'Enable or disable data deletion for WPGraphQL logging.', 'wpgraphql-logging' ), @@ -101,7 +85,7 @@ public function get_fields(): array { $fields[ self::DATA_RETENTION_DAYS ] = new TextIntegerField( self::DATA_RETENTION_DAYS, - $this->get_name(), + self::get_name(), __( 'Number of Days to Retain Logs', 'wpgraphql-logging' ), '', __( 'Number of days to retain log data before deletion.', 'wpgraphql-logging' ), @@ -111,7 +95,7 @@ public function get_fields(): array { $fields[ self::DATA_SANITIZATION_ENABLED ] = new CheckboxField( self::DATA_SANITIZATION_ENABLED, - $this->get_name(), + self::get_name(), __( 'Data Sanitization Enabled', 'wpgraphql-logging' ), '', __( 'Enable or disable data sanitization for WPGraphQL logging.', 'wpgraphql-logging' ), @@ -120,7 +104,7 @@ public function get_fields(): array { $fields[ self::DATA_SANITIZATION_METHOD ] = new SelectField( self::DATA_SANITIZATION_METHOD, - $this->get_name(), + self::get_name(), __( 'Data Sanitization Method', 'wpgraphql-logging' ), [ 'recommended' => __( 'Recommended', 'wpgraphql-logging' ), @@ -134,7 +118,7 @@ public function get_fields(): array { $fields[ self::DATA_SANITIZATION_CUSTOM_FIELD_ANONYMIZE ] = new TextInputField( self::DATA_SANITIZATION_CUSTOM_FIELD_ANONYMIZE, - $this->get_name(), + self::get_name(), __( 'Custom Fields to Anonymize', 'wpgraphql-logging' ), 'wpgraphql-logging-custom', __( 'Comma-separated list of custom fields to anonymize.', 'wpgraphql-logging' ), @@ -143,7 +127,7 @@ public function get_fields(): array { $fields[ self::DATA_SANITIZATION_CUSTOM_FIELD_REMOVE ] = new TextInputField( self::DATA_SANITIZATION_CUSTOM_FIELD_REMOVE, - $this->get_name(), + self::get_name(), __( 'Custom Fields to Remove', 'wpgraphql-logging' ), 'wpgraphql-logging-custom', __( 'Comma-separated list of custom fields to remove.', 'wpgraphql-logging' ), @@ -151,7 +135,7 @@ public function get_fields(): array { $fields[ self::DATA_SANITIZATION_CUSTOM_FIELD_TRUNCATE ] = new TextInputField( self::DATA_SANITIZATION_CUSTOM_FIELD_TRUNCATE, - $this->get_name(), + self::get_name(), __( 'Custom Fields to Truncate', 'wpgraphql-logging' ), 'wpgraphql-logging-custom', __( 'Comma-separated list of custom fields to truncate.', 'wpgraphql-logging' ), @@ -159,4 +143,20 @@ public function get_fields(): array { return apply_filters( 'wpgraphql_logging_data_management_fields', $fields ); } + + /** + * Get the name/identifier of the tab. + */ + public static function get_name(): string { + return 'data_management'; + } + + /** + * Get the label of the tab. + * + * @return string The tab label. + */ + public static function get_label(): string { + return 'Data Management'; + } } diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php index 08d417d5..438a3dfc 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/SettingsTabInterface.php @@ -16,23 +16,23 @@ */ interface SettingsTabInterface { /** - * Get the name of the tab. + * Get the fields for this tab. * - * @return string The tab name/identifier. + * @return array Array of fields keyed by field ID. */ - public function get_name(): string; + public function get_fields(): array; /** - * Get the label of the tab. + * Get the name of the tab. * - * @return string The tab label. + * @return string The tab name/identifier. */ - public function get_label(): string; + public static function get_name(): string; /** - * Get the fields for this tab. + * Get the label of the tab. * - * @return array Array of fields keyed by field ID. + * @return string The tab label. */ - public function get_fields(): array; + public static function get_label(): string; } diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/SettingsFormManager.php b/plugins/wpgraphql-logging/src/Admin/Settings/SettingsFormManager.php index 07c736e9..c1f323c2 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/SettingsFormManager.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/SettingsFormManager.php @@ -48,7 +48,7 @@ public function render_form(): void { ); foreach ( $this->field_collection->get_tabs() as $tab ) { - $this->render_tab_section( $tab->get_name(), $tab->get_label() ); + $this->render_tab_section( $tab::get_name(), $tab::get_label() ); } } diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php index 1a743da0..6170c2fe 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php @@ -9,6 +9,8 @@ declare(strict_types=1); +use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; +use WPGraphQL\Logging\Admin\Settings\Fields\Tab\DataManagementTab; use WPGraphQL\Logging\Admin\Settings\LoggingSettingsService; $wpgraphql_logging_tabs_config = (array) get_query_var( 'wpgraphql_logging_main_page_config' ); @@ -60,7 +62,7 @@

    @@ -75,7 +77,7 @@

      diff --git a/plugins/wpgraphql-logging/src/Admin/SettingsPage.php b/plugins/wpgraphql-logging/src/Admin/SettingsPage.php index 6a1244dd..fccf6ae8 100644 --- a/plugins/wpgraphql-logging/src/Admin/SettingsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/SettingsPage.php @@ -5,6 +5,7 @@ namespace WPGraphQL\Logging\Admin; use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldCollection; +use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\SettingsTabInterface; use WPGraphQL\Logging\Admin\Settings\Menu\MenuPage; use WPGraphQL\Logging\Admin\Settings\SettingsFormManager; @@ -94,7 +95,7 @@ public function register_settings_page(): void { continue; } - $tab_labels[ $tab_key ] = $tab->get_label(); + $tab_labels[ $tab_key ] = $tab::get_label(); } $page = new MenuPage( @@ -134,25 +135,34 @@ public function register_settings_fields(): void { public function get_current_tab( array $tabs = [] ): string { $tabs = $this->get_tabs( $tabs ); if ( empty( $tabs ) ) { - return 'basic_configuration'; + return $this->get_default_tab(); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for tab navigation only, no form processing if ( ! isset( $_GET['tab'] ) || ! is_string( $_GET['tab'] ) ) { - return 'basic_configuration'; + return $this->get_default_tab(); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading GET parameter for tab navigation only, no form processing $tab = sanitize_text_field( $_GET['tab'] ); if ( ! is_string( $tab ) || '' === $tab ) { - return 'basic_configuration'; + return $this->get_default_tab(); } if ( array_key_exists( $tab, $tabs ) ) { return $tab; } - return 'basic_configuration'; + return $this->get_default_tab(); + } + + /** + * Get the default tab slug. + * + * @return string The default tab slug. + */ + public function get_default_tab(): string { + return BasicConfigurationTab::get_name(); } /** From 37815896ef8fb54782bfd1890f65c47d8c5d1dd6 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 24 Sep 2025 20:26:27 +0100 Subject: [PATCH 02/18] Refactor settings page and add new tests Refactored SettingsPage to use a getter for the field collection and improved template path handling. Removed special email and URL sanitization from TextInputField, now using only sanitize_text_field. Added new and expanded unit tests for SettingsFieldCollection, MenuPage, TextIntegerField, and improved test coverage and docblocks for existing field and settings page tests. Renamed Settings_Page_Test.php to SettingsPageTest.php for consistency. --- .../Settings/Fields/Field/TextInputField.php | 8 -- .../src/Admin/SettingsPage.php | 37 ++++- .../Fields/Field/CheckboxFieldTest.php | 7 + .../Settings/Fields/Field/SelectFieldTest.php | 8 ++ .../Fields/Field/TextInputFieldTest.php | 127 ++---------------- .../Fields/Field/TextIntegerFieldTest.php | 69 ++++++++++ .../Fields/SettingsFieldCollectionTest.php | 63 +++++++++ .../Admin/Settings/Menu/MenuPageTest.php | 94 +++++++++++++ ...ngs_Page_Test.php => SettingsPageTest.php} | 64 +++++---- 9 files changed, 319 insertions(+), 158 deletions(-) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextIntegerFieldTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/SettingsFieldCollectionTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Menu/MenuPageTest.php rename plugins/wpgraphql-logging/tests/wpunit/Admin/{Settings_Page_Test.php => SettingsPageTest.php} (66%) diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php index 4d897327..971d5ced 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Field/TextInputField.php @@ -70,14 +70,6 @@ public function render_field( array $option_value, string $setting_key, string $ * @return string The sanitized string value. */ public function sanitize_field( $value ): string { - if ( 'email' === $this->get_input_type() ) { - return sanitize_email( (string) $value ); - } - - if ( 'url' === $this->get_input_type() ) { - return esc_url_raw( (string) $value ); - } - return sanitize_text_field( (string) $value ); } diff --git a/plugins/wpgraphql-logging/src/Admin/SettingsPage.php b/plugins/wpgraphql-logging/src/Admin/SettingsPage.php index fccf6ae8..cbec4410 100644 --- a/plugins/wpgraphql-logging/src/Admin/SettingsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/SettingsPage.php @@ -79,15 +79,23 @@ public function init_field_collection(): void { $this->field_collection = new SettingsFieldCollection(); } + /** + * Get the field collection. + */ + public function get_field_collection(): ?SettingsFieldCollection { + return $this->field_collection; + } + /** * Registers the settings page. */ public function register_settings_page(): void { - if ( is_null( $this->field_collection ) ) { + $collection = $this->get_field_collection(); + if ( is_null( $collection ) ) { return; } - $tabs = $this->field_collection->get_tabs(); + $tabs = $collection->get_tabs(); $tab_labels = []; foreach ( $tabs as $tab_key => $tab ) { @@ -102,7 +110,7 @@ public function register_settings_page(): void { __( 'WPGraphQL Logging Settings', 'wpgraphql-logging' ), 'WPGraphQL Logging', self::PLUGIN_MENU_SLUG, - trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_DIR ) . 'src/Admin/Settings/Templates/admin.php', + $this->get_admin_template(), [ 'wpgraphql_logging_main_page_config' => [ 'tabs' => $tab_labels, @@ -114,14 +122,25 @@ public function register_settings_page(): void { $page->register_page(); } + /** + * Get the admin template path. + * + * @return string The path to the admin template file. + */ + public function get_admin_template() : string { + $template_path = trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_DIR ) . 'src/Admin/Settings/Templates/admin.php'; + return (string) apply_filters( 'wpgraphql_logging_admin_template_path', $template_path ); + } + /** * Registers the settings fields for each tab. */ public function register_settings_fields(): void { - if ( ! isset( $this->field_collection ) ) { + $collection = $this->get_field_collection(); + if ( ! isset( $collection ) ) { return; } - $settings_manager = new SettingsFormManager( $this->field_collection ); + $settings_manager = new SettingsFormManager( $collection ); $settings_manager->render_form(); } @@ -200,6 +219,8 @@ public function load_scripts_styles( string $hook_suffix ): void { WPGRAPHQL_LOGGING_VERSION, true ); + + do_action( 'wpgraphql_logging_admin_enqueue_scripts', $hook_suffix ); } /** @@ -213,8 +234,10 @@ protected function get_tabs(array $tabs = []): array { if ( ! empty( $tabs ) ) { return $tabs; } - if ( ! is_null( $this->field_collection ) ) { - return $this->field_collection->get_tabs(); + + $collection = $this->get_field_collection(); + if ( ! is_null( $collection ) ) { + return $collection->get_tabs(); } return []; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php index e456f5db..3be3d128 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php @@ -8,6 +8,13 @@ use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldInterface; use lucatume\WPBrowser\TestCase\WPTestCase; +/** + * Test class for CheckboxField. + * + * @package WPGraphQL\Logging + * + * @since 0.0.1 + */ class CheckboxFieldTest extends WPTestCase { protected ?CheckboxField $field = null; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php index aa07150e..29e785d9 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php @@ -8,6 +8,14 @@ use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldInterface; use lucatume\WPBrowser\TestCase\WPTestCase; + +/** + * Test class for SelectField. + * + * @package WPGraphQL\Logging + * + * @since 0.0.1 + */ class SelectFieldTest extends WPTestCase { protected ?SelectField $field = null; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php index 0b436cc1..bafbf4ea 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php @@ -6,8 +6,16 @@ use WPGraphQL\Logging\Admin\Settings\Fields\Field\TextInputField; use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldInterface; +use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; use lucatume\WPBrowser\TestCase\WPTestCase; +/** + * Test class for TextInputField. + * + * @package WPGraphQL\Logging + * + * @since 0.0.1 + */ class TextInputFieldTest extends WPTestCase { protected ?TextInputField $field = null; @@ -16,7 +24,7 @@ protected function setUp(): void { parent::setUp(); $this->field = new TextInputField( 'ip_restrictions', - 'basic_configuration', + BasicConfigurationTab::get_name(), 'IP Restrictions', 'custom-css-class', 'A comma separated list of IP addresses to restrict logging to. Leave empty to log from all IPs.', @@ -28,9 +36,9 @@ protected function setUp(): void { public function test_basic_field_properties(): void { $field = $this->field; $this->assertEquals( 'ip_restrictions', $field->get_id() ); - $this->assertTrue( $field->should_render_for_tab( 'basic_configuration' ) ); + $this->assertTrue( $field->should_render_for_tab( BasicConfigurationTab::get_name() ) ); $this->assertFalse( $field->should_render_for_tab( 'other_tab' ) ); - $this->assertTrue( $field->should_render_for_tab( 'basic_configuration' ) ); + $this->assertTrue( $field->should_render_for_tab( BasicConfigurationTab::get_name() ) ); } @@ -66,7 +74,7 @@ public function test_render_field() { $field = $this->field; $option_value = []; $setting_key = 'wpgraphql_logging_settings'; - $tab_key = 'basic_configuration'; + $tab_key = BasicConfigurationTab::get_name(); ob_start(); $field->render_field_callback( [ 'tab_key' => $tab_key, 'settings_key' => $setting_key ] ); $rendered_output = ob_get_clean(); @@ -76,115 +84,4 @@ public function test_render_field() { $this->assertStringContainsString( 'placeholder="e.g. 192.168.1.1, 10.0.0.1"', $rendered_output ); $this->assertStringContainsString( 'type="text"', $rendered_output ); } - - public function test_sanitize_field_email() { - - $field = new TextInputField( - 'email_address', - 'basic_configuration', - 'Email Address', - 'custom-css-class', - 'The email address to send logs to.', - 'example@example.com', - '' - ); - - - $this->assertEquals('test@example.com', $field->sanitize_field('test@example.com')); - } - - public function test_sanitize_field_url() { - - $field = new TextInputField( - 'url', - 'basic_configuration', - 'URL', - 'custom-css-class', - 'The URL to send logs to.', - 'https://example.com', - '' - ); - - - $this->assertEquals('https://example.com', $field->sanitize_field('https://example.com')); - } - - public function test_add_settings_field_registers_field() { - $field = $this->field; - global $wp_settings_fields; - $field->add_settings_field('section_id', 'page_id', ['foo' => 'bar']); - $this->assertArrayHasKey('page_id', $wp_settings_fields); - $this->assertArrayHasKey('section_id', $wp_settings_fields['page_id']); - } - - public function test_render_field_callback() { - $field = $this->field; - $option_value = []; - $setting_key = 'wpgraphql_logging_settings'; - $tab_key = 'basic_configuration'; - $args = [ - 'tab_key' => $tab_key, - 'settings_key' => $setting_key, - ]; - - ob_start(); - $field->render_field_callback( $args ); - $rendered_output = ob_get_contents(); - ob_end_clean(); - - $this->assertStringContainsString( 'name="wpgraphql_logging_settings[basic_configuration][ip_restrictions]"', $rendered_output ); - $this->assertStringContainsString( 'value=""', $rendered_output ); - $this->assertStringContainsString( 'class="custom-css-class"', $rendered_output ); - $this->assertStringContainsString( 'placeholder="e.g. 192.168.1.1, 10.0.0.1"', $rendered_output ); - $this->assertStringContainsString( 'type="text"', $rendered_output ); - $this->assertStringContainsString( 'A comma separated list of IP addresses to restrict logging to. Leave empty to log from all IPs.', $rendered_output ); - $this->assertStringNotContainsString('multiple="multiple"', $rendered_output); - } - - public function test_get_field_value() { - // Use a field with a non-empty default value to verify fallbacks - $field = new TextInputField( - 'test_field', - 'basic_configuration', - 'Test Field', - 'css', - 'desc', - 'ph', - 'DEFAULT' - ); - - $settings_key = 'wpgraphql_logging_settings'; - $tab_key = 'basic_configuration'; - - $extract_value = static function( string $html ): string { - $matches = []; - preg_match('/value=\"([^\"]*)\"/', $html, $matches); - return $matches[1] ?? ''; - }; - - // 1) Tab exists but field ID missing -> default - $html = $field->render_field( [ $tab_key => [] ], $settings_key, $tab_key ); - $this->assertSame( 'DEFAULT', $extract_value( $html ) ); - - // 2) Field ID present with null -> default - $html = $field->render_field( [ $tab_key => [ 'test_field' => null ] ], $settings_key, $tab_key ); - $this->assertSame( 'DEFAULT', $extract_value( $html ) ); - - // 3) Field ID present with empty string -> empty string (not default) - $html = $field->render_field( [ $tab_key => [ 'test_field' => '' ] ], $settings_key, $tab_key ); - $this->assertSame( '', $extract_value( $html ) ); - - // 4) Field ID present with a value -> that value - $html = $field->render_field( [ $tab_key => [ 'test_field' => 'custom' ] ], $settings_key, $tab_key ); - $this->assertSame( 'custom', $extract_value( $html ) ); - - // 5) Empty field ID -> default - // Create a subclass overriding get_id to simulate empty ID - $emptyIdField = new class('ignored', $tab_key, 'Title', 'css', 'desc', 'ph', 'DEF') extends TextInputField { - public function get_id(): string { return ''; } - }; - $html = $emptyIdField->render_field( [ $tab_key => [ 'ignored' => 'value' ] ], $settings_key, $tab_key ); - $this->assertSame( 'DEF', $extract_value( $html ) ); - } - } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextIntegerFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextIntegerFieldTest.php new file mode 100644 index 00000000..8491506a --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextIntegerFieldTest.php @@ -0,0 +1,69 @@ +field = new TextIntegerField( + DataManagementTab::DATA_RETENTION_DAYS, + DataManagementTab::get_name(), + __( 'Number of Days to Retain Logs', 'wpgraphql-logging' ), + '', + __( 'Number of days to retain log data before deletion.', 'wpgraphql-logging' ), + __( 'e.g., 30', 'wpgraphql-logging' ), + '30' + ); + } + + public function test_input_type(): void { + $field = $this->field; + $reflection = new ReflectionClass($field); + $method = $reflection->getMethod('get_input_type'); + $method->setAccessible(true); + $this->assertEquals( 'number', $method->invoke($field) ); + } + + public function test_sanitize_field() { + $field = $this->field; + + // Valid integer inputs + $this->assertEquals( '123', $field->sanitize_field( '123' ) ); + $this->assertEquals( '0', $field->sanitize_field( '0' ) ); + $this->assertEquals( '999999', $field->sanitize_field( '999999' ) ); + + // Negative numbers should be converted to positive + $this->assertEquals( '-123', $field->sanitize_field( '-123' ) ); + + // Decimal numbers should be converted to integers + $this->assertEquals( '123', $field->sanitize_field( '123.45' ) ); + $this->assertEquals( '123', $field->sanitize_field( '123.99' ) ); + + // Non-numeric strings should return empty or default + $this->assertEquals( '0', $field->sanitize_field( 'abc' ) ); + $this->assertEquals( '0', $field->sanitize_field( 'not a number' ) ); + + // XSS attempts + $this->assertEquals( '0', $field->sanitize_field( '' ) ); + $this->assertEquals( '0', $field->sanitize_field( '' ) ); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/SettingsFieldCollectionTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/SettingsFieldCollectionTest.php new file mode 100644 index 00000000..c242530e --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/SettingsFieldCollectionTest.php @@ -0,0 +1,63 @@ +collection = new SettingsFieldCollection(); + } + + public function test_constructor_initializes_tabs(): void { + $tabs = $this->collection->get_tabs(); + + $this->assertNotEmpty($tabs); + $this->assertArrayHasKey(BasicConfigurationTab::get_name(), $tabs); + $this->assertArrayHasKey(DataManagementTab::get_name(), $tabs); + } + + + + public function test_add_field(): void { + $field = $this->createMock(SettingsFieldInterface::class); + $key = 'test_field'; + + $this->collection->add_field($key, $field); + + $this->assertEquals($field, $this->collection->get_field($key)); + $this->assertInstanceOf(SettingsFieldInterface::class, $this->collection->get_field($key)); + + // Remove field + $this->collection->remove_field($key); + $this->assertNull($this->collection->get_field($key)); + } + + public function test_get_tab(): void { + $tab = $this->collection->get_tab(BasicConfigurationTab::get_name()); + + $this->assertInstanceOf(SettingsTabInterface::class, $tab); + $this->assertEquals(BasicConfigurationTab::get_name(), $tab->get_name()); + + $this->assertNull($this->collection->get_tab('nonexistent_tab')); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Menu/MenuPageTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Menu/MenuPageTest.php new file mode 100644 index 00000000..72d9b9b6 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Menu/MenuPageTest.php @@ -0,0 +1,94 @@ + ['key' => 'value']]; + + $menu_page = new MenuPage($page_title, $menu_title, $menu_slug, $template, $args); + + $this->assertInstanceOf(MenuPage::class, $menu_page); + } + + public function test_registration_callback_with_missing_template(): void { + $menu_page = new MenuPage( + 'Test Page', + 'Test Menu', + 'test-slug', + '/non/existent/template.php' + ); + + ob_start(); + $menu_page->registration_callback(); + $output = ob_get_clean(); + + $this->assertStringContainsString('notice notice-error', $output); + $this->assertStringContainsString('The WPGraphQL Logging Settings template does not exist.', $output); + } + + public function test_registration_callback_with_empty_template(): void { + $menu_page = new MenuPage( + 'Test Page', + 'Test Menu', + 'test-slug', + '' + ); + + ob_start(); + $menu_page->registration_callback(); + $output = ob_get_clean(); + + $this->assertStringContainsString('notice notice-error', $output); + } + + public function test_registration_callback_sets_query_vars(): void { + // Create a temporary template file + $template_path = wp_tempnam('test-template'); + file_put_contents($template_path, ''); + + $args = [ + 'test_var1' => ['key1' => 'value1'], + 'test_var2' => ['key2' => 'value2'] + ]; + + $menu_page = new MenuPage( + 'Test Page', + 'Test Menu', + 'test-slug', + $template_path, + $args + ); + + ob_start(); + $menu_page->registration_callback(); + $output = ob_get_clean(); + + // Check that query vars were set + $this->assertEquals(['key1' => 'value1'], get_query_var('test_var1')); + $this->assertEquals(['key2' => 'value2'], get_query_var('test_var2')); + $this->assertStringContainsString('Template loaded', $output); + + // Clean up + unlink($template_path); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings_Page_Test.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/SettingsPageTest.php similarity index 66% rename from plugins/wpgraphql-logging/tests/wpunit/Admin/Settings_Page_Test.php rename to plugins/wpgraphql-logging/tests/wpunit/Admin/SettingsPageTest.php index 2682123b..d8d4bb34 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings_Page_Test.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/SettingsPageTest.php @@ -7,8 +7,17 @@ use lucatume\WPBrowser\TestCase\WPTestCase; use WPGraphQL\Logging\Admin\SettingsPage; use ReflectionClass; +use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldCollection; -class Settings_Page_Test extends WPTestCase { + +/** + * Test class for SettingsPage. + * + * @package WPGraphQL\Logging + * + * @since 0.0.1 + */ +class SettingsPageTest extends WPTestCase { public function setUp(): void { parent::setUp(); @@ -33,19 +42,39 @@ public function test_settings_page_instance() { $this->assertNull( $instanceProperty->getValue() ); $instance = SettingsPage::init(); + $instance->setup(); $this->assertInstanceOf( SettingsPage::class, $instanceProperty->getValue() ); $this->assertSame( $instance, $instanceProperty->getValue(), 'SettingsPage::init() should set the static instance property' ); } - public function test_setup_registers_hooks(): void { - $page = new SettingsPage(); + public function test_setup_registers_hooks(): void { + $page = new SettingsPage(); $page->setup(); $this->assertEquals(10, has_action('init', [$page, 'init_field_collection'])); $this->assertEquals(10, has_action('admin_menu', [$page, 'register_settings_page'])); $this->assertEquals(10, has_action('admin_init', [$page, 'register_settings_fields'])); $this->assertEquals(10, has_action('admin_enqueue_scripts', [$page, 'load_scripts_styles'])); + + // Init Field Collection + $page->init_field_collection(); + $page->register_settings_fields(); + $page->register_settings_page(); + $page->load_scripts_styles('settings_page_' . SettingsPage::PLUGIN_MENU_SLUG); + + $this->assertNotNull($page->get_field_collection(), 'Field collection should be initialized in setup'); + $this->assertInstanceOf( SettingsFieldCollection::class, $page->get_field_collection(), 'Field collection should be initialized in setup' ); + } + + public function test_init_field_collection_initializes_field_collection(): void { + + $page = SettingsPage::init(); + $page->setup(); + $page->init_field_collection(); + + $this->assertNotNull($page->get_field_collection(), 'Field collection should be initialized in setup'); + $this->assertInstanceOf( SettingsFieldCollection::class, $page->get_field_collection(), 'Field collection should be initialized in setup' ); } public function test_register_settings_page_no_field_collection_does_nothing(): void { @@ -68,13 +97,13 @@ public function test_get_current_tab_behaviour(): void { // Provide custom tabs and no $_GET -> default $tabs = [ 'basic_configuration' => new class implements \WPGraphQL\Logging\Admin\Settings\Fields\Tab\SettingsTabInterface { - public function get_name(): string { return 'basic_configuration'; } - public function get_label(): string { return 'Basic Configuration'; } + public static function get_name(): string { return 'basic_configuration'; } + public static function get_label(): string { return 'Basic Configuration'; } public function get_fields(): array { return []; } }, 'advanced' => new class implements \WPGraphQL\Logging\Admin\Settings\Fields\Tab\SettingsTabInterface { - public function get_name(): string { return 'advanced'; } - public function get_label(): string { return 'Advanced'; } + public static function get_name(): string { return 'advanced'; } + public static function get_label(): string { return 'Advanced'; } public function get_fields(): array { return []; } }, ]; @@ -89,25 +118,4 @@ public function get_fields(): array { return []; } $_GET['tab'] = 'advanced'; $this->assertSame('advanced', $page->get_current_tab($tabs)); } - - public function test_load_scripts_styles_enqueues_assets_conditionally(): void { - $page = new SettingsPage(); - - // Wrong page hook -> nothing enqueued - $page->load_scripts_styles('some_other_page'); - $this->assertFalse(wp_style_is('wpgraphql-logging-settings-css', 'enqueued')); - $this->assertFalse(wp_script_is('wpgraphql-logging-settings-js', 'enqueued')); - - // Correct page hook -> stylesheet should enqueue if file exists; script only if file exists - $page->load_scripts_styles('settings_page_' . SettingsPage::PLUGIN_MENU_SLUG); - - // CSS is present in this repository, so this should be enqueued - $this->assertTrue(wp_style_is('wpgraphql-logging-settings-css', 'enqueued')); - - // JS may not exist; expectation: not enqueued if file missing - $expectedJs = file_exists( trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_DIR ) . 'assets/js/settings/wp-graphql-logging-settings.js' ); - $this->assertSame($expectedJs, wp_script_is('wpgraphql-logging-settings-js', 'enqueued')); - } - - } From bbb6750d0f6056cc7ccd19d5c78bc9464b06efc9 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 26 Sep 2025 15:46:41 +0100 Subject: [PATCH 03/18] Removed LoggerSettingService as not needed. Updated tests for Admin Settings. --- .../Admin/Settings/ConfigurationHelper.php | 35 ++-- .../Admin/Settings/LoggingSettingsService.php | 76 -------- .../Admin/Settings/SettingsFormManager.php | 8 +- .../src/Admin/Settings/Templates/admin.php | 11 +- .../src/Admin/SettingsPage.php | 6 +- .../Settings/ConfigurationHelperTest.php | 120 +++++++++++++ .../Fields/Field/CheckboxFieldTest.php | 12 +- .../Settings/SettingsFormManagerTest.php | 168 ++++++++++++++++++ .../wpgraphql-logging/wpgraphql-logging.php | 2 +- 9 files changed, 324 insertions(+), 114 deletions(-) delete mode 100644 plugins/wpgraphql-logging/src/Admin/Settings/LoggingSettingsService.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/ConfigurationHelperTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/SettingsFormManagerTest.php diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php b/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php index 0dc6850d..75a76e3d 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/ConfigurationHelper.php @@ -75,13 +75,13 @@ public function get_config(): array { * Get configuration for a specific section (tab). * * @param string $section The configuration section key. - * @param array $default_value Default value if section not found. + * @param array $default_value_value Default value if section not found. * * @return array */ - public function get_section_config( string $section, array $default_value = [] ): array { + public function get_section_config( string $section, array $default_value_value = [] ): array { $config = $this->get_config(); - return $config[ $section ] ?? $default_value; + return $config[ $section ] ?? $default_value_value; } /** @@ -89,11 +89,11 @@ public function get_section_config( string $section, array $default_value = [] ) * * @param string $section The configuration section key. * @param string $setting_key The setting key within the section. - * @param mixed $default_value Default value if setting not found. + * @param mixed $default_value_value Default value if setting not found. */ - public function get_setting( string $section, string $setting_key, $default_value = null ): mixed { + public function get_setting( string $section, string $setting_key, $default_value_value = null ): mixed { $section_config = $this->get_section_config( $section ); - return $section_config[ $setting_key ] ?? $default_value; + return $section_config[ $setting_key ] ?? $default_value_value; } /** @@ -135,15 +135,6 @@ public function clear_cache(): void { wp_cache_delete( $option_key, $this->get_settings_group() ); } - /** - * Reload the configuration from the database. - * This bypasses any cache and forces a fresh load. - */ - public function reload_config(): void { - $this->clear_cache(); - $this->load_config(); - } - /** * Get the option key for the settings. */ @@ -158,6 +149,18 @@ public function get_settings_group(): string { return (string) apply_filters( 'wpgraphql_logging_settings_group_settings_group', WPGRAPHQL_LOGGING_SETTINGS_GROUP ); } + /** + * Get the raw option value from the database. + * + * @param string $option_key The option key to retrieve. + * @param mixed $default_value Default value if option not found. + * + * @return array The option value as an array. + */ + public function get_option_value( string $option_key, mixed $default_value = null ): array { + return (array) get_option( $option_key, $default_value ); + } + /** * Hook into WordPress to clear cache when settings are updated. * This should be called during plugin initialization. @@ -201,7 +204,7 @@ protected function load_config(): void { } // Load from database. - $this->config = (array) get_option( $option_key, [] ); + $this->config = $this->get_option_value( $option_key, [] ); // Cache the result in both cache groups. wp_cache_set( $option_key, $this->config, self::CACHE_GROUP, $cache_duration ); diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/LoggingSettingsService.php b/plugins/wpgraphql-logging/src/Admin/Settings/LoggingSettingsService.php deleted file mode 100644 index e84caa79..00000000 --- a/plugins/wpgraphql-logging/src/Admin/Settings/LoggingSettingsService.php +++ /dev/null @@ -1,76 +0,0 @@ -config_helper = ConfigurationHelper::get_instance(); - } - - /** - * Get the settings values. - * - * @return array - */ - public function get_settings_values(): array { - return $this->config_helper->get_config(); - } - - /** - * Get the configuration for a specific tab. - * - * @param string $tab_key The tab key. - * - * @return array|null - */ - public function get_tab_config( string $tab_key ): ?array { - $config = $this->config_helper->get_section_config( $tab_key ); - return empty( $config ) ? null : $config; - } - - /** - * Get a specific setting value from a tab. - * - * @param string $tab_key The tab key. - * @param string $setting_key The setting key. - * @param mixed $default_value The default value if not found. - */ - public function get_setting( string $tab_key, string $setting_key, $default_value = null ): mixed { - return $this->config_helper->get_setting( $tab_key, $setting_key, $default_value ); - } - - /** - * The option key for the settings group. - */ - public static function get_option_key(): string { - return ConfigurationHelper::get_instance()->get_option_key(); - } - - /** - * The settings group for the options. - */ - public static function get_settings_group(): string { - return ConfigurationHelper::get_instance()->get_settings_group(); - } -} diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/SettingsFormManager.php b/plugins/wpgraphql-logging/src/Admin/Settings/SettingsFormManager.php index c1f323c2..90efda4a 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/SettingsFormManager.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/SettingsFormManager.php @@ -4,6 +4,7 @@ namespace WPGraphQL\Logging\Admin\Settings; +use WPGraphQL\Logging\Admin\Settings\ConfigurationHelper; use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldCollection; /** @@ -18,8 +19,9 @@ class SettingsFormManager { /** * @param \WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldCollection $field_collection Collection of fields to be registered in the settings sections. + * @param \WPGraphQL\Logging\Admin\Settings\ConfigurationHelper $configuration_helper The configuration helper instance to access settings. */ - public function __construct(readonly SettingsFieldCollection $field_collection ) { + public function __construct(readonly SettingsFieldCollection $field_collection, readonly ConfigurationHelper $configuration_helper) { /** * Fire off init action. * @@ -110,14 +112,14 @@ public function sanitize_settings( ?array $new_input ): array { * Get the option key for the settings group. */ public function get_option_key(): string { - return LoggingSettingsService::get_option_key(); + return $this->configuration_helper->get_option_key(); } /** * Get the settings group for the options. */ public function get_settings_group(): string { - return LoggingSettingsService::get_settings_group(); + return $this->configuration_helper->get_settings_group(); } /** diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php index 6170c2fe..26428650 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php @@ -9,13 +9,14 @@ declare(strict_types=1); +use WPGraphQL\Logging\Admin\Settings\ConfigurationHelper; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\DataManagementTab; -use WPGraphQL\Logging\Admin\Settings\LoggingSettingsService; -$wpgraphql_logging_tabs_config = (array) get_query_var( 'wpgraphql_logging_main_page_config' ); -$wpgraphql_logging_current_tab = (string) ( $wpgraphql_logging_tabs_config['current_tab'] ?? '' ); -$wpgraphql_logging_tabs = (array) ( $wpgraphql_logging_tabs_config['tabs'] ?? [] ); +$wpgraphql_logging_configuration_helper = ConfigurationHelper::get_instance(); +$wpgraphql_logging_tabs_config = (array) get_query_var( 'wpgraphql_logging_main_page_config' ); +$wpgraphql_logging_current_tab = (string) ( $wpgraphql_logging_tabs_config['current_tab'] ?? '' ); +$wpgraphql_logging_tabs = (array) ( $wpgraphql_logging_tabs_config['tabs'] ?? [] ); ?>
      @@ -39,7 +40,7 @@
      get_settings_group() ); do_settings_sections( 'wpgraphql-logging-' . $wpgraphql_logging_current_tab ); submit_button(); ?> diff --git a/plugins/wpgraphql-logging/src/Admin/SettingsPage.php b/plugins/wpgraphql-logging/src/Admin/SettingsPage.php index cbec4410..34262dff 100644 --- a/plugins/wpgraphql-logging/src/Admin/SettingsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/SettingsPage.php @@ -4,6 +4,7 @@ namespace WPGraphQL\Logging\Admin; +use WPGraphQL\Logging\Admin\Settings\ConfigurationHelper; use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldCollection; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; use WPGraphQL\Logging\Admin\Settings\Fields\Tab\SettingsTabInterface; @@ -127,7 +128,7 @@ public function register_settings_page(): void { * * @return string The path to the admin template file. */ - public function get_admin_template() : string { + public function get_admin_template(): string { $template_path = trailingslashit( WPGRAPHQL_LOGGING_PLUGIN_DIR ) . 'src/Admin/Settings/Templates/admin.php'; return (string) apply_filters( 'wpgraphql_logging_admin_template_path', $template_path ); } @@ -140,7 +141,8 @@ public function register_settings_fields(): void { if ( ! isset( $collection ) ) { return; } - $settings_manager = new SettingsFormManager( $collection ); + $configuration_helper = ConfigurationHelper::get_instance(); + $settings_manager = new SettingsFormManager( $collection, $configuration_helper ); $settings_manager->render_form(); } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/ConfigurationHelperTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/ConfigurationHelperTest.php new file mode 100644 index 00000000..041a3d96 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/ConfigurationHelperTest.php @@ -0,0 +1,120 @@ + [ + 'enabled' => true, + 'log_level' => 'info', + 'max_log_entries' => 1000, + 'enable_debug_mode' => false + ], + 'data_management' => [ + 'enabled' => false, + 'auto_cleanup' => true, + 'cleanup_interval' => 'weekly', + 'retention_period' => 30, + 'export_format' => 'json' + ] + ]; + + protected $default = []; + + + public function setUp(): void { + $configuration_helper = ConfigurationHelper::get_instance(); + $option_key = $configuration_helper->get_option_key(); + $this->default = get_option($option_key, []); + update_option($option_key, $this->config); + parent::setUp(); + } + + public function tearDown(): void { + parent::tearDown(); + $configuration_helper = ConfigurationHelper::get_instance(); + $option_key = $configuration_helper->get_option_key(); + update_option($option_key, $this->default); + } + + + /** + * Test that instance is initially null and gets created. + */ + public function test_instance_initially_null_then_created(): void { + // Use reflection to access the private static property + $reflection = new \ReflectionClass(ConfigurationHelper::class); + $instanceProperty = $reflection->getProperty('instance'); + $instanceProperty->setAccessible(true); + + // Reset the instance to null + $instanceProperty->setValue(null, null); + + // Verify instance is null initially + $this->assertNull($instanceProperty->getValue()); + + // Get instance should create it + $instance = ConfigurationHelper::get_instance(); + + // Verify instance is no longer null and is correct type + $this->assertNotNull($instanceProperty->getValue()); + $this->assertInstanceOf(ConfigurationHelper::class, $instance); + $this->assertSame($instance, $instanceProperty->getValue()); + } + + /** + * Test get_config returns empty array when no config is set. + */ + public function test_get_config_returns_empty_array_when_no_config(): void { + $instance = ConfigurationHelper::get_instance(); + $config = $instance->get_config(); + + $this->assertIsArray($config); + } + + public function test_get_setting(): void { + $instance = ConfigurationHelper::get_instance(); + + $default_value = ['test_key' => 'default_value']; + $section = 'nonexistent_section'; + $setting_key = 'test_key'; + $result = $instance->get_setting($section, $setting_key, $default_value); + $this->assertEquals($default_value, $result); + } + + public function test_get_basic_config_returns_array(): void { + $instance = ConfigurationHelper::get_instance(); + $basic_config = $instance->get_basic_config(); + + $this->assertIsArray($basic_config); + $this->assertSame($this->config['basic_configuration'], $basic_config); + } + + public function test_get_data_management_returns_array(): void { + $instance = ConfigurationHelper::get_instance(); + $data_management = $instance->get_data_management_config(); + + $this->assertIsArray($data_management); + $this->assertSame($this->config['data_management'], $data_management); + } + + public function test_get_is_enabled(): void { + $instance = ConfigurationHelper::get_instance(); + $this->assertTrue($instance->is_enabled('basic_configuration', 'enabled')); + $this->assertFalse($instance->is_enabled('data_management', 'enabled')); + } + +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php index 3be3d128..aa6e891d 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php @@ -61,16 +61,6 @@ public function test_render_field(): void { $rendered_output = ob_get_contents(); ob_end_clean(); - $expected_output = << - - Enable or disable query logging for WPGraphQL requests. - -HTML; - $this->assertEquals( - preg_replace('/[\s\t\r\n]+/', '', $expected_output), - preg_replace('/[\s\t\r\n]+/', '', $rendered_output) - ); - + $this->assertStringContainsString( 'type="checkbox"', $rendered_output ); } } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/SettingsFormManagerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/SettingsFormManagerTest.php new file mode 100644 index 00000000..0a2e8a71 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/SettingsFormManagerTest.php @@ -0,0 +1,168 @@ +field_collection = $this->createMock(SettingsFieldCollection::class); + $this->configuration_helper = $this->createMock(ConfigurationHelper::class); + + $this->manager = new SettingsFormManager( + $this->field_collection, + $this->configuration_helper + ); + } + + public function test_get_option_key_returns_configuration_helper_value(): void { + $expected = 'test_option_key'; + $this->configuration_helper + ->expects($this->once()) + ->method('get_option_key') + ->willReturn($expected); + + $result = $this->manager->get_option_key(); + + $this->assertEquals($expected, $result); + } + + public function test_get_settings_group_returns_configuration_helper_value(): void { + $expected = 'test_settings_group'; + $this->configuration_helper + ->expects($this->once()) + ->method('get_settings_group') + ->willReturn($expected); + + $result = $this->manager->get_settings_group(); + + $this->assertEquals($expected, $result); + } + + public function test_sanitize_settings_returns_empty_array_when_input_is_null(): void { + $result = $this->manager->sanitize_settings(null); + + $this->assertEquals([], $result); + } + + public function test_sanitize_settings_returns_old_input_when_no_tabs(): void { + $old_option = ['existing' => 'data']; + update_option('test_option', $old_option); + + $this->configuration_helper + ->method('get_option_key') + ->willReturn('test_option'); + + $this->field_collection + ->method('get_tabs') + ->willReturn([]); + + $result = $this->manager->sanitize_settings(['new' => 'data']); + + $this->assertEquals($old_option, $result); + } + + public function test_sanitize_settings_sanitizes_known_fields(): void { + $old_option = []; + update_option('test_option', $old_option); + + $this->configuration_helper + ->method('get_option_key') + ->willReturn('test_option'); + + $tab = $this->createMock(SettingsTabInterface::class); + $this->field_collection + ->method('get_tabs') + ->willReturn(['test_tab' => $tab]); + + $field = $this->createMock(SettingsFieldInterface::class); + $field->expects($this->once()) + ->method('sanitize_field') + ->with('raw_value') + ->willReturn('sanitized_value'); + + $this->field_collection + ->method('get_field') + ->with('test_field') + ->willReturn($field); + + $new_input = ['test_tab' => ['test_field' => 'raw_value']]; + $result = $this->manager->sanitize_settings($new_input); + + $expected = ['test_tab' => ['test_field' => 'sanitized_value']]; + $this->assertEquals($expected, $result); + } + + public function test_sanitize_settings_skips_unknown_fields(): void { + $old_option = []; + update_option('test_option', $old_option); + + $this->configuration_helper + ->method('get_option_key') + ->willReturn('test_option'); + + $tab = $this->createMock(SettingsTabInterface::class); + $this->field_collection + ->method('get_tabs') + ->willReturn(['test_tab' => $tab]); + + $this->field_collection + ->method('get_field') + ->with('unknown_field') + ->willReturn(null); + + $new_input = ['test_tab' => ['unknown_field' => 'value']]; + $result = $this->manager->sanitize_settings($new_input); + + $expected = ['test_tab' => []]; + $this->assertEquals($expected, $result); + } + + public function test_sanitize_settings_prunes_redundant_tabs(): void { + $old_option = ['old_tab' => ['data' => 'value'], 'valid_tab' => ['field' => 'value']]; + update_option('test_option', $old_option); + + $this->configuration_helper + ->method('get_option_key') + ->willReturn('test_option'); + + $tab = $this->createMock(SettingsTabInterface::class); + $this->field_collection + ->method('get_tabs') + ->willReturn(['valid_tab' => $tab]); + + $field = $this->createMock(SettingsFieldInterface::class); + $field->method('sanitize_field')->willReturn('new_value'); + + $this->field_collection + ->method('get_field') + ->willReturn($field); + + $new_input = ['valid_tab' => ['field' => 'new_value']]; + $result = $this->manager->sanitize_settings($new_input); + + $this->assertArrayNotHasKey('old_tab', $result); + $this->assertArrayHasKey('valid_tab', $result); + } +} diff --git a/plugins/wpgraphql-logging/wpgraphql-logging.php b/plugins/wpgraphql-logging/wpgraphql-logging.php index d9667991..c7a1b017 100644 --- a/plugins/wpgraphql-logging/wpgraphql-logging.php +++ b/plugins/wpgraphql-logging/wpgraphql-logging.php @@ -179,4 +179,4 @@ function wpgraphql_logging_load_textdomain(): void { /** @psalm-suppress HookNotFound */ add_action( 'plugins_loaded', static function (): void { wpgraphql_logging_init(); -}, 10, 0 ); +}, 100, 0 ); From 308217aa917cd1cbde31277f1e74f0f591b1df9d Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 26 Sep 2025 16:30:23 +0100 Subject: [PATCH 04/18] Fixed bug when query is empty. Added tests for DownloadLogService. --- .../View/Download/DownloadLogService.php | 51 +++- .../src/Admin/ViewLogsPage.php | 3 + .../src/Logger/Database/DatabaseEntity.php | 5 +- .../View/Download/DownloadLogServiceTest.php | 277 ++++++++++++++++++ 4 files changed, 320 insertions(+), 16 deletions(-) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php diff --git a/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php b/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php index 4032be55..da7f6cef 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php @@ -5,7 +5,9 @@ namespace WPGraphQL\Logging\Admin\View\Download; use League\Csv\Writer; +use WPGraphQL\Logging\Logger\Database\DatabaseEntity; use WPGraphQL\Logging\Logger\Database\LogsRepository; +use WPGraphQL\Logging\Logger\LoggerService; /** * Service for handling log downloads. @@ -49,6 +51,22 @@ public function generate_csv( int $log_id ): void { } $writer = Writer::createFromStream( $output ); + $headers = $this->get_headers( $log ); + $content = $this->get_content( $log ); + $writer->insertOne( $headers ); + $writer->insertOne( $content ); + fclose( $output ); + exit; + } + + /** + * Get default CSV headers. + * + * @param DatabaseEntity $log The log entry. + * + * @return array The default CSV headers. + */ + public function get_headers(DatabaseEntity $log): array { $headers = [ 'ID', 'Date', @@ -60,25 +78,28 @@ public function generate_csv( int $log_id ): void { 'Context', 'Extra', ]; + return apply_filters( 'wpgraphql_logging_csv_headers', $headers, $log->get_id(), $log ); + } + /** + * Get CSV content for a log entry. + * + * @param DatabaseEntity $log The log entry. + * + * @return array The CSV content for the log entry. + */ + public function get_content(DatabaseEntity $log): array { $content = [ - $log->get_id(), - $log->get_datetime(), - $log->get_level(), - $log->get_level_name(), - $log->get_message(), - $log->get_channel(), - $log->get_query(), + $log->get_id() ?? '', + $log->get_datetime() ?? '', + $log->get_level() ?? '', + $log->get_level_name() ?? '', + $log->get_message() ?? '', + $log->get_channel() ?? '', + $log->get_query() ?? '', wp_json_encode( $log->get_context() ), wp_json_encode( $log->get_extra() ), ]; - - - $headers = apply_filters( 'wpgraphql_logging_csv_headers', $headers, $log_id, $log ); - $content = apply_filters( 'wpgraphql_logging_csv_content', $content, $log_id, $log ); - $writer->insertOne( $headers ); - $writer->insertOne( $content ); - fclose( $output ); - exit; + return apply_filters( 'wpgraphql_logging_csv_content', $content, $log->get_id(), $log ); } } diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index 6942b05d..7280f8e9 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -130,6 +130,9 @@ public function enqueue_admin_scripts( string $hook_suffix ): void { 'jquery-ui-timepicker-addon', 'jQuery(document).ready(function($){ $(".wpgraphql-logging-datepicker").datetimepicker({ dateFormat: "yy-mm-dd", timeFormat: "HH:mm:ss" }); });' ); + + // Allow other plugins to enqueue their own scripts/styles. + do_action( 'wpgraphql_logging_view_logs_admin_enqueue_scripts', $hook_suffix ); } /** diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index c1ea5a18..424c457c 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -250,7 +250,10 @@ public function get_query(): ?string { return null; } - $query = $context['query']; + $query = $context['query'] ?? null; + if ( is_string( $query ) ) { + return $query; + } $request = $context['request'] ?? null; if ( empty( $request ) || ! is_array( $request ) ) { diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php new file mode 100644 index 00000000..f9710ed8 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php @@ -0,0 +1,277 @@ + 'wpgraphql_logging', + 'level' => 200, + 'level_name' => 'INFO', + 'message' => 'WPGraphQL Outgoing Response', + 'context' => [ + 'site_url' => 'http://test.local', + 'wp_version' => '6.8.2', + 'wp_debug_mode' => true, + 'plugin_version'=> '0.0.1' + ], + 'extra' => [ + 'ip' => '127.0.0.1', + 'url' => '/index.php?graphql', + 'server' => 'test.local', + 'referrer' => 'http://test.local/wp-admin/admin.php?page=graphiql-ide', + 'process_id' => 5819, + 'http_method' => 'POST', + 'memory_usage' => '14 MB', + 'wpgraphql_query' => 'query GetPost($uri: ID!) { post(id: $uri, idType: URI) { title content } }', + 'memory_peak_usage' => '14 MB', + 'wpgraphql_variables' => [ + 'uri' => 'hello-world' + ], + 'wpgraphql_operation_name' => 'GetPost' + ] + ]; + + + public function setUp(): void { + parent::setUp(); + $this->service = new DownloadLogService(); + $this->repository = new LogsRepository(); + } + + public function tearDown(): void { + $this->repository->delete_all(); + parent::tearDown(); + } + + public function set_as_admin(): void { + $admin_user = $this->factory->user->create(['role' => 'administrator']); + wp_set_current_user($admin_user); + set_current_screen('dashboard'); + } + + public function test_generate_csv_requires_admin_capabilities(): void { + // Test without admin capabilities + wp_set_current_user(0); + + $this->expectException(\WPDieException::class); + $this->expectExceptionMessage('You do not have sufficient permissions to access this page.'); + + $this->service->generate_csv(1); + } + + public function test_generate_csv_requires_valid_log_id_not_zero(): void { + $this->set_as_admin(); + + $this->expectException(\WPDieException::class); + $this->expectExceptionMessage('Invalid log ID.'); + + $this->service->generate_csv(0); + } + + public function test_generate_csv_requires_valid_log_id_in_database(): void { + $this->set_as_admin(); + + $this->expectException(\WPDieException::class); + $this->expectExceptionMessage('Log not found.'); + + $this->service->generate_csv(9999999); + } + + public function test_generate_csv_returns_valid_csv(): void { + $this->set_as_admin(); + $entity = DatabaseEntity::create(...array_values($this->fixture)); + $log_id = $entity->save(); + + + $headers = $this->service->get_headers($entity); + $content = $this->service->get_content($entity); + + $this->assertSame([ + 'ID', + 'Date', + 'Level', + 'Level Name', + 'Message', + 'Channel', + 'Query', + 'Context', + 'Extra', + ], $headers); + + $this->assertIsArray($content); + $this->assertCount(9, $content); + $this->assertEquals($log_id, $content[0]); + $this->assertEquals($this->fixture['level'], $content[2]); + $this->assertEquals($this->fixture['level_name'], $content[3]); + $this->assertEquals($this->fixture['message'], $content[4]); + $this->assertEquals($this->fixture['channel'], $content[5]); + $this->assertEquals(wp_json_encode($this->fixture['context']), $content[7]); + $this->assertEquals(wp_json_encode($this->fixture['extra']), $content[8]); + + + // Capture output + // ob_start(); + // $this->service->generate_csv($log_id); + // $output = ob_get_clean(); + + // // // Parse CSV output + // $lines = explode("\n", trim($output)); + // $headers = str_getcsv($lines[0]); + // $content = str_getcsv($lines[1]); + + // // Verify headers + // $expected_headers = [ + // 'ID', + // 'Date', + // 'Level', + // 'Level Name', + // 'Message', + // 'Channel', + // 'Query', + // 'Context', + // 'Extra', + // ]; + // $this->assertEquals($expected_headers, $headers); + + // // Verify content + // $this->assertEquals($log_id, $content[0]); + // $this->assertEquals('2023-01-01 12:00:00', $content[1]); + // $this->assertEquals('200', $content[2]); + // $this->assertEquals('INFO', $content[3]); + // $this->assertEquals('Test log message', $content[4]); + // $this->assertEquals('wpgraphql', $content[5]); + // $this->assertEquals('query { posts { nodes { id title } } }', $content[6]); + // $this->assertEquals('{"user_id":1}', $content[7]); + // $this->assertEquals('{"test":"data"}', $content[8]); + // } + + // public function test_generate_csv_with_nonexistent_log(): void { + // // Set admin user + // $admin_user = $this->factory->user->create(['role' => 'administrator']); + // wp_set_current_user($admin_user); + // set_current_screen('dashboard'); + + // $this->expectOutputString(''); + + // ob_start(); + // $this->service->generate_csv(999999); + // $output = ob_get_clean(); + + // $this->assertEmpty($output); + } + + // public function test_csv_filename_filter(): void { + // // Set admin user + // $admin_user = $this->factory->user->create(['role' => 'administrator']); + // wp_set_current_user($admin_user); + // set_current_screen('dashboard'); + + // // Create test log entry + // global $wpdb; + // $wpdb->insert( + // $wpdb->prefix . 'wpgraphql_logs', + // [ + // 'level' => 200, + // 'level_name' => 'INFO', + // 'message' => 'Test message', + // 'channel' => 'wpgraphql', + // 'datetime' => '2023-01-01 12:00:00', + // 'context' => '{}', + // 'extra' => '{}', + // 'query' => 'test query' + // ] + // ); + // $log_id = $wpdb->insert_id; + + // // Add filter for filename + // add_filter('wpgraphql_logging_csv_filename', function($filename) { + // return 'custom_log_export.csv'; + // }); + + // // Check headers contain custom filename + // ob_start(); + // $this->service->generate_csv($log_id); + // $output = ob_get_contents(); + // ob_end_clean(); + + // $headers = headers_list(); + // $content_disposition_found = false; + // foreach ($headers as $header) { + // if (strpos($header, 'Content-Disposition') !== false && strpos($header, 'custom_log_export.csv') !== false) { + // $content_disposition_found = true; + // break; + // } + // } + + // $this->assertTrue($content_disposition_found); + // } + + // public function test_csv_headers_filter(): void { + // // Set admin user + // $admin_user = $this->factory->user->create(['role' => 'administrator']); + // wp_set_current_user($admin_user); + // set_current_screen('dashboard'); + + // // Create test log entry + // global $wpdb; + // $wpdb->insert( + // $wpdb->prefix . 'wpgraphql_logs', + // [ + // 'level' => 200, + // 'level_name' => 'INFO', + // 'message' => 'Test message', + // 'channel' => 'wpgraphql', + // 'datetime' => '2023-01-01 12:00:00', + // 'context' => '{}', + // 'extra' => '{}', + // 'query' => 'test query' + // ] + // ); + // $log_id = $wpdb->insert_id; + + // // Add filter for headers + // add_filter('wpgraphql_logging_csv_headers', function($headers) { + // return array_merge($headers, ['Custom Field']); + // }); + + // // Add filter for content + // add_filter('wpgraphql_logging_csv_content', function($content) { + // return array_merge($content, ['Custom Value']); + // }); + + // ob_start(); + // $this->service->generate_csv($log_id); + // $output = ob_get_contents(); + // ob_end_clean(); + + // // Parse CSV output + // $lines = explode("\n", trim($output)); + // $headers = str_getcsv($lines[0]); + // $content = str_getcsv($lines[1]); + + // // Verify custom header and content + // $this->assertContains('Custom Field', $headers); + // $this->assertContains('Custom Value', $content); + // } +} From 75c5438031cd898f76e822815012b6358062e17d Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 26 Sep 2025 17:51:54 +0100 Subject: [PATCH 05/18] Added tests for ViewLogsPage class --- .../View/Download/DownloadLogService.php | 23 +- .../src/Admin/ViewLogsPage.php | 18 +- .../src/Logger/Database/DatabaseEntity.php | 2 +- .../wpunit/Admin/View/ViewLogsPageTest.php | 211 ++++++++++++++++++ 4 files changed, 237 insertions(+), 17 deletions(-) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php diff --git a/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php b/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php index da7f6cef..2caa30ea 100644 --- a/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php +++ b/plugins/wpgraphql-logging/src/Admin/View/Download/DownloadLogService.php @@ -7,7 +7,6 @@ use League\Csv\Writer; use WPGraphQL\Logging\Logger\Database\DatabaseEntity; use WPGraphQL\Logging\Logger\Database\LogsRepository; -use WPGraphQL\Logging\Logger\LoggerService; /** * Service for handling log downloads. @@ -62,9 +61,9 @@ public function generate_csv( int $log_id ): void { /** * Get default CSV headers. * - * @param DatabaseEntity $log The log entry. + * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $log The log entry. * - * @return array The default CSV headers. + * @return array The default CSV headers. */ public function get_headers(DatabaseEntity $log): array { $headers = [ @@ -84,19 +83,19 @@ public function get_headers(DatabaseEntity $log): array { /** * Get CSV content for a log entry. * - * @param DatabaseEntity $log The log entry. + * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $log The log entry. * - * @return array The CSV content for the log entry. + * @return array The CSV content for the log entry. */ public function get_content(DatabaseEntity $log): array { $content = [ - $log->get_id() ?? '', - $log->get_datetime() ?? '', - $log->get_level() ?? '', - $log->get_level_name() ?? '', - $log->get_message() ?? '', - $log->get_channel() ?? '', - $log->get_query() ?? '', + $log->get_id(), + $log->get_datetime(), + $log->get_level(), + $log->get_level_name(), + $log->get_message(), + $log->get_channel(), + $log->get_query(), wp_json_encode( $log->get_context() ), wp_json_encode( $log->get_extra() ), ]; diff --git a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php index 7280f8e9..62fa5522 100644 --- a/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php +++ b/plugins/wpgraphql-logging/src/Admin/ViewLogsPage.php @@ -184,7 +184,19 @@ public function process_filters_redirect(): void { return; } - $redirect_url = menu_page_url( self::ADMIN_PAGE_SLUG, false ); + $redirect_url = $this->get_redirect_url(); + + wp_safe_redirect( $redirect_url ); + exit; + } + + /** + * Constructs the redirect URL with filter parameters. + * + * @return string The constructed redirect URL. + */ + public function get_redirect_url(): string { + $redirect_url = menu_page_url( self::ADMIN_PAGE_SLUG, false ); $possible_filters = [ 'start_date', @@ -205,9 +217,7 @@ public function process_filters_redirect(): void { return '' !== $value; } ), $redirect_url ); $redirect_url = apply_filters( 'wpgraphql_logging_filter_redirect_url', $redirect_url, $filters ); - - wp_safe_redirect( $redirect_url ); - exit; + return (string) $redirect_url; } /** diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index 424c457c..5f9f53c3 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -239,7 +239,7 @@ public function get_datetime(): string { /** * Extracts and returns the GraphQL query from the context, if available. * - * @phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh + * @phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh, Generic.Metrics.CyclomaticComplexity.TooHigh * * @return string|null The GraphQL query string, or null if not available. */ diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php new file mode 100644 index 00000000..1b6a9673 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php @@ -0,0 +1,211 @@ +getProperty('instance'); + $instance->setAccessible(true); + $instance->setValue(null); + } + + protected function tearDown(): void { + parent::tearDown(); + + // Clean up singleton instance + $reflection = new \ReflectionClass(ViewLogsPage::class); + $instance = $reflection->getProperty('instance'); + $instance->setAccessible(true); + $instance->setValue(null); + } + + public function set_as_admin(): void { + $admin_user = $this->factory->user->create(['role' => 'administrator']); + wp_set_current_user($admin_user); + set_current_screen('dashboard'); + } + + + public function test_init_returns_null_when_user_cannot_manage_options(): void { + // Mock user without permissions + wp_set_current_user(0); + + $result = ViewLogsPage::init(); + + $this->assertNull($result); + } + + public function test_init_returns_same_instance_on_multiple_calls(): void { + $this->set_as_admin(); + $instance1 = ViewLogsPage::init(); + $instance2 = ViewLogsPage::init(); + + $this->assertSame($instance1, $instance2); + } + + public function test_enqueue_admin_scripts_only_on_correct_page(): void { + $this->set_as_admin(); + $instance = ViewLogsPage::init(); + + // Test with wrong hook suffix + $instance->enqueue_admin_scripts('different-page'); + $this->assertFalse(wp_script_is('jquery-ui-datepicker', 'enqueued')); + + // Test with correct hook suffix (simulate the page hook) + $reflection = new \ReflectionClass($instance); + $pageHookProperty = $reflection->getProperty('page_hook'); + $pageHookProperty->setAccessible(true); + $pageHookProperty->setValue($instance, 'test-page-hook'); + + $instance->enqueue_admin_scripts('test-page-hook'); + $this->assertTrue(wp_script_is('jquery-ui-datepicker', 'enqueued')); + $this->assertTrue(wp_script_is('jquery-ui-slider', 'enqueued')); + } + + public function test_get_post_value_returns_null_for_missing_key(): void { + $this->set_as_admin(); + $instance = ViewLogsPage::init(); + $reflection = new \ReflectionClass($instance); + $method = $reflection->getMethod('get_post_value'); + $method->setAccessible(true); + + $result = $method->invoke($instance, 'nonexistent_key'); + + $this->assertNull($result); + } + + public function test_register_page() : void { + $this->set_as_admin(); + $instance = ViewLogsPage::init(); + $instance->register_settings_page(); + + global $menu; + $found = false; + foreach ($menu as $item) { + if ($item[2] === ViewLogsPage::ADMIN_PAGE_SLUG) { + $found = true; + break; + } + } + + $this->assertTrue($found, 'Admin menu should contain the GraphQL Logs page'); + } + + public function test_register_admin_page() : void { + $this->set_as_admin(); + $instance = ViewLogsPage::init(); + $instance->register_settings_page(); + + // View + $_REQUEST['action'] = 'view'; + $_GET['log'] = '123'; + + ob_start(); + $instance->render_admin_page(); + $output = ob_get_clean(); + $this->assertNotFalse($output); + + // Download + $_REQUEST['action'] = 'download'; + ob_start(); + $instance->render_admin_page(); + $output = ob_get_clean(); + + $this->assertEquals('', $output); + + // Clean up + unset($_REQUEST['action'], $_GET['log']); + + + // Default + ob_start(); + $instance->render_admin_page(); + $output = ob_get_clean(); + + $this->assertStringContainsString('

      WPGraphQL Logs

      ', $output); + } + + + public function test_process_page_actions_before_rendering_as_download_action() : void { + $this->set_as_admin(); + $instance = ViewLogsPage::init(); + + // Test download action + $_GET['action'] = 'download'; + $_GET['log'] = 'nonexistent-log-id'; + + ob_start(); + $this->expectException(\WPDieException::class); + $this->expectExceptionMessage('Invalid log ID.'); + $instance->process_page_actions_before_rendering(); + $output = ob_get_clean(); + + } + + + public function test_process_page_actions_before_rendering_as_filter_action() : void { + $this->set_as_admin(); + $instance = ViewLogsPage::init(); + // Test process_filters_redirect + $_GET['action'] = 'filter'; + $_GET['log'] = 'nonexistent-log-id'; + ob_start(); + $instance->process_page_actions_before_rendering(); + $output = ob_get_clean(); + $this->assertEquals('', $output); + + // Clean up + unset($_GET['action'], $_GET['log']); + } + + public function test_process_filters_redirect_with_invalid_nonce(): void { + $this->set_as_admin(); + $instance = ViewLogsPage::init(); + + // Simulate POST request with invalid nonce + $_POST['wpgraphql_logging_nonce'] = 'invalid_nonce'; + + ob_start(); + $instance->process_filters_redirect(); + $output = ob_get_clean(); + + $this->assertEquals('', $output); + + // Clean up + unset($_POST['wpgraphql_logging_nonce']); + } + + public function test_get_redirect_url_constructs_correct_url(): void { + $this->set_as_admin(); + $instance = ViewLogsPage::init(); + + // Simulate POST data + $_POST['start_date'] = '2025-01-01 00:00:00'; + $_POST['end_date'] = '2025-12-31 23:59:59'; + $_POST['level_filter'] = [1]; // Should not be an array + $_POST['orderby'] = 'id'; + $_POST['order'] = 'ASC'; + + $url = $instance->get_redirect_url(); + $this->assertStringNotContainsString('level_filter', $url); + $this->assertEquals( + menu_page_url(ViewLogsPage::ADMIN_PAGE_SLUG, false) . + '&start_date=2025-01-01 00:00:00&end_date=2025-12-31 23:59:59&orderby=id&order=ASC', + $url + ); + } +} From c3302a60fd7bb5aa71bf80c71f9b1dbc7b026701 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 26 Sep 2025 18:44:13 +0100 Subject: [PATCH 06/18] Added tests for ListTable. Admin test coverage now above 85%. --- .../View/Download/DownloadLogServiceTest.php | 29 +- .../wpunit/Admin/View/List/ListTableTest.php | 410 ++++++++++++++++++ 2 files changed, 431 insertions(+), 8 deletions(-) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php index f9710ed8..0a61187c 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php @@ -8,7 +8,7 @@ use WPGraphQL\Logging\Admin\View\Download\DownloadLogService; use WPGraphQL\Logging\Logger\Database\LogsRepository; use WPGraphQL\Logging\Logger\Database\DatabaseEntity; - +use Mockery; /** @@ -59,11 +59,6 @@ public function setUp(): void { $this->repository = new LogsRepository(); } - public function tearDown(): void { - $this->repository->delete_all(); - parent::tearDown(); - } - public function set_as_admin(): void { $admin_user = $this->factory->user->create(['role' => 'administrator']); wp_set_current_user($admin_user); @@ -100,8 +95,26 @@ public function test_generate_csv_requires_valid_log_id_in_database(): void { public function test_generate_csv_returns_valid_csv(): void { $this->set_as_admin(); - $entity = DatabaseEntity::create(...array_values($this->fixture)); - $log_id = $entity->save(); + // Mock a database entity instead of creating a real one + $entity = \Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_id')->andReturn(123); + $entity->shouldReceive('get_datetime')->andReturn('2023-01-01 12:00:00'); + $entity->shouldReceive('get_level')->andReturn($this->fixture['level']); + $entity->shouldReceive('get_level_name')->andReturn($this->fixture['level_name']); + $entity->shouldReceive('get_message')->andReturn($this->fixture['message']); + $entity->shouldReceive('get_channel')->andReturn($this->fixture['channel']); + $entity->shouldReceive('get_query')->andReturn($this->fixture['extra']['wpgraphql_query']); + $entity->shouldReceive('get_context')->andReturn($this->fixture['context']); + $entity->shouldReceive('get_extra')->andReturn($this->fixture['extra']); + + // Mock the repository to return our mocked entity + $this->repository = \Mockery::mock(LogsRepository::class); + $this->repository->shouldReceive('find_by_id')->with(123)->andReturn($entity); + + // Inject the mocked repository into the service + $this->service = new DownloadLogService($this->repository); + + $log_id = 123; $headers = $this->service->get_headers($entity); diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php new file mode 100644 index 00000000..78471014 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/List/ListTableTest.php @@ -0,0 +1,410 @@ +repository = Mockery::mock(LogsRepository::class); + $this->list_table = new ListTable($this->repository); + } + + public function tearDown(): void { + Mockery::close(); + parent::tearDown(); + } + + public function test_constructor_sets_default_args(): void { + $args = [ + 'singular' => 'Custom Log', + 'plural' => 'Custom Logs', + 'ajax' => true, + ]; + + $list_table = new ListTable($this->repository, $args); + + $this->assertInstanceOf(ListTable::class, $list_table); + } + + public function test_get_columns_returns_expected_columns(): void { + $columns = $this->list_table->get_columns(); + + $expected_columns = [ + 'cb', + 'id', + 'date', + 'wpgraphql_query', + 'level', + 'level_name', + 'event', + 'process_id', + 'request_headers', + 'memory_usage', + ]; + + foreach ($expected_columns as $column) { + $this->assertArrayHasKey($column, $columns); + } + } + + public function test_get_bulk_actions_returns_expected_actions(): void { + $actions = $this->list_table->get_bulk_actions(); + + $this->assertArrayHasKey('delete', $actions); + $this->assertArrayHasKey('delete_all', $actions); + $this->assertEquals('Delete Selected', $actions['delete']); + $this->assertEquals('Delete All', $actions['delete_all']); + } + + public function test_get_sortable_columns_returns_expected_columns(): void { + $reflection = new \ReflectionClass($this->list_table); + $method = $reflection->getMethod('get_sortable_columns'); + $method->setAccessible(true); + + $sortable = $method->invoke($this->list_table); + + $this->assertArrayHasKey('id', $sortable); + $this->assertArrayHasKey('date', $sortable); + $this->assertArrayHasKey('level', $sortable); + $this->assertArrayHasKey('level_name', $sortable); + } + + public function test_column_cb_returns_checkbox_for_valid_item(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_id')->andReturn(123); + + $result = $this->list_table->column_cb($entity); + + $this->assertStringContainsString('assertStringContainsString('value="123"', $result); + } + + public function test_column_cb_returns_empty_string_for_invalid_item(): void { + $result = $this->list_table->column_cb('invalid'); + + $this->assertEquals('', $result); + } + + public function test_column_id_returns_formatted_id_with_actions(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_id')->andReturn(456); + + $result = $this->list_table->column_id($entity); + + $this->assertStringContainsString('456', $result); + $this->assertStringContainsString('View', $result); + $this->assertStringContainsString('Download', $result); + } + + public function test_column_default_returns_null_for_invalid_item(): void { + $result = $this->list_table->column_default('invalid', 'date'); + + $this->assertNull($result); + } + + public function test_column_default_returns_date_for_date_column(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_datetime')->andReturn('2023-01-01 12:00:00'); + + $result = $this->list_table->column_default($entity, 'date'); + + $this->assertEquals('2023-01-01 12:00:00', $result); + } + + public function test_column_default_returns_level_for_level_column(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_level')->andReturn(200); + + $result = $this->list_table->column_default($entity, 'level'); + + $this->assertEquals(200, $result); + } + + public function test_get_query_returns_formatted_query(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_query')->andReturn('{ user { id name } }'); + + $result = $this->list_table->get_query($entity); + + $this->assertStringContainsString('assertStringContainsString('{ user { id name } }', $result); + } + + public function test_get_query_returns_empty_string_for_empty_query(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_query')->andReturn(''); + + $result = $this->list_table->get_query($entity); + + $this->assertEquals('', $result); + } + + public function test_get_event_returns_event_from_extra(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn(['wpgraphql_event' => 'query_executed']); + + $result = $this->list_table->get_event($entity); + + $this->assertEquals('query_executed', $result); + } + + public function test_get_event_returns_message_when_no_event_in_extra(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn([]); + $entity->shouldReceive('get_message')->andReturn('Default message'); + + $result = $this->list_table->get_event($entity); + + $this->assertEquals('Default message', $result); + } + + public function test_get_process_id_returns_process_id_from_extra(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn(['process_id' => '12345']); + + $result = $this->list_table->get_process_id($entity); + + $this->assertEquals(12345, $result); + } + + public function test_get_process_id_returns_zero_when_not_in_extra(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn([]); + + $result = $this->list_table->get_process_id($entity); + + $this->assertEquals(0, $result); + } + + public function test_get_memory_usage_returns_memory_from_extra(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn(['memory_peak_usage' => '2MB']); + + $result = $this->list_table->get_memory_usage($entity); + + $this->assertEquals('2MB', $result); + } + + public function test_get_request_headers_returns_formatted_headers(): void { + $headers = ['Content-Type' => 'application/json', 'Authorization' => 'Bearer token']; + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn(['request_headers' => $headers]); + + $result = $this->list_table->get_request_headers($entity); + + $this->assertStringContainsString('assertStringContainsString('Content-Type', $result); + $this->assertStringContainsString('Authorization', $result); + } + + public function test_get_request_headers_returns_empty_string_for_empty_headers(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn([]); + + $result = $this->list_table->get_request_headers($entity); + + $this->assertEquals('', $result); + } + + public function test_format_code_returns_formatted_pre_tag(): void { + $reflection = new \ReflectionClass($this->list_table); + $method = $reflection->getMethod('format_code'); + $method->setAccessible(true); + + $result = $method->invoke($this->list_table, 'test code'); + + $this->assertStringContainsString('assertStringContainsString('test code', $result); + $this->assertStringContainsString('overflow-x: auto', $result); + } + + public function test_format_code_returns_empty_string_for_empty_input(): void { + $reflection = new \ReflectionClass($this->list_table); + $method = $reflection->getMethod('format_code'); + $method->setAccessible(true); + + $result = $method->invoke($this->list_table, ''); + + $this->assertEquals('', $result); + } + + public function test_process_where_returns_empty_array_for_invalid_nonce(): void { + $reflection = new \ReflectionClass($this->list_table); + $method = $reflection->getMethod('process_where'); + $method->setAccessible(true); + + $request = ['wpgraphql_logging_nonce' => 'invalid_nonce']; + $result = $method->invoke($this->list_table, $request); + + $this->assertEquals([], $result); + } + + public function test_process_where_handles_level_filter(): void { + $reflection = new \ReflectionClass($this->list_table); + $method = $reflection->getMethod('process_where'); + $method->setAccessible(true); + + $request = ['level_filter' => 'ERROR']; + $result = $method->invoke($this->list_table, $request); + + $this->assertContains("level_name = 'ERROR'", $result); + } + + public function test_process_where_handles_date_filters(): void { + $reflection = new \ReflectionClass($this->list_table); + $method = $reflection->getMethod('process_where'); + $method->setAccessible(true); + + $request = [ + 'start_date' => '2023-01-01', + 'end_date' => '2023-12-31' + ]; + $result = $method->invoke($this->list_table, $request); + + $this->assertCount(2, $result); + $this->assertStringContainsString("datetime >= '2023-01-01", $result[0]); + $this->assertStringContainsString("datetime <= '2023-12-31", $result[1]); + } + + public function test_prepare_items_sets_pagination_args(): void { + $this->repository->shouldReceive('get_log_count')->andReturn(50); + $this->repository->shouldReceive('get_logs')->andReturn([]); + + $_REQUEST = []; + + $this->list_table->prepare_items(); + + // Verify that pagination was set up (indirectly through no exceptions) + $this->assertTrue(true); + } + + public function test_prepare_items_handles_orderby_and_order_params(): void { + $this->repository->shouldReceive('get_log_count')->andReturn(10); + $this->repository->shouldReceive('get_logs')->andReturn([]); + + $_REQUEST = [ + 'orderby' => 'date', + 'order' => 'DESC' + ]; + + $this->list_table->prepare_items(); + + // Verify that no exceptions were thrown + $this->assertTrue(true); + } + + public function test_column_query_returns_query_from_extra(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn(['wpgraphql_query' => '{ user { id name } }']); + + $result = $this->list_table->column_query($entity); + + $this->assertEquals('{ user { id name } }', $result); + } + + public function test_column_default_returns_channel_for_channel_column(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_channel')->andReturn('wpgraphql'); + + $result = $this->list_table->column_default($entity, 'channel'); + + $this->assertEquals('wpgraphql', $result); + } + + public function test_column_default_returns_level_name_for_level_name_column(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_level_name')->andReturn('ERROR'); + + $result = $this->list_table->column_default($entity, 'level_name'); + + $this->assertEquals('ERROR', $result); + } + + public function test_column_default_returns_message_for_message_column(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_message')->andReturn('Test log message'); + + $result = $this->list_table->column_default($entity, 'message'); + + $this->assertEquals('Test log message', $result); + } + + public function test_column_default_returns_event_for_event_column(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn(['wpgraphql_event' => 'query_executed']); + + $result = $this->list_table->column_default($entity, 'event'); + + $this->assertEquals('query_executed', $result); + } + + public function test_column_default_returns_process_id_for_process_id_column(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn(['process_id' => '98765']); + + $result = $this->list_table->column_default($entity, 'process_id'); + + $this->assertEquals(98765, $result); + } + + public function test_column_default_returns_memory_usage_for_memory_usage_column(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn(['memory_peak_usage' => '5MB']); + + $result = $this->list_table->column_default($entity, 'memory_usage'); + + $this->assertEquals('5MB', $result); + } + + public function test_column_default_returns_query_for_wpgraphql_query_column(): void { + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_query')->andReturn('{ posts { id title } }'); + + $result = $this->list_table->column_default($entity, 'wpgraphql_query'); + + $this->assertStringContainsString('assertStringContainsString('{ posts { id title } }', $result); + } + + public function test_column_default_returns_headers_for_request_headers_column(): void { + $headers = ['User-Agent' => 'Test Agent', 'Accept' => 'application/json']; + $entity = Mockery::mock(DatabaseEntity::class); + $entity->shouldReceive('get_extra')->andReturn(['request_headers' => $headers]); + + $result = $this->list_table->column_default($entity, 'request_headers'); + + $this->assertStringContainsString('assertStringContainsString('User-Agent', $result); + $this->assertStringContainsString('Test Agent', $result); + } + + public function test_column_default_returns_empty_string_for_unknown_column(): void { + $entity = Mockery::mock(DatabaseEntity::class); + + $result = $this->list_table->column_default($entity, 'unknown_column'); + + $this->assertEquals('', $result); + } + +} From 716d53493de94adae8f03fa6595f5a3ca06dffa1 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 26 Sep 2025 19:41:11 +0100 Subject: [PATCH 07/18] Fixed namespaces for test classes. Added missing tests for the database entity. --- .../src/Logger/Database/LogsRepository.php | 1 - .../Settings/ConfigurationHelperTest.php | 2 +- .../Fields/Field/CheckboxFieldTest.php | 2 +- .../Settings/Fields/Field/SelectFieldTest.php | 2 +- .../Fields/Field/TextInputFieldTest.php | 2 +- .../Fields/Field/TextIntegerFieldTest.php | 2 +- .../Fields/SettingsFieldCollectionTest.php | 2 +- .../Admin/Settings/Menu/MenuPageTest.php | 2 +- .../Settings/SettingsFormManagerTest.php | 2 +- .../tests/wpunit/Admin/SettingsPageTest.php | 2 +- .../View/Download/DownloadLogServiceTest.php | 2 +- .../wpunit/Admin/View/ViewLogsPageTest.php | 8 +- .../tests/wpunit/Core/ActivationTest.php | 6 +- .../tests/wpunit/Core/AutoloaderTest.php | 6 +- .../tests/wpunit/Core/DeactivationTest.php | 5 +- .../tests/wpunit/Core/PluginTest.php | 7 +- .../tests/wpunit/Events/EventManagerTest.php | 6 +- .../Logger/Database/DatabaseEntityTest.php | 126 +++++++++++++++++- .../tests/wpunit/Logger/LoggerServiceTest.php | 6 +- 19 files changed, 165 insertions(+), 26 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php index bca698bf..66db4731 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -22,7 +22,6 @@ class LogsRepository { * @return array<\WPGraphQL\Logging\Logger\Database\DatabaseEntity> */ public function get_logs(array $args = []): array { - global $wpdb; $defaults = [ 'number' => 100, 'offset' => 0, diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/ConfigurationHelperTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/ConfigurationHelperTest.php index 041a3d96..9bc7b5f1 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/ConfigurationHelperTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/ConfigurationHelperTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\WPUnit\Admin\Settings; +namespace WPGraphQL\Logging\Tests\Admin\Settings; use WPGraphQL\Logging\Admin\Settings\ConfigurationHelper; use Codeception\TestCase\WPTestCase; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php index aa6e891d..e913e7d8 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/CheckboxFieldTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WPGraphQL\Logging\wpunit\Admin\Settings\Fields\Field; +namespace WPGraphQL\Logging\Tests\Admin\Settings\Fields\Field; use WPGraphQL\Logging\Admin\Settings\Fields\Field\CheckboxField; use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldInterface; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php index 29e785d9..16005866 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/SelectFieldTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WPGraphQL\Logging\wpunit\Admin\Settings\Fields\Field; +namespace WPGraphQL\Logging\Tests\Admin\Settings\Fields\Field; use WPGraphQL\Logging\Admin\Settings\Fields\Field\SelectField; use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldInterface; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php index bafbf4ea..2bce18f7 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextInputFieldTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WPGraphQL\Logging\wpunit\Admin\Settings\Fields\Field; +namespace WPGraphQL\Logging\Tests\Admin\Settings\Fields; use WPGraphQL\Logging\Admin\Settings\Fields\Field\TextInputField; use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldInterface; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextIntegerFieldTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextIntegerFieldTest.php index 8491506a..d7267bde 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextIntegerFieldTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/Field/TextIntegerFieldTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WPGraphQL\Logging\wpunit\Admin\Settings\Fields\Field; +namespace WPGraphQL\Logging\Tests\Admin\Settings\Fields; use WPGraphQL\Logging\Admin\Settings\Fields\Field\TextIntegerField; use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldInterface; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/SettingsFieldCollectionTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/SettingsFieldCollectionTest.php index c242530e..ec73f584 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/SettingsFieldCollectionTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Fields/SettingsFieldCollectionTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\WPUnit\Admin\Settings\Fields; +namespace WPGraphQL\Logging\Tests\Admin\Settings\Fields; use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldCollection; use WPGraphQL\Logging\Admin\Settings\Fields\SettingsFieldInterface; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Menu/MenuPageTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Menu/MenuPageTest.php index 72d9b9b6..6bc0297c 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Menu/MenuPageTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/Menu/MenuPageTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\WPUnit\Admin\Settings\Menu; +namespace WPGraphQL\Logging\Tests\Admin\Settings\Menu; use WPGraphQL\Logging\Admin\SettingsPage; use WPGraphQL\Logging\Admin\Settings\Menu\MenuPage; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/SettingsFormManagerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/SettingsFormManagerTest.php index 0a2e8a71..1108f550 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/SettingsFormManagerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/Settings/SettingsFormManagerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WPGraphQL\Logging\Tests\WPUnit\Admin\Settings; +namespace WPGraphQL\Logging\Tests\Admin\Settings; use Codeception\TestCase\WPTestCase; use WPGraphQL\Logging\Admin\Settings\ConfigurationHelper; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/SettingsPageTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/SettingsPageTest.php index d8d4bb34..7680b9fd 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/SettingsPageTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/SettingsPageTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WPGraphQL\Logging\wpunit\Admin; +namespace WPGraphQL\Logging\Tests\Admin; use lucatume\WPBrowser\TestCase\WPTestCase; use WPGraphQL\Logging\Admin\SettingsPage; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php index 0a61187c..92557cea 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/Download/DownloadLogServiceTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WPGraphQL\Logging\wpunit\Admin\View\Download; +namespace WPGraphQL\Logging\Tests\Admin\View\Download; use lucatume\WPBrowser\TestCase\WPTestCase; use WPGraphQL\Logging\Admin\View\Download\DownloadLogService; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php index 1b6a9673..12968345 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Admin/View/ViewLogsPageTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\WPUnit\Admin\View; +namespace WPGraphQL\Logging\Tests\Admin\View; use WPGraphQL\Logging\Admin\ViewLogsPage; @@ -10,6 +10,12 @@ use Codeception\TestCase\WPTestCase; use Brain\Monkey; +/** + * Test for the ViewLogsPage + * + * @package WPGraphQL\Logging + * @since 0.0.1 + */ class ViewLogsPageTest extends WPTestCase { diff --git a/plugins/wpgraphql-logging/tests/wpunit/Core/ActivationTest.php b/plugins/wpgraphql-logging/tests/wpunit/Core/ActivationTest.php index e75b7f25..a241ce24 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Core/ActivationTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Core/ActivationTest.php @@ -8,8 +8,12 @@ use lucatume\WPBrowser\TestCase\WPTestCase; use WPGraphQL\Logging\Logger\Database\DatabaseEntity; + /** - * Test class for the activation callback. + * Test for the activation callback + * + * @package WPGraphQL\Logging + * @since 0.0.1 */ class ActivationTest extends WPTestCase { diff --git a/plugins/wpgraphql-logging/tests/wpunit/Core/AutoloaderTest.php b/plugins/wpgraphql-logging/tests/wpunit/Core/AutoloaderTest.php index df2d03a4..7cb4fb15 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Core/AutoloaderTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Core/AutoloaderTest.php @@ -8,8 +8,12 @@ use lucatume\WPBrowser\TestCase\WPTestCase; use ReflectionClass; + /** - * Test class for the Autoloader. + * Test for the Autoloader + * + * @package WPGraphQL\Logging + * @since 0.0.1 */ class Autoloader_Test extends WPTestCase { diff --git a/plugins/wpgraphql-logging/tests/wpunit/Core/DeactivationTest.php b/plugins/wpgraphql-logging/tests/wpunit/Core/DeactivationTest.php index bcc0f387..152736ad 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Core/DeactivationTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Core/DeactivationTest.php @@ -7,9 +7,12 @@ use lucatume\WPBrowser\TestCase\WPTestCase; + /** - * Test class for the de-activation callback. + * Test for the deactivation callback * + * @package WPGraphQL\Logging + * @since 0.0.1 */ class DeactivationTest extends WPTestCase { protected function setUp(): void { diff --git a/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php b/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php index ef96cc1d..d288f9cc 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php @@ -9,11 +9,12 @@ use ReflectionClass; use WPGraphQL\Logging\Logger\Database\DatabaseEntity; + /** - * Class PluginTest - * - * Tests for the Plugin class + * Test for the Plugin * + * @package WPGraphQL\Logging + * @since 0.0.1 */ class PluginTest extends WPTestCase { diff --git a/plugins/wpgraphql-logging/tests/wpunit/Events/EventManagerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Events/EventManagerTest.php index ae427157..72ccf5ff 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Events/EventManagerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Events/EventManagerTest.php @@ -9,10 +9,12 @@ use WPGraphQL\Logging\Events\EventManager; use WPGraphQL\Logging\Events\Events; + /** - * Class EventManagerTest + * Test for the EventManager * - * Tests for the EventManager class. + * @package WPGraphQL\Logging + * @since 0.0.1 */ class EventManagerTest extends WPTestCase { diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php index cc0b1e05..415d1d0f 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php @@ -2,17 +2,16 @@ declare(strict_types=1); -namespace WPGraphQL\Logging\Tests\Database; +namespace WPGraphQL\Logging\Tests\Logger\Database; use lucatume\WPBrowser\TestCase\WPTestCase; use DateTimeImmutable; use ReflectionClass; use WPGraphQL\Logging\Logger\Database\DatabaseEntity; +use Mockery; /** - * Class DatabaseEntityTest - * - * Tests for the DatabaseEntity class. + * Test for the DatabaseEntity * * @package WPGraphQL\Logging * @since 0.0.1 @@ -178,4 +177,123 @@ public function test_save_returns_zero_on_database_failure(): void // 6. Assert that the save method returned 0, indicating failure. $this->assertSame(0, $result_id, 'The save() method should return 0 when the database insert fails.'); } + + public function test_get_query() : void + { + $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity->shouldReceive('get_context')->andReturn([ + 'query' => 'query GetAllPosts { posts { nodes { title content } } }' + ]); + + $this->assertEquals( + 'query GetAllPosts { posts { nodes { title content } } }', + $mockEntity->get_query() + ); + } + + public function test_get_query_in_request() : void + { + $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity->shouldReceive('get_context')->andReturn([ + 'request' => [ + 'params' => [ + 'query' => 'query GetAllPosts { posts { nodes { title content } } }' + ] + ] + ]); + + $this->assertEquals( + 'query GetAllPosts { posts { nodes { title content } } }', + $mockEntity->get_query() + ); + } + + public function test_get_invalid_query() : void + { + $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity->shouldReceive('get_context')->andReturn([ + 'request' => 'query GetAllPosts { posts { nodes { title content } } }' + ]); + + $this->assertNull( + $mockEntity->get_query() + ); + + $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity->shouldReceive('get_context')->andReturn([ + 'request' => [ + 'query GetAllPosts { posts { nodes { title content } } }' + ] + ]); + + $this->assertNull( + $mockEntity->get_query() + ); + + $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity->shouldReceive('get_context')->andReturn([]); + + $this->assertNull( + $mockEntity->get_query() + ); + + + $mockEntity = Mockery::mock(DatabaseEntity::class)->makePartial(); + $mockEntity->shouldReceive('get_context')->andReturn([ + 'request' => [ + 'params' => [ + 'invalid_key' => 'query GetAllPosts { posts { nodes { title content } } }' + ] + ] + ]); + + $this->assertNull( + $mockEntity->get_query() + ); + } + + public function test_find_logs() : void { + + $log_data = [ + 'channel' => 'wpgraphql_logging', + 'level' => 200, + 'level_name' => 'INFO', + 'message' => 'WPGraphQL Outgoing Response', + 'context' => [ + 'site_url' => 'http://test.local', + 'wp_version' => '6.8.2', + 'wp_debug_mode' => true, + 'plugin_version'=> '0.0.1' + ], + 'extra' => [ + 'ip' => '127.0.0.1', + 'url' => '/index.php?graphql', + 'server' => 'test.local', + 'referrer' => 'http://test.local/wp-admin/admin.php?page=graphiql-ide', + 'process_id' => 5819, + 'http_method' => 'POST', + 'memory_usage' => '14 MB', + 'wpgraphql_query' => 'query GetPost($uri: ID!) { post(id: $uri, idType: URI) { title content } }', + 'memory_peak_usage' => '14 MB', + 'wpgraphql_variables' => [ + 'uri' => 'hello-world' + ], + 'wpgraphql_operation_name' => 'GetPost' + ] + ]; + + // Create and save the entity + $entity = DatabaseEntity::create(...array_values($log_data)); + $insert_id = $entity->save(); + + $where_clauses = [ + 'level' => 200, + 'id' => $insert_id + ]; + $logs = DatabaseEntity::find_logs(10, 0, $where_clauses, 'id', 'DESC'); + $this->assertIsArray($logs); + $this->assertCount(1, $logs); + $this->assertInstanceOf(DatabaseEntity::class, $logs[0]); + + } } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/LoggerServiceTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/LoggerServiceTest.php index 3d896953..a4079113 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/LoggerServiceTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/LoggerServiceTest.php @@ -12,10 +12,12 @@ use WPGraphQL\Logging\Logger\Database\DatabaseEntity; use Monolog\LogRecord; + /** - * Class LoggerServiceTest + * Test for the LoggerService * - * Tests for the LoggerService class. + * @package WPGraphQL\Logging + * @since 0.0.1 */ class LoggerServiceTest extends WPTestCase { From 6a4557569770bdefc93f3a45c696d6ca58e26255 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 26 Sep 2025 20:35:46 +0100 Subject: [PATCH 08/18] Added tests for LogsRepository. --- .../Logger/Database/DatabaseEntityTest.php | 4 +- .../Logger/Database/LogsRepositoryTest.php | 183 ++++++++++++++++++ 2 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Logger/Database/LogsRepositoryTest.php diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php index 415d1d0f..c87023f2 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php @@ -75,7 +75,7 @@ public function test_save_method_inserts_log_into_database(): void ]; // Create and save the entity - $entity = DatabaseEntity::create(...array_values($log_data)); + $entity = DatabaseEntity::create(...$log_data); $insert_id = $entity->save(); $this->assertIsInt( $insert_id ); @@ -283,7 +283,7 @@ public function test_find_logs() : void { ]; // Create and save the entity - $entity = DatabaseEntity::create(...array_values($log_data)); + $entity = DatabaseEntity::create(...$log_data); $insert_id = $entity->save(); $where_clauses = [ diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/LogsRepositoryTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/LogsRepositoryTest.php new file mode 100644 index 00000000..bd1fa9c1 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/LogsRepositoryTest.php @@ -0,0 +1,183 @@ +logs_repository = new LogsRepository(); + + // Create the database table for testing + DatabaseEntity::create_table(); + } + + protected function tearDown(): void + { + // Clean up the database table + $this->logs_repository->delete_all(); + parent::tearDown(); + } + + public function insert_mock_data(): void + { + $log_data = [ + 'channel' => 'wpgraphql_logging', + 'level' => 200, + 'level_name' => 'INFO', + 'message' => 'WPGraphQL Outgoing Response', + 'context' => [ + 'site_url' => 'http://test.local', + 'wp_version' => '6.8.2', + 'wp_debug_mode' => true, + 'plugin_version'=> '0.0.1' + ], + 'extra' => [ + 'ip' => '127.0.0.1', + 'url' => '/index.php?graphql', + 'server' => 'test.local', + 'referrer' => 'http://test.local/wp-admin/admin.php?page=graphiql-ide', + 'process_id' => 5819, + 'http_method' => 'POST', + 'memory_usage' => '14 MB', + 'wpgraphql_query' => 'query GetPost($uri: ID!) { post(id: $uri, idType: URI) { title content } }', + 'memory_peak_usage' => '14 MB', + 'wpgraphql_variables' => [ + 'uri' => 'hello-world' + ], + 'wpgraphql_operation_name' => 'GetPost' + ] + ]; + + $log_entry = DatabaseEntity::create(...$log_data); + $log_entry->save(); + $log_data['level'] = 400; + $log_data['level_name'] = 'ERROR'; + $log_data['message'] = 'WPGraphQL Error'; + sleep(1); // Ensure different timestamps + $log_entry = DatabaseEntity::create(...$log_data); + $log_entry->save(); + } + + public function test_get_logs_with_default_args(): void + { + $this->insert_mock_data(); + $logs = $this->logs_repository->get_logs(); + $this->assertIsArray($logs); + $this->assertCount(2, $logs); + $this->assertInstanceOf(DatabaseEntity::class, $logs[0]); + $this->assertInstanceOf(DatabaseEntity::class, $logs[1]); + } + + public function test_get_logs_with_custom_args(): void + { + $this->insert_mock_data(); + $args = [ + 'number' => 50, + 'offset' => 0, + 'orderby' => 'datetime', + 'order' => 'DESC' + ]; + $logs = $this->logs_repository->get_logs($args); + $this->assertIsArray($logs); + $this->assertCount(2, $logs); + + // Should get the last inserted log first + $this->assertEquals('WPGraphQL Error', $logs[0]->get_message()); + + /** + * Test default orderby + */ + $args['orderby'] = ''; + $logs = $this->logs_repository->get_logs($args); + $this->assertIsArray($logs); + + // Should be last as default is DESC + $this->assertEquals('WPGraphQL Error', $logs[0]->get_message()); + + /** + * Test where is string should not work + */ + $args['where'] = 'level = 200'; + $logs = $this->logs_repository->get_logs($args); + $this->assertIsArray($logs); + + /** + * Test invalid order + */ + $args['order'] = ''; + $logs = $this->logs_repository->get_logs($args); + $this->assertIsArray($logs); + $this->assertCount(2, $logs); + + // Should be last one as where clause is ignored + $this->assertEquals('WPGraphQL Error', $logs[0]->get_message()); + + // Check log count + $this->assertEquals(2, $this->logs_repository->get_log_count([])); + $this->assertEquals(1, $this->logs_repository->get_log_count(["level = 400", "channel = 'wpgraphql_logging'"])); + } + + public function test_delete_logs(): void + { + $this->insert_mock_data(); + $logs = $this->logs_repository->get_logs(); + $this->assertCount(2, $logs); + + // Delete one log + $result = $this->logs_repository->delete($logs[0]->get_id()); + $this->assertTrue($result); + + // Delete invalid logs + $result = $this->logs_repository->delete(0); + $this->assertFalse($result); + + // Check remaining logs + $logs = $this->logs_repository->get_logs(); + $this->assertCount(1, $logs); + + // Delete all logs + $this->logs_repository->delete_all(); + $logs = $this->logs_repository->get_logs(); + $this->assertCount(0, $logs); + } + + public function test_delete_log_older_than(): void + { + $this->insert_mock_data(); + $logs = $this->logs_repository->get_logs(); + $this->assertCount(2, $logs); + $date = $logs[0]->get_datetime(); + $dateTime = new \DateTime($date); + + $result = $this->logs_repository->delete_log_older_than($dateTime); + $this->assertTrue($result); + $logs = $this->logs_repository->get_logs(); + $this->assertCount(1, $logs); + + // Delete last log + $dateTime->modify('+1 second'); + $result = $this->logs_repository->delete_log_older_than($dateTime); + $this->assertTrue($result); + $logs = $this->logs_repository->get_logs(); + $this->assertCount(0, $logs); + } +} From 070ce144474fa1ab1312bed9d629d045832888d8 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 26 Sep 2025 21:14:59 +0100 Subject: [PATCH 09/18] Added tests for individual rules. Updated placeholder text for excluded queries field. --- .../Fields/Tab/BasicConfigurationTab.php | 2 +- .../tests/wpunit/Rules/AdminUserRuleTest.php | 97 +++++++++++++++++++ .../tests/wpunit/Rules/EnabledRuleTest.php | 90 +++++++++++++++++ .../wpunit/Rules/ExcludeQueryRuleTest.php | 80 +++++++++++++++ .../wpunit/Rules/IpRestrictionsRuleTest.php | 93 ++++++++++++++++++ .../wpunit/Rules/LogResponseRuleTest.php | 80 +++++++++++++++ .../tests/wpunit/Rules/QueryNullRuleTest.php | 93 ++++++++++++++++++ 7 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Rules/AdminUserRuleTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Rules/EnabledRuleTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Rules/ExcludeQueryRuleTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Rules/IpRestrictionsRuleTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Rules/LogResponseRuleTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Rules/QueryNullRuleTest.php diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/BasicConfigurationTab.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/BasicConfigurationTab.php index 849e74cc..e04f8720 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/BasicConfigurationTab.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/BasicConfigurationTab.php @@ -97,7 +97,7 @@ public function get_fields(): array { __( 'Exclude Queries', 'wpgraphql-logging' ), '', __( 'Comma-separated list of GraphQL query names to exclude from logging.', 'wpgraphql-logging' ), - __( 'e.g., __schema,SeedNode,__typename', 'wpgraphql-logging' ) + __( 'e.g., __schema,GetSeedNode', 'wpgraphql-logging' ) ); $fields[ self::ADMIN_USER_LOGGING ] = new CheckboxField( diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/AdminUserRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Rules/AdminUserRuleTest.php new file mode 100644 index 00000000..b406d646 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Rules/AdminUserRuleTest.php @@ -0,0 +1,97 @@ +rule = new AdminUserRule(); + } + + public function set_admin_user() { + $user_id = $this->factory()->user->create(['role' => 'administrator']); + wp_set_current_user($user_id); + } + + public function test_get_name_returns_correct_name(): void { + $this->assertEquals('admin_user_rule', $this->rule->get_name()); + } + + public function test_passes_when_admin_user_logging_disabled(): void { + $config = [ + BasicConfigurationTab::ADMIN_USER_LOGGING => false, + ]; + + $this->assertTrue($this->rule->passes($config)); + } + + public function test_passes_when_admin_user_logging_config_missing(): void { + $config = []; + + $this->assertTrue($this->rule->passes($config)); + } + + public function test_passes_when_admin_user_logging_enabled_and_user_can_manage_options(): void { + $this->set_admin_user(); + + $config = [ + BasicConfigurationTab::ADMIN_USER_LOGGING => true, + ]; + + $this->assertTrue($this->rule->passes($config)); + + + $config = [ + BasicConfigurationTab::ADMIN_USER_LOGGING => false, + ]; + + $this->assertTrue($this->rule->passes($config)); + } + + public function test_fails_when_admin_user_logging_enabled_and_user_cannot_manage_options(): void { + $user_id = $this->factory()->user->create(['role' => 'subscriber']); + wp_set_current_user($user_id); + + $config = [ + BasicConfigurationTab::ADMIN_USER_LOGGING => true, + ]; + + $this->assertFalse($this->rule->passes($config)); + } + + public function test_fails_when_admin_user_logging_enabled_and_no_user_logged_in(): void { + wp_set_current_user(0); + + $config = [ + BasicConfigurationTab::ADMIN_USER_LOGGING => true, + ]; + + $this->assertFalse($this->rule->passes($config)); + } + + public function test_passes_with_query_string_parameter(): void { + $config = [ + BasicConfigurationTab::ADMIN_USER_LOGGING => false, + ]; + + $this->assertTrue($this->rule->passes($config, 'query { posts { id } }')); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/EnabledRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Rules/EnabledRuleTest.php new file mode 100644 index 00000000..d3b6cfa7 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Rules/EnabledRuleTest.php @@ -0,0 +1,90 @@ +rule = new EnabledRule(); + } + + public function test_get_name_returns_correct_name(): void { + $name = $this->rule->get_name(); + + $this->assertSame('enabled_rule', $name); + } + + public function test_passes_when_enabled_is_true(): void { + $config = [ + BasicConfigurationTab::ENABLED => true, + ]; + + $result = $this->rule->passes($config); + + $this->assertTrue($result); + } + + public function test_passes_when_enabled_is_false(): void { + $config = [ + BasicConfigurationTab::ENABLED => false, + ]; + + $result = $this->rule->passes($config); + + $this->assertFalse($result); + } + + public function test_passes_when_enabled_key_missing(): void { + $config = []; + + $result = $this->rule->passes($config); + + $this->assertFalse($result); + } + + public function test_passes_with_truthy_values(): void { + $config = [ + BasicConfigurationTab::ENABLED => 1, + ]; + + $result = $this->rule->passes($config); + + $this->assertTrue($result); + } + + public function test_passes_with_falsy_values(): void { + $config = [ + BasicConfigurationTab::ENABLED => 0, + ]; + + $result = $this->rule->passes($config); + + $this->assertFalse($result); + } + + public function test_passes_ignores_query_string(): void { + $config = [ + BasicConfigurationTab::ENABLED => true, + ]; + + $result = $this->rule->passes($config, 'query { posts { id } }'); + + $this->assertTrue($result); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/ExcludeQueryRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Rules/ExcludeQueryRuleTest.php new file mode 100644 index 00000000..9ab475b2 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Rules/ExcludeQueryRuleTest.php @@ -0,0 +1,80 @@ +rule = new ExcludeQueryRule(); + } + + public function test_passes_when_no_excluded_queries_configured(): void { + $config = []; + $query_string = 'query { posts { nodes { id title } } }'; + + $this->assertTrue($this->rule->passes($config, $query_string)); + } + + public function test_passes_when_excluded_queries_is_empty_string(): void { + $config = [BasicConfigurationTab::EXCLUDE_QUERY => '']; + $query_string = 'query { posts { nodes { id title } } }'; + + $this->assertTrue($this->rule->passes($config, $query_string)); + } + + public function test_passes_when_query_string_is_null(): void { + $config = [BasicConfigurationTab::EXCLUDE_QUERY => 'introspection']; + + $this->assertTrue($this->rule->passes($config, null)); + } + + public function test_fails_when_query_contains_excluded_term(): void { + $config = [BasicConfigurationTab::EXCLUDE_QUERY => 'introspection']; + $query_string = 'query IntrospectionQuery { __schema { types { name } } }'; + + $this->assertFalse($this->rule->passes($config, $query_string)); + } + + public function test_fails_with_case_insensitive_matching(): void { + $config = [BasicConfigurationTab::EXCLUDE_QUERY => 'INTROSPECTION']; + $query_string = 'query introspectionQuery { __schema { types { name } } }'; + + $this->assertFalse($this->rule->passes($config, $query_string)); + } + + public function test_handles_multiple_excluded_queries(): void { + $config = [BasicConfigurationTab::EXCLUDE_QUERY => 'introspection, __schema, GetSeedNode']; + + $this->assertFalse($this->rule->passes($config, 'query { __schema { types } }')); + $this->assertTrue($this->rule->passes($config, 'query { posts { nodes { id } } }')); + $this->assertFalse($this->rule->passes($config, 'query GetSeedNode { node(id: "1") { id } }')); + } + + public function test_passes_when_query_does_not_match_excluded_terms(): void { + $config = [BasicConfigurationTab::EXCLUDE_QUERY => 'introspection, debug']; + $query_string = 'query { posts { nodes { id title content } } }'; + + $this->assertTrue($this->rule->passes($config, $query_string)); + } + + public function test_get_name_returns_correct_name(): void { + $this->assertEquals('exclude_query_rule', $this->rule->get_name()); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/IpRestrictionsRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Rules/IpRestrictionsRuleTest.php new file mode 100644 index 00000000..b2d00d78 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Rules/IpRestrictionsRuleTest.php @@ -0,0 +1,93 @@ +rule = new IpRestrictionsRule(); + } + + public function tearDown(): void { + unset($_SERVER['REMOTE_ADDR']); + parent::tearDown(); + } + + public function test_passes_when_no_ip_restrictions_configured(): void { + $config = []; + $this->assertTrue($this->rule->passes($config)); + } + + public function test_passes_when_empty_ip_restrictions_configured(): void { + $config = [BasicConfigurationTab::IP_RESTRICTIONS => '']; + $this->assertTrue($this->rule->passes($config)); + } + + public function test_fails_when_remote_addr_not_set(): void { + $config = [BasicConfigurationTab::IP_RESTRICTIONS => '192.168.1.1']; + unset($_SERVER['REMOTE_ADDR']); + $this->assertFalse($this->rule->passes($config)); + } + + public function test_fails_when_invalid_ip_in_remote_addr(): void { + $config = [BasicConfigurationTab::IP_RESTRICTIONS => '192.168.1.1']; + $_SERVER['REMOTE_ADDR'] = 'invalid-ip'; + $this->assertFalse($this->rule->passes($config)); + } + + public function test_passes_when_ip_matches_single_allowed_ip(): void { + $config = [BasicConfigurationTab::IP_RESTRICTIONS => '192.168.1.1']; + $_SERVER['REMOTE_ADDR'] = '192.168.1.1'; + $this->assertTrue($this->rule->passes($config)); + } + + public function test_fails_when_ip_does_not_match_single_allowed_ip(): void { + $config = [BasicConfigurationTab::IP_RESTRICTIONS => '192.168.1.1']; + $_SERVER['REMOTE_ADDR'] = '192.168.1.2'; + $this->assertFalse($this->rule->passes($config)); + } + + public function test_passes_when_ip_matches_one_of_multiple_allowed_ips(): void { + $config = [BasicConfigurationTab::IP_RESTRICTIONS => '192.168.1.1,10.0.0.1,172.16.0.1']; + $_SERVER['REMOTE_ADDR'] = '10.0.0.1'; + $this->assertTrue($this->rule->passes($config)); + } + + public function test_fails_when_ip_does_not_match_any_allowed_ips(): void { + $config = [BasicConfigurationTab::IP_RESTRICTIONS => '192.168.1.1,10.0.0.1,172.16.0.1']; + $_SERVER['REMOTE_ADDR'] = '203.0.113.1'; + $this->assertFalse($this->rule->passes($config)); + } + + public function test_handles_whitespace_in_ip_list(): void { + $config = [BasicConfigurationTab::IP_RESTRICTIONS => ' 192.168.1.1 , 10.0.0.1 , 172.16.0.1 ']; + $_SERVER['REMOTE_ADDR'] = '10.0.0.1'; + $this->assertTrue($this->rule->passes($config)); + } + + public function test_works_with_ipv6_addresses(): void { + $config = [BasicConfigurationTab::IP_RESTRICTIONS => '::1,2001:db8::1']; + $_SERVER['REMOTE_ADDR'] = '::1'; + $this->assertTrue($this->rule->passes($config)); + } + + public function test_get_name_returns_correct_identifier(): void { + $this->assertEquals('ip_restrictions', $this->rule->get_name()); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/LogResponseRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Rules/LogResponseRuleTest.php new file mode 100644 index 00000000..56da36e5 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Rules/LogResponseRuleTest.php @@ -0,0 +1,80 @@ +rule = new LogResponseRule(); + } + + public function testPassesReturnsTrueWhenLogResponseIsEnabled(): void { + $config = [ + BasicConfigurationTab::LOG_RESPONSE => true, + ]; + + $result = $this->rule->passes($config); + + $this->assertTrue($result); + } + + public function testPassesReturnsFalseWhenLogResponseIsDisabled(): void { + $config = [ + BasicConfigurationTab::LOG_RESPONSE => false, + ]; + + $result = $this->rule->passes($config); + + $this->assertFalse($result); + } + + public function testPassesReturnsFalseWhenLogResponseIsNotSet(): void { + $config = []; + + $result = $this->rule->passes($config); + + $this->assertFalse($result); + } + + public function testPassesCastsValueToBoolean(): void { + $config = [ + BasicConfigurationTab::LOG_RESPONSE => 1, + ]; + + $result = $this->rule->passes($config); + + $this->assertTrue($result); + } + + public function testPassesIgnoresQueryStringParameter(): void { + $config = [ + BasicConfigurationTab::LOG_RESPONSE => true, + ]; + + $result = $this->rule->passes($config, 'query { posts { id } }'); + + $this->assertTrue($result); + } + + public function testGetNameReturnsCorrectString(): void { + $result = $this->rule->get_name(); + + $this->assertEquals('log_response_rule', $result); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/QueryNullRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Rules/QueryNullRuleTest.php new file mode 100644 index 00000000..c35bb1a1 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Rules/QueryNullRuleTest.php @@ -0,0 +1,93 @@ +rule = new QueryNullRule(); + } + + /** + * Test that the rule passes with a valid query string. + */ + public function test_passes_with_valid_query_string(): void { + $config = []; + $query_string = 'query { posts { id title } }'; + + $this->assertTrue($this->rule->passes($config, $query_string)); + } + + /** + * Test that the rule fails with null query string. + */ + public function test_fails_with_null_query_string(): void { + $config = []; + + $this->assertFalse($this->rule->passes($config, null)); + } + + /** + * Test that the rule fails with empty string. + */ + public function test_fails_with_empty_string(): void { + $config = []; + $query_string = ''; + + $this->assertFalse($this->rule->passes($config, $query_string)); + } + + /** + * Test that the rule fails with whitespace-only string. + */ + public function test_fails_with_whitespace_only_string(): void { + $config = []; + $query_string = ' '; + + $this->assertFalse($this->rule->passes($config, $query_string)); + } + + /** + * Test that the rule passes with string containing whitespace but also content. + */ + public function test_passes_with_padded_query_string(): void { + $config = []; + $query_string = ' query { posts { id } } '; + + $this->assertTrue($this->rule->passes($config, $query_string)); + } + + /** + * Test that get_name returns the expected rule name. + */ + public function test_get_name_returns_correct_name(): void { + $this->assertEquals('query_null_rule', $this->rule->get_name()); + } + + /** + * Test that config parameter doesn't affect the rule outcome. + */ + public function test_config_does_not_affect_outcome(): void { + $config_empty = []; + $config_with_data = ['some_key' => 'some_value']; + $query_string = 'query { posts { id } }'; + + $this->assertTrue($this->rule->passes($config_empty, $query_string)); + $this->assertTrue($this->rule->passes($config_with_data, $query_string)); + } +} From 59dbb0f727cc34fb8798978ac35b22143ed3405d Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Fri, 26 Sep 2025 21:21:31 +0100 Subject: [PATCH 10/18] Added test for SampleRateRule. --- .../wpunit/Rules/SamplingRateRuleTest.php | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Rules/SamplingRateRuleTest.php diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/SamplingRateRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Rules/SamplingRateRuleTest.php new file mode 100644 index 00000000..58d37425 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Rules/SamplingRateRuleTest.php @@ -0,0 +1,84 @@ +rule = new SamplingRateRule(); + } + + public function test_get_name_returns_correct_name(): void { + $this->assertEquals('enabled_rule', $this->rule->get_name()); + } + + public function test_passes_with_100_percent_sampling_rate(): void { + $config = [ + BasicConfigurationTab::DATA_SAMPLING => 100, + ]; + + $result = $this->rule->passes($config); + $this->assertTrue($result); + } + + public function test_passes_with_0_percent_sampling_rate(): void { + $config = [ + BasicConfigurationTab::DATA_SAMPLING => 0, + ]; + + $result = $this->rule->passes($config); + $this->assertFalse($result); + } + + public function test_passes_with_default_sampling_rate_when_not_set(): void { + $config = []; + + // Since default is 100, it should always pass + $result = $this->rule->passes($config); + $this->assertTrue($result); + } + + public function test_passes_with_string_sampling_rate(): void { + $config = [ + BasicConfigurationTab::DATA_SAMPLING => '50', + ]; + + $result = $this->rule->passes($config); + $this->assertIsBool($result); + } + + public function test_passes_with_null_query_string(): void { + $config = [ + BasicConfigurationTab::DATA_SAMPLING => 100, + ]; + + $result = $this->rule->passes($config, null); + $this->assertTrue($result); + } + + public function test_passes_with_query_string(): void { + $config = [ + BasicConfigurationTab::DATA_SAMPLING => 100, + ]; + + $result = $this->rule->passes($config, 'query { posts { id } }'); + $this->assertTrue($result); + } +} From 785a70f4e35a87fa55d2bf0c09507e4636f24a0c Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Mon, 29 Sep 2025 12:16:11 +0100 Subject: [PATCH 11/18] Added tests for Data Sanisation. --- .../Processors/DataSanitizationProcessor.php | 11 +- .../DataSanizationProcessorTest.php | 182 ++++++++++++++++++ .../{ => Logger}/Rules/AdminUserRuleTest.php | 0 .../{ => Logger}/Rules/EnabledRuleTest.php | 0 .../Rules/ExcludeQueryRuleTest.php | 0 .../Rules/IpRestrictionsRuleTest.php | 0 .../Rules/LogResponseRuleTest.php | 0 .../{ => Logger}/Rules/QueryNullRuleTest.php | 0 .../wpunit/Logger/Rules/RuleManagerTest.php | 105 ++++++++++ .../Rules/SamplingRateRuleTest.php | 0 10 files changed, 293 insertions(+), 5 deletions(-) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Logger/Processors/DataSanizationProcessorTest.php rename plugins/wpgraphql-logging/tests/wpunit/{ => Logger}/Rules/AdminUserRuleTest.php (100%) rename plugins/wpgraphql-logging/tests/wpunit/{ => Logger}/Rules/EnabledRuleTest.php (100%) rename plugins/wpgraphql-logging/tests/wpunit/{ => Logger}/Rules/ExcludeQueryRuleTest.php (100%) rename plugins/wpgraphql-logging/tests/wpunit/{ => Logger}/Rules/IpRestrictionsRuleTest.php (100%) rename plugins/wpgraphql-logging/tests/wpunit/{ => Logger}/Rules/LogResponseRuleTest.php (100%) rename plugins/wpgraphql-logging/tests/wpunit/{ => Logger}/Rules/QueryNullRuleTest.php (100%) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/RuleManagerTest.php rename plugins/wpgraphql-logging/tests/wpunit/{ => Logger}/Rules/SamplingRateRuleTest.php (100%) diff --git a/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php b/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php index 4fdd3ce4..4e3226aa 100644 --- a/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php +++ b/plugins/wpgraphql-logging/src/Logger/Processors/DataSanitizationProcessor.php @@ -35,7 +35,7 @@ public function __construct() { /** * Check if data sanitization is enabled. */ - protected function is_enabled(): bool { + public function is_enabled(): bool { $is_enabled = (bool) ( $this->config[ DataManagementTab::DATA_SANITIZATION_ENABLED ] ?? false ); return apply_filters( 'wpgraphql_logging_data_sanitization_enabled', $is_enabled ); } @@ -52,7 +52,8 @@ protected function get_rules(): array { if ( 'recommended' === $method ) { return apply_filters( 'wpgraphql_logging_data_sanitization_rules', $this->get_recommended_rules() ); } - return apply_filters( 'wpgraphql_logging_data_sanitization_rules', $this->get_custom_rules() ); + + return apply_filters( 'wpgraphql_logging_data_sanitization_rules', $this->get_custom_rules() ); } /** @@ -92,7 +93,7 @@ protected function get_custom_rules(): array { $field_string = trim( $field_string ); $field_list = array_filter( - explode( ',', $field_string ), + array_map( 'trim', explode( ',', $field_string ) ), static function ($value) { return '' !== $value; } @@ -102,6 +103,7 @@ static function ($value) { $rules[ $field ] = $action; } } + return $rules; } @@ -140,8 +142,7 @@ protected function &navigate_to_parent(array &$data, array $keys): ?array { $current = &$data; foreach ( $keys as $segment ) { if ( ! is_array( $current ) || ! isset( $current[ $segment ] ) ) { - $null = null; - return $null; + return null; } $current = &$current[ $segment ]; } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Processors/DataSanizationProcessorTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Processors/DataSanizationProcessorTest.php new file mode 100644 index 00000000..56ef880b --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Processors/DataSanizationProcessorTest.php @@ -0,0 +1,182 @@ +getProperty('config'); + $configProperty->setAccessible(true); + $configProperty->setValue($processor, $config); + return $processor; + } + + public function test_process_record_not_enabled(): void + { + $processor = $this->create_mock_processor( + [ + DataManagementTab::DATA_SANITIZATION_ENABLED => false, + DataManagementTab::DATA_SANITIZATION_METHOD => 'recommended', + ] + ); + + $record = new LogRecord( + new DateTimeImmutable('now'), + 'wpgraphql_logging', + Level::Info, + 'Test log message', + ['test_field' => 'test_value'], + [] + ); + $result = $processor->__invoke($record); + $this->assertSame($record, $result); + } + + + public function test_process_record_empty_custom_rules_no_processing(): void { + + $processor = $this->create_mock_processor( + [ + DataManagementTab::DATA_SANITIZATION_ENABLED => true, + DataManagementTab::DATA_SANITIZATION_METHOD => 'custom', + DataManagementTab::DATA_SANITIZATION_CUSTOM_FIELD_ANONYMIZE => '', + DataManagementTab::DATA_SANITIZATION_CUSTOM_FIELD_REMOVE => '', + DataManagementTab::DATA_SANITIZATION_CUSTOM_FIELD_TRUNCATE => '' + ] + ); + + $record = new LogRecord( + new DateTimeImmutable('now'), + 'wpgraphql_logging', + Level::Info, + 'Test log message', + ['test_field' => 'test_value'], + [] + ); + $result = $processor->__invoke($record); + $this->assertSame($record, $result); + } + + + public function test_process_record_process_recommended_rules(): void + { + $processor = $this->create_mock_processor( + [ + DataManagementTab::DATA_SANITIZATION_ENABLED => true, + DataManagementTab::DATA_SANITIZATION_METHOD => 'recommended', + ] + ); + + $record = new LogRecord( + new DateTimeImmutable('now'), + 'wpgraphql_logging', + Level::Info, + 'Test log message', + [ + 'request' => [ + 'app_context' => [ + 'viewer' => [ + 'data' => [ + 'user_email' => 'sensitive_data', + ], + 'allcaps' => 'administrator', + 'cap_key' => 'sensitive_data', + 'caps' => 'sensitive_data', + 'safe_field' => 'safe_data' + ] + ] + ], + 'query' => 'query { posts { id } }' + ], + [] + ); + $result = $processor->__invoke($record); + $this->assertNotSame($record, $result); + + $data = $result->toArray(); + + $this->assertSame($data['context'], [ + 'request' => [ + 'app_context' => [ + 'viewer' => [ + 'safe_field' => 'safe_data' + ] + ] + ], + 'query' => 'query { posts { id } }' + ]); + } + + + public function test_process_record_process_custom_rules(): void + { + $processor = $this->create_mock_processor( + [ + DataManagementTab::DATA_SANITIZATION_ENABLED => true, + DataManagementTab::DATA_SANITIZATION_METHOD => 'custom', + DataManagementTab::DATA_SANITIZATION_CUSTOM_FIELD_ANONYMIZE => 'request.app_context.viewer.user_email, request.app_context.viewer.display_name', + DataManagementTab::DATA_SANITIZATION_CUSTOM_FIELD_REMOVE => 'request.app_context.viewer.allcaps, request.app_context.viewer.cap_key', + DataManagementTab::DATA_SANITIZATION_CUSTOM_FIELD_TRUNCATE => 'request.app_context.viewer.caps' + ] + ); + + $record = new LogRecord( + new DateTimeImmutable('now'), + 'wpgraphql_logging', + Level::Info, + 'Test log message', + [ + 'request' => [ + 'app_context' => [ + 'viewer' => [ + 'display_name' => 'Sensitive Name', + 'user_email' => 'sensitive_data', + 'allcaps' => 'administrator', + 'cap_key' => 'sensitive_data', + 'caps' => 'This is a really long string that should be truncated', + 'safe_field' => 'safe_data' + ] + ] + ], + 'query' => 'query { posts { id } }' + ], + [] + ); + $result = $processor->__invoke($record); + $this->assertNotSame($record, $result); + + $data = $result->toArray(); + + $this->assertSame($data['context'], [ + 'request' => [ + 'app_context' => [ + 'viewer' => [ + 'display_name' => '***', + 'user_email' => '***', + 'caps' => 'This is a really long string that should be tru...', + 'safe_field' => 'safe_data', + ] + ] + ], + 'query' => 'query { posts { id } }' + ]); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/AdminUserRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/AdminUserRuleTest.php similarity index 100% rename from plugins/wpgraphql-logging/tests/wpunit/Rules/AdminUserRuleTest.php rename to plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/AdminUserRuleTest.php diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/EnabledRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/EnabledRuleTest.php similarity index 100% rename from plugins/wpgraphql-logging/tests/wpunit/Rules/EnabledRuleTest.php rename to plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/EnabledRuleTest.php diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/ExcludeQueryRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/ExcludeQueryRuleTest.php similarity index 100% rename from plugins/wpgraphql-logging/tests/wpunit/Rules/ExcludeQueryRuleTest.php rename to plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/ExcludeQueryRuleTest.php diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/IpRestrictionsRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/IpRestrictionsRuleTest.php similarity index 100% rename from plugins/wpgraphql-logging/tests/wpunit/Rules/IpRestrictionsRuleTest.php rename to plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/IpRestrictionsRuleTest.php diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/LogResponseRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/LogResponseRuleTest.php similarity index 100% rename from plugins/wpgraphql-logging/tests/wpunit/Rules/LogResponseRuleTest.php rename to plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/LogResponseRuleTest.php diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/QueryNullRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/QueryNullRuleTest.php similarity index 100% rename from plugins/wpgraphql-logging/tests/wpunit/Rules/QueryNullRuleTest.php rename to plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/QueryNullRuleTest.php diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/RuleManagerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/RuleManagerTest.php new file mode 100644 index 00000000..96236882 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/RuleManagerTest.php @@ -0,0 +1,105 @@ +rule_manager = new RuleManager(); + } + + public function test_add_rule(): void { + $rule = $this->createMock(LoggingRuleInterface::class); + $rule->method('get_name')->willReturn('test_rule'); + + $this->rule_manager->add_rule($rule); + + // Test that all_rules_pass works with the added rule + $rule->method('passes')->willReturn(true); + $this->assertTrue($this->rule_manager->all_rules_pass([])); + } + + public function test_all_rules_pass_with_no_rules(): void { + $this->assertTrue($this->rule_manager->all_rules_pass([])); + } + + public function test_all_rules_pass_with_single_passing_rule(): void { + $rule = $this->createMock(LoggingRuleInterface::class); + $rule->method('get_name')->willReturn('passing_rule'); + $rule->method('passes')->willReturn(true); + + $this->rule_manager->add_rule($rule); + + $this->assertTrue($this->rule_manager->all_rules_pass(['key' => 'value'])); + } + + public function test_all_rules_pass_with_single_failing_rule(): void { + $rule = $this->createMock(LoggingRuleInterface::class); + $rule->method('get_name')->willReturn('failing_rule'); + $rule->method('passes')->willReturn(false); + + $this->rule_manager->add_rule($rule); + + $this->assertFalse($this->rule_manager->all_rules_pass(['key' => 'value'])); + } + + public function test_all_rules_pass_with_multiple_passing_rules(): void { + $rule1 = $this->createMock(LoggingRuleInterface::class); + $rule1->method('get_name')->willReturn('rule_1'); + $rule1->method('passes')->willReturn(true); + + $rule2 = $this->createMock(LoggingRuleInterface::class); + $rule2->method('get_name')->willReturn('rule_2'); + $rule2->method('passes')->willReturn(true); + + $this->rule_manager->add_rule($rule1); + $this->rule_manager->add_rule($rule2); + + $this->assertTrue($this->rule_manager->all_rules_pass(['key' => 'value'])); + } + + public function test_all_rules_pass_with_mixed_rules(): void { + $passing_rule = $this->createMock(LoggingRuleInterface::class); + $passing_rule->method('get_name')->willReturn('passing_rule'); + $passing_rule->method('passes')->willReturn(true); + + $failing_rule = $this->createMock(LoggingRuleInterface::class); + $failing_rule->method('get_name')->willReturn('failing_rule'); + $failing_rule->method('passes')->willReturn(false); + + $this->rule_manager->add_rule($passing_rule); + $this->rule_manager->add_rule($failing_rule); + + $this->assertFalse($this->rule_manager->all_rules_pass(['key' => 'value'])); + } + + public function test_all_rules_pass_with_query_string(): void { + $rule = $this->createMock(LoggingRuleInterface::class); + $rule->method('get_name')->willReturn('query_rule'); + $rule->expects($this->once()) + ->method('passes') + ->with(['key' => 'value'], 'query { user { name } }') + ->willReturn(true); + + $this->rule_manager->add_rule($rule); + + $this->assertTrue($this->rule_manager->all_rules_pass(['key' => 'value'], 'query { user { name } }')); + } + +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Rules/SamplingRateRuleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/SamplingRateRuleTest.php similarity index 100% rename from plugins/wpgraphql-logging/tests/wpunit/Rules/SamplingRateRuleTest.php rename to plugins/wpgraphql-logging/tests/wpunit/Logger/Rules/SamplingRateRuleTest.php From d8b73665d1131b60a07f80f84e8e32c374252ac9 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Mon, 29 Sep 2025 15:58:04 +0100 Subject: [PATCH 12/18] Added tests for Data Deletion Scheduler. --- .../Scheduler/DataDeletionScheduler.php | 2 +- .../Scheduler/DataDeletionSchedulerTest.php | 166 ++++++++++++++++++ 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php diff --git a/plugins/wpgraphql-logging/src/Logger/Scheduler/DataDeletionScheduler.php b/plugins/wpgraphql-logging/src/Logger/Scheduler/DataDeletionScheduler.php index 9acf5d7c..3ed11d42 100644 --- a/plugins/wpgraphql-logging/src/Logger/Scheduler/DataDeletionScheduler.php +++ b/plugins/wpgraphql-logging/src/Logger/Scheduler/DataDeletionScheduler.php @@ -90,7 +90,7 @@ public function perform_deletion(): void { } try { - self::delete_old_logs( $retention_days ); + $this->delete_old_logs( $retention_days ); } catch ( \Throwable $e ) { do_action('wpgraphql_logging_cleanup_error', [ 'error_message' => $e->getMessage(), diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php new file mode 100644 index 00000000..9b4b74a9 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php @@ -0,0 +1,166 @@ +repository = new LogsRepository(); + $this->generate_logs(); + $this->initial_log_count = $this->get_total_log_count(); + // wp_clear_scheduled_hook(DataDeletionScheduler::DELETION_HOOK); + } + + protected function tearDown(): void { + $this->delete_logs(); + // wp_clear_scheduled_hook(DataDeletionScheduler::DELETION_HOOK); + parent::tearDown(); + } + + + public function generate_logs() : void { + + global $wpdb; + $table_name = DatabaseEntity::get_table_name(); + $repository = new LogsRepository(); + $now = new \DateTime(); + for ($i = 0; $i < 10; $i++) { + $entity = DatabaseEntity::create( + 'wpgraphql_logging', + 200, + 'info', + 'Test log ' . $i, + ['query' => 'query { posts { id title } }'], + [] + ); + + + $id = $entity->save(); + + // Manually set the datetime to simulate old logs + $log_date = (clone $now)->modify("-" . $i . " days"); + $wpdb->update( + $table_name, + ['datetime' => $log_date->format('Y-m-d H:i:s')], + ['id' => $id], + ['%s'], + ['%d'] + ); + } + } + + public function delete_logs() : void { + $this->repository->delete_all(); + } + + public function get_total_log_count() : int { + return $this->repository->get_log_count([]); + } + + public function test_init_creates_singleton_instance(): void { + $reflection = new \ReflectionClass(DataDeletionScheduler::class); + $instanceProperty = $reflection->getProperty('instance'); + $instanceProperty->setAccessible(true); + $instanceProperty->setValue(null, null); + + $instance1 = DataDeletionScheduler::init(); + $instance2 = DataDeletionScheduler::init(); + $this->assertSame($instance1, $instance2); + $this->assertInstanceOf(DataDeletionScheduler::class, $instance1); + $this->assertInstanceOf(DataDeletionScheduler::class, $instance2); + } + + + public function create_mock_scheduler(array $config) : DataDeletionScheduler { + $scheduler = DataDeletionScheduler::init(); + $reflection = new \ReflectionClass($scheduler); + $configProperty = $reflection->getProperty('config'); + $configProperty->setAccessible(true); + $configProperty->setValue($scheduler, $config); + return $scheduler; + } + + + public function test_data_deletion_scheduler_no_deletion_when_disabled(): void { + + $scheduler = $this->create_mock_scheduler( + [ + DataManagementTab::DATA_DELETION_ENABLED => false, + DataManagementTab::DATA_RETENTION_DAYS => 30 + ] + ); + + $scheduler->perform_deletion(); + $this->assertEquals($this->initial_log_count, $this->get_total_log_count()); + } + + public function test_data_deletion_scheduler_no_deletion_invalid_retention_days(): void { + + $scheduler = $this->create_mock_scheduler( + [ + DataManagementTab::DATA_DELETION_ENABLED => true, + DataManagementTab::DATA_RETENTION_DAYS => 'invalid_integer' + ] + ); + + $scheduler->perform_deletion(); + $this->assertEquals($this->initial_log_count, $this->get_total_log_count()); + } + + public function test_data_deletion_scheduler_no_deletion_zero_retention_days(): void { + + $scheduler = $this->create_mock_scheduler( + [ + DataManagementTab::DATA_DELETION_ENABLED => true, + DataManagementTab::DATA_RETENTION_DAYS => 0 + ] + ); + + $scheduler->perform_deletion(); + $this->assertEquals($this->initial_log_count, $this->get_total_log_count()); + } + + public function test_data_deletion_scheduler_valid_config(): void { + + $scheduler = $this->create_mock_scheduler( + [ + DataManagementTab::DATA_DELETION_ENABLED => true, + DataManagementTab::DATA_RETENTION_DAYS => 3 + ] + ); + + $expected_count = $this->repository->get_log_count(['datetime >= NOW() - INTERVAL 3 DAY']); + // Delete logs + $scheduler->perform_deletion(); + $total_count = $this->get_total_log_count(); + $this->assertLessThan($this->initial_log_count, $total_count); + $this->assertGreaterThanOrEqual(0, $total_count); + $this->assertEquals($expected_count, $total_count); + } + + public function test_schedule_deletion_schedules_event(): void { + wp_clear_scheduled_hook(DataDeletionScheduler::DELETION_HOOK); + $scheduler = DataDeletionScheduler::init(); + $this->assertFalse(wp_next_scheduled(DataDeletionScheduler::DELETION_HOOK)); + $scheduler->schedule_deletion(); + $this->assertNotFalse(wp_next_scheduled(DataDeletionScheduler::DELETION_HOOK)); + } +} From 9559fbe0baa22f308f4fbc3968bbae07af7b131d Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 30 Sep 2025 14:54:24 +0100 Subject: [PATCH 13/18] Added tests for Events Action Logger. Added fixes to the action logger after finding a few bugs. --- .../src/Events/QueryActionLogger.php | 61 +-- .../src/Logger/Rules/ExcludeQueryRule.php | 2 +- .../wpunit/Events/QueryActionLoggerTest.php | 465 ++++++++++++++++++ .../wpunit/Events/QueryEventLifecycleTest.php | 48 ++ .../Scheduler/DataDeletionSchedulerTest.php | 2 - 5 files changed, 547 insertions(+), 31 deletions(-) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Events/QueryEventLifecycleTest.php diff --git a/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php b/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php index ab18d09f..289b8358 100644 --- a/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php +++ b/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php @@ -61,16 +61,10 @@ public function __construct( LoggerService $logger, array $config ) { */ public function log_pre_request( ?string $query, ?string $operation_name, ?array $variables ): void { try { - if ( ! $this->is_logging_enabled( $this->config, $query ) ) { - return; - } - $selected_events = $this->config[ BasicConfigurationTab::EVENT_LOG_SELECTION ] ?? []; - if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { - return; - } - if ( ! in_array( Events::PRE_REQUEST, $selected_events, true ) ) { + if ( ! $this->should_log_event( Events::PRE_REQUEST, $query ) ) { return; } + $context = [ 'query' => $query, 'variables' => $variables, @@ -97,23 +91,21 @@ public function log_pre_request( ?string $query, ?string $operation_name, ?array public function log_graphql_before_execute( Request $request ): void { try { /** @var \GraphQL\Server\OperationParams $params */ - $params = $request->params; + $params = $request->params; + if ( null === $params || ! \is_object( $params ) ) { + return; + } + $query = $params->query; + if ( ! $this->should_log_event( Events::BEFORE_GRAPHQL_EXECUTION, $query ) ) { + return; + } + $context = [ - 'query' => $params->query, + 'query' => $query, 'operation_name' => $params->operation, 'variables' => $params->variables, 'params' => $params, ]; - if ( ! $this->is_logging_enabled( $this->config, $params->query ) ) { - return; - } - $selected_events = $this->config[ BasicConfigurationTab::EVENT_LOG_SELECTION ] ?? []; - if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { - return; - } - if ( ! in_array( Events::BEFORE_GRAPHQL_EXECUTION, $selected_events, true ) ) { - return; - } $payload = EventManager::transform( Events::BEFORE_GRAPHQL_EXECUTION, [ 'context' => $context, @@ -154,16 +146,10 @@ public function log_before_response_returned( ?string $query_id ): void { try { - if ( ! $this->is_logging_enabled( $this->config, $query ) ) { - return; - } - $selected_events = $this->config[ BasicConfigurationTab::EVENT_LOG_SELECTION ] ?? []; - if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { - return; - } - if ( ! in_array( Events::BEFORE_RESPONSE_RETURNED, $selected_events, true ) ) { + if ( ! $this->should_log_event( Events::BEFORE_RESPONSE_RETURNED, $query ) ) { return; } + $encoded_request = wp_json_encode( $request ); $context = [ 'response' => $response, @@ -196,6 +182,25 @@ public function log_before_response_returned( } } + /** + * Determine if the event should be logged based on the configuration and selected events. + * + * @param string $event The event name. + * @param string|null $query The GraphQL query (optional). + * + * @return bool True if the event should be logged, false otherwise. + */ + public function should_log_event(string $event, ?string $query = null): bool { + if ( ! $this->is_logging_enabled( $this->config, $query ) ) { + return false; + } + $selected_events = $this->config[ BasicConfigurationTab::EVENT_LOG_SELECTION ] ?? []; + if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { + return false; + } + return in_array( $event, $selected_events, true ); + } + /** * Get the context for the response. * diff --git a/plugins/wpgraphql-logging/src/Logger/Rules/ExcludeQueryRule.php b/plugins/wpgraphql-logging/src/Logger/Rules/ExcludeQueryRule.php index 9a2bf6e0..e422d580 100644 --- a/plugins/wpgraphql-logging/src/Logger/Rules/ExcludeQueryRule.php +++ b/plugins/wpgraphql-logging/src/Logger/Rules/ExcludeQueryRule.php @@ -24,7 +24,7 @@ class ExcludeQueryRule implements LoggingRuleInterface { */ public function passes(array $config, ?string $query_string = null): bool { $queries = $config[ BasicConfigurationTab::EXCLUDE_QUERY ] ?? ''; - if ( null === $query_string || '' === trim( $queries ) ) { + if ( null === $query_string || ! is_string( $queries ) || '' === trim( $queries ) ) { return true; } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php new file mode 100644 index 00000000..1b8eca60 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php @@ -0,0 +1,465 @@ +repository = new LogsRepository(); + $this->logger = LoggerService::get_instance(); + } + + public function tearDown(): void { + parent::tearDown(); + $this->repository->delete_all(); + } + + public function create_instance(array $config) : QueryActionLogger { + return new QueryActionLogger($this->logger, $config); + } + + public function get_log_count(): int { + return $this->repository->get_log_count([]); + } + + public function assert_log_count(int $expected_count): void { + $actual_count = $this->get_log_count(); + $this->assertEquals($expected_count, $actual_count, "Expected log count to be {$expected_count}, but got {$actual_count}."); + } + + + /************************************************************** + * Pre request tests + **************************************************************/ + + public function test_pre_request_no_logging_as_config_disabled(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => false, + ] + ); + $instance->log_pre_request('{ test_query }', null, null); + $this->assert_log_count(0); + } + + public function test_pre_request_no_logging_as_event_none_selected(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => '', + ] + ); + $instance->log_pre_request('{ test_query }', null, null); + $this->assert_log_count(0); + } + + public function test_pre_request_no_logging_as_event_not_selected(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::BEFORE_GRAPHQL_EXECUTION] + ] + ); + $instance->log_pre_request('{ test_query }', null, null); + $this->assert_log_count(0); + } + + public function test_pre_request_no_logging_as_event_as_excluded(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::PRE_REQUEST], + BasicConfigurationTab::EXCLUDE_QUERY => 'test_query', + ] + ); + $instance->log_pre_request('{ test_query }', null, null); + $this->assert_log_count(0); + } + + public function test_pre_request_log_event(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::PRE_REQUEST], + ] + ); + $instance->log_pre_request('{ test_query }', null, null); + $this->assert_log_count(1); + } + + public function test_pre_request_add_context(): void { + + // Test subscribing a transform to add context + Plugin::transform(Events::PRE_REQUEST, function(array $payload): array { + $payload['context']['custom_key'] = 'custom_value'; + $payload['level'] = Level::Debug; + + return $payload; + } + ); + + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::PRE_REQUEST], + ] + ); + $instance->log_pre_request('{ test_query }', null, null); + $this->assert_log_count(1); + + // Check that the additional context is present in the log + $logs = $this->repository->get_logs([]); + $this->assertCount(1, $logs); + $log = $logs[0]; + $this->assertInstanceOf(DatabaseEntity::class, $log); + + + $this->assertEquals(Level::Debug->value, $log->get_level()); + $this->assertArrayHasKey('custom_key', $log->get_context()); + $this->assertEquals('custom_value', $log->get_context()['custom_key']); + } + + + /************************************************************** + * Before response returned tests + **************************************************************/ + + + public function make_query_graphql_before_execute(QueryActionLogger $instance, ?string $query = null, ?string $operation = null, ?array $variables = null): void { + $query = $query ?? '{ test_query }'; + $request = new Request(); + $reflection = new \ReflectionClass($request); + $property = $reflection->getProperty('params'); + $property->setAccessible(true); + + // Set up params object + $params = new \stdClass(); + $params->query = $query; + $params->operation = $operation; + $params->variables = $variables; + $property->setValue($request, $params); + $instance->log_graphql_before_execute($request); + } + + + public function test_graphql_before_execute_no_logging_as_config_not_enabled(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => false, + ] + ); + $this->make_query_graphql_before_execute($instance); + $this->assert_log_count(0); + } + + public function test_graphql_before_execute_no_logging_as_event_none_selected(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [], + ] + ); + $this->make_query_graphql_before_execute($instance); + $this->assert_log_count(0); + } + + + public function test_graphql_before_execute_no_logging_as_event_not_selected(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::PRE_REQUEST], + ] + ); + + $this->make_query_graphql_before_execute($instance); + $this->assert_log_count(0); + } + + + public function test_graphql_before_execute_no_logging_as_event_as_no_query(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::PRE_REQUEST, Events::BEFORE_GRAPHQL_EXECUTION], + ] + ); + $request = new Request(); + $instance->log_graphql_before_execute($request); + $this->assert_log_count(0); + } + + public function test_graphql_before_execute_log_event_add(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::PRE_REQUEST, Events::BEFORE_GRAPHQL_EXECUTION], + ] + ); + $params = [ + 'query' => '{ query GetPost($uri: ID!) { + post(id: $uri, idType: URI) { + title + content + } + } }', + 'variables' => ['var1' => 'value1'], + 'operation' => 'TestOperation', + ]; + $this->make_query_graphql_before_execute( + $instance, + $params['query'], + $params['operation'], + $params['variables'] + ); + $this->assert_log_count(1); + } + + public function test_graphql_before_execute_add_context(): void { + + + // Test subscribing a transform to add context + Plugin::transform(Events::BEFORE_GRAPHQL_EXECUTION, function(array $payload): array { + $payload['context']['custom_key'] = 'custom_value'; + $payload['level'] = Level::Error; + + return $payload; + } + ); + + + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::BEFORE_GRAPHQL_EXECUTION], + ] + ); + $params = [ + 'query' => '{ query GetPost($uri: ID!) { + post(id: $uri, idType: URI) { + title + content + } + } }', + 'variables' => ['var1' => 'value1'], + 'operation' => 'TestOperation', + ]; + $this->make_query_graphql_before_execute( + $instance, + $params['query'], + $params['operation'], + $params['variables'] + ); + $this->assert_log_count(1); + + // Check that the additional context is present in the log + $logs = $this->repository->get_logs([]); + $this->assertCount(1, $logs); + $log = $logs[0]; + $this->assertInstanceOf(DatabaseEntity::class, $log); + + + $this->assertEquals(Level::Error->value, $log->get_level()); + $this->assertArrayHasKey('custom_key', $log->get_context()); + $this->assertEquals('custom_value', $log->get_context()['custom_key']); + } + + /************************************************************** + * Log before response returned tests + **************************************************************/ + + public function test_log_before_response_returned_no_logging_as_config_not_enabled(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => false, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::BEFORE_RESPONSE_RETURNED], + ] + ); + + + $schema_config = SchemaConfig::create(); + $schema = new WPSchema($schema_config, WPGraphQL::get_type_registry()); + $query = '{ query GetPost($uri: ID!) { + post(id: $uri, idType: URI) { + title + content + } + } }'; + $variables = ['uri' => '/sample-post/']; + + $response = new ExecutionResult(['query' => '{ test_query }']); + $instance->log_before_response_returned($response, $response, $schema, null, $query, $variables, new Request(), null); + $this->assert_log_count(0); + } + + + public function test_log_before_response_returned_log_data(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::BEFORE_RESPONSE_RETURNED], + ] + ); + + $schema_config = SchemaConfig::create(); + $schema = new WPSchema($schema_config, WPGraphQL::get_type_registry()); + + $query = '{ query GetPost($uri: ID!) { + post(id: $uri, idType: URI) { + title + content + } + } }'; + $variables = ['uri' => '/sample-post/']; + + $response = new ExecutionResult(['query' => '{ test_query }']); + $instance->log_before_response_returned($response, $response, $schema, null, $query, $variables, new Request(), null); + $this->assert_log_count(1); + } + + public function test_log_before_response_returned_log_data_with_errors(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::BEFORE_RESPONSE_RETURNED], + ] + ); + + $schema_config = SchemaConfig::create(); + $schema = new WPSchema($schema_config, WPGraphQL::get_type_registry()); + + $query = '{ query GetPost($uri: ID!) { + post(id: $uri, idType: URI) { + title + content + } + } }'; + $variables = ['uri' => '/sample-post/']; + + $response = new ExecutionResult(['query' => '{ test_query }'], [new Error('Test error')]); + $instance->log_before_response_returned($response, $response, $schema, null, $query, $variables, new Request(), null); + $this->assert_log_count(1); + + + // Check for error level and context + $logs = $this->repository->get_logs([]); + $this->assertCount(1, $logs); + $log = $logs[0]; + $this->assertInstanceOf(DatabaseEntity::class, $log); + + + $this->assertEquals(Level::Error->value, $log->get_level()); + $this->assertArrayHasKey('errors', $log->get_context()); + } + + + public function test_log_before_response_returned_log_data_with_errors_array(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::BEFORE_RESPONSE_RETURNED], + ] + ); + + $schema_config = SchemaConfig::create(); + $schema = new WPSchema($schema_config, WPGraphQL::get_type_registry()); + + $query = '{ query GetPost($uri: ID!) { + post(id: $uri, idType: URI) { + title + content + } + } }'; + $variables = ['uri' => '/sample-post/']; + + $response = [ + 'query' => '{ test_query }', + 'errors' => [new Error('Test error')] + ]; + $instance->log_before_response_returned($response, $response, $schema, null, $query, $variables, new Request(), null); + $this->assert_log_count(1); + + + // Check for error level and context + $logs = $this->repository->get_logs([]); + $this->assertCount(1, $logs); + $log = $logs[0]; + $this->assertInstanceOf(DatabaseEntity::class, $log); + + + $this->assertEquals(Level::Error->value, $log->get_level()); + $this->assertArrayHasKey('errors', $log->get_context()); + } + + + public function test_log_before_response_returned_log_data_with_empty_errors_array(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::BEFORE_RESPONSE_RETURNED], + ] + ); + + $schema_config = SchemaConfig::create(); + $schema = new WPSchema($schema_config, WPGraphQL::get_type_registry()); + + $query = '{ query GetPost($uri: ID!) { + post(id: $uri, idType: URI) { + title + content + } + } }'; + $variables = ['uri' => '/sample-post/']; + + $response = [ + 'query' => '{ test_query }', + 'errors' => [] + ]; + $instance->log_before_response_returned($response, $response, $schema, null, $query, $variables, new Request(), null); + $this->assert_log_count(1); + + + // Check for error level and context + $logs = $this->repository->get_logs([]); + $this->assertCount(1, $logs); + $log = $logs[0]; + $this->assertInstanceOf(DatabaseEntity::class, $log); + + + $this->assertNotEquals(Level::Error->value, $log->get_level()); + $this->assertArrayNotHasKey('errors', $log->get_context()); + } + + + +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryEventLifecycleTest.php b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryEventLifecycleTest.php new file mode 100644 index 00000000..34d66185 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryEventLifecycleTest.php @@ -0,0 +1,48 @@ +getProperty('instance'); + $instanceProperty->setAccessible(true); + $instanceProperty->setValue(null, null); + + $instance1 = QueryEventLifecycle::init(); + $instance2 = QueryEventLifecycle::init(); + $this->assertSame($instance1, $instance2); + $this->assertInstanceOf(QueryEventLifecycle::class, $instance1); + $this->assertInstanceOf(QueryEventLifecycle::class, $instance2); + + + // Check that hooks are registered + $actionLoggerProp = $reflection->getProperty('action_logger'); + $actionLogger = $actionLoggerProp->getValue($instance1); + $this->assertTrue(has_action(Events::PRE_REQUEST, [ $actionLogger, 'log_pre_request' ]) !== false); + $this->assertTrue(has_action(Events::BEFORE_GRAPHQL_EXECUTION, [$actionLogger, 'log_graphql_before_execute']) !== false); + $this->assertTrue(has_action(Events::BEFORE_RESPONSE_RETURNED, [$actionLoggerProp->getValue($instance1), 'log_before_response_returned']) !== false); + + + $filterLoggerProp = $reflection->getProperty('filter_logger'); + $filterLogger = $filterLoggerProp->getValue($instance1); + $this->assertTrue(has_filter(Events::REQUEST_DATA, [$filterLogger, 'log_graphql_request_data']) !== false); + $this->assertTrue(has_filter(Events::REQUEST_RESULTS, [$filterLogger, 'log_graphql_request_results']) !== false); + $this->assertTrue(has_filter(Events::RESPONSE_HEADERS_TO_SEND, [$filterLogger, 'add_logging_headers']) !== false); + } +} diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php index 9b4b74a9..100df7d3 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Scheduler/DataDeletionSchedulerTest.php @@ -26,12 +26,10 @@ protected function setUp(): void { $this->repository = new LogsRepository(); $this->generate_logs(); $this->initial_log_count = $this->get_total_log_count(); - // wp_clear_scheduled_hook(DataDeletionScheduler::DELETION_HOOK); } protected function tearDown(): void { $this->delete_logs(); - // wp_clear_scheduled_hook(DataDeletionScheduler::DELETION_HOOK); parent::tearDown(); } From 2a773885c5f402b03e31d04b3461307e8c2f1b14 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 30 Sep 2025 16:12:24 +0100 Subject: [PATCH 14/18] Added fixes to EventFilterLogger and added tests. --- .../src/Events/QueryActionLogger.php | 30 -- .../src/Events/QueryFilterLogger.php | 44 +-- .../src/Logger/LoggingHelper.php | 30 ++ .../wpunit/Events/QueryActionLoggerTest.php | 3 +- .../wpunit/Events/QueryFilterLoggerTest.php | 263 ++++++++++++++++++ 5 files changed, 305 insertions(+), 65 deletions(-) create mode 100644 plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php diff --git a/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php b/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php index 289b8358..71f6c9e2 100644 --- a/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php +++ b/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php @@ -6,7 +6,6 @@ use GraphQL\Executor\ExecutionResult; use Monolog\Level; -use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; use WPGraphQL\Logging\Logger\LoggerService; use WPGraphQL\Logging\Logger\LoggingHelper; use WPGraphQL\Request; @@ -182,25 +181,6 @@ public function log_before_response_returned( } } - /** - * Determine if the event should be logged based on the configuration and selected events. - * - * @param string $event The event name. - * @param string|null $query The GraphQL query (optional). - * - * @return bool True if the event should be logged, false otherwise. - */ - public function should_log_event(string $event, ?string $query = null): bool { - if ( ! $this->is_logging_enabled( $this->config, $query ) ) { - return false; - } - $selected_events = $this->config[ BasicConfigurationTab::EVENT_LOG_SELECTION ] ?? []; - if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { - return false; - } - return in_array( $event, $selected_events, true ); - } - /** * Get the context for the response. * @@ -224,14 +204,4 @@ protected function get_response_errors( array|ExecutionResult $response ): ?arra return $errors; } - - /** - * Handles and logs application errors. - * - * @param string $event - * @param \Throwable $exception - */ - protected function process_application_error( string $event, \Throwable $exception ): void { - error_log( 'Error for WPGraphQL Logging - ' . $event . ': ' . $exception->getMessage() . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine() ); //phpcs:ignore - } } diff --git a/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php b/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php index 4a7d1891..b2ac6f13 100644 --- a/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php +++ b/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php @@ -6,7 +6,6 @@ use GraphQL\Executor\ExecutionResult; use Monolog\Level; -use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; use WPGraphQL\Logging\Logger\LoggerService; use WPGraphQL\Logging\Logger\LoggingHelper; use WPGraphQL\Logging\Logger\Rules\EnabledRule; @@ -63,16 +62,8 @@ public function __construct( LoggerService $logger, array $config ) { */ public function log_graphql_request_data( array $query_data ): array { try { - $selected_events = $this->config[ BasicConfigurationTab::EVENT_LOG_SELECTION ] ?? []; - if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { - return $query_data; - } - if ( ! in_array( Events::REQUEST_DATA, $selected_events, true ) ) { - return $query_data; - } - $query_string = $query_data['query'] ?? null; - if ( ! $this->is_logging_enabled( $this->config, $query_string ) ) { + if ( ! $this->should_log_event( Events::REQUEST_DATA, $query_string ) ) { return $query_data; } @@ -82,8 +73,11 @@ public function log_graphql_request_data( array $query_data ): array { 'operation_name' => $query_data['operationName'] ?? null, ]; - $payload = EventManager::transform( Events::REQUEST_DATA, [ 'context' => $context ] ); - $this->logger->log( Level::Info, 'WPGraphQL Request Data', $payload['context'] ); + $payload = EventManager::transform( Events::REQUEST_DATA, [ + 'context' => $context, + 'level' => Level::Info, + ] ); + $this->logger->log( $payload['level'], 'WPGraphQL Request Data', $payload['context'] ); EventManager::publish( Events::REQUEST_DATA, [ 'context' => $payload['context'] ] ); } catch ( \Throwable $e ) { $this->process_application_error( Events::REQUEST_DATA, $e ); @@ -117,15 +111,7 @@ public function log_graphql_request_results( ?string $query_id ): array|ExecutionResult { try { - if ( ! $this->is_logging_enabled( $this->config, $query ) ) { - return $response; - } - - $selected_events = $this->config[ BasicConfigurationTab::EVENT_LOG_SELECTION ] ?? []; - if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { - return $response; - } - if ( ! in_array( Events::REQUEST_RESULTS, $selected_events, true ) ) { + if ( ! $this->should_log_event( Events::REQUEST_RESULTS, $query ) ) { return $response; } @@ -134,9 +120,9 @@ public function log_graphql_request_results( $encoded_request = wp_json_encode( $request ); $context = [ 'response' => $response, - 'operation_name' => $params->operation, - 'query' => $params->query, - 'variables' => $params->variables, + 'operation_name' => $params->operation ?? null, + 'query' => $params->query ?? null, + 'variables' => $params->variables ?? null, 'request' => false !== $encoded_request ? json_decode( $encoded_request, true ) : null, 'query_id' => $query_id, ]; @@ -185,14 +171,4 @@ public function add_logging_headers( array $headers ): array { return $headers; } - - /** - * Handles and logs application errors. - * - * @param string $event The name of the event where the error occurred. - * @param \Throwable $exception The exception that was caught. - */ - protected function process_application_error( string $event, \Throwable $exception ): void { - error_log( 'Error for WPGraphQL Logging - ' . $event . ': ' . $exception->getMessage() . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine() ); //phpcs:ignore - } } diff --git a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php index 43734b43..c0c11069 100644 --- a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php +++ b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php @@ -4,6 +4,7 @@ namespace WPGraphQL\Logging\Logger; +use WPGraphQL\Logging\Admin\Settings\Fields\Tab\BasicConfigurationTab; use WPGraphQL\Logging\Logger\Rules\AdminUserRule; use WPGraphQL\Logging\Logger\Rules\EnabledRule; use WPGraphQL\Logging\Logger\Rules\ExcludeQueryRule; @@ -40,6 +41,25 @@ public function should_log_response(array $config): bool { return $rule->passes( $config ); } + /** + * Determine if the event should be logged based on the configuration and selected events. + * + * @param string $event The event name. + * @param string|null $query The GraphQL query (optional). + * + * @return bool True if the event should be logged, false otherwise. + */ + public function should_log_event(string $event, ?string $query = null): bool { + if ( ! $this->is_logging_enabled( $this->config, $query ) ) { + return false; + } + $selected_events = $this->config[ BasicConfigurationTab::EVENT_LOG_SELECTION ] ?? []; + if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { + return false; + } + return in_array( $event, $selected_events, true ); + } + /** * Get the rule manager, initializing it if necessary. */ @@ -75,4 +95,14 @@ protected function is_logging_enabled( array $config, ?string $query_string = nu */ return apply_filters( 'wpgraphql_logging_is_enabled', $is_enabled, $config ); } + + /** + * Handles and logs application errors. + * + * @param string $event + * @param \Throwable $exception + */ + protected function process_application_error( string $event, \Throwable $exception ): void { + error_log( 'Error for WPGraphQL Logging - ' . $event . ': ' . $exception->getMessage() . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine() ); //phpcs:ignore + } } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php index 1b8eca60..c38dcdfa 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryActionLoggerTest.php @@ -21,7 +21,7 @@ use WPGraphQL\Request; /** - * Class QueryActionLoggerTest + * Tests for the QueryActionLogger class. * * @package WPGraphQL\Logging\Tests\Events * @@ -419,6 +419,7 @@ public function test_log_before_response_returned_log_data_with_errors_array(): $this->assertEquals(Level::Error->value, $log->get_level()); $this->assertArrayHasKey('errors', $log->get_context()); + $this->assertEquals('WPGraphQL Response with Errors', $log->get_message()); } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php new file mode 100644 index 00000000..4e6c83a9 --- /dev/null +++ b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php @@ -0,0 +1,263 @@ +repository = new LogsRepository(); + $this->logger = LoggerService::get_instance(); + } + + public function tearDown(): void { + parent::tearDown(); + $this->repository->delete_all(); + } + + public function create_instance(array $config) : QueryFilterLogger { + return new QueryFilterLogger($this->logger, $config); + } + + public function get_log_count(): int { + return $this->repository->get_log_count([]); + } + + public function assert_log_count(int $expected_count): void { + $actual_count = $this->get_log_count(); + $this->assertEquals($expected_count, $actual_count, "Expected log count to be {$expected_count}, but got {$actual_count}."); + } + + /************************************************************** + * graphql_request_data + **************************************************************/ + + public function test_graphql_request_data_no_logging_as_config_disabled(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => false, + ] + ); + $instance->log_graphql_request_data(['query' => '{ testQuery }']); + $this->assert_log_count(0); + } + + public function test_graphql_request_data_no_logging_as_not_selected(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::PRE_REQUEST], + ] + ); + $instance->log_graphql_request_data(['query' => '{ testQuery }']); + $this->assert_log_count(0); + } + + public function test_graphql_request_data_log_event(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::REQUEST_DATA], + ] + ); + $instance->log_graphql_request_data(['query' => '{ testQuery }']); + $this->assert_log_count(1); + } + + + public function test_graphql_request_data_log_event_with_context_data_from_subscriber(): void { + + // Add a subscriber to modify the context data and log level. + Plugin::transform(Events::REQUEST_DATA, function(array $payload): array { + $payload['context']['meta_data'] = 'This is meta value.'; + $payload['level'] = Level::Critical; + + return $payload; + } + ); + + + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::REQUEST_DATA], + ] + ); + $instance->log_graphql_request_data(['query' => '{ testQuery }']); + $this->assert_log_count(1); + + // Check for the new meta_data field in the log entry. + $logs = $this->repository->get_logs([], 10, 0); + $this->assertNotEmpty($logs); + $log_entry = $logs[0]; + $this->assertArrayHasKey('meta_data', $log_entry->get_context()); + $this->assertEquals('This is meta value.', $log_entry->get_context()['meta_data']); + $this->assertEquals(Level::Critical->value, $log_entry->get_level()); + } + + + /************************************************************** + * graphql_request_results + **************************************************************/ + + + public function test_graphql_request_results_no_logging_as_not_enabled(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => false, + ] + ); + + + $schema_config = SchemaConfig::create(); + $schema = new WPSchema($schema_config, WPGraphQL::get_type_registry()); + $query = '{ query GetPost($uri: ID!) { + post(id: $uri, idType: URI) { + title + content + } + } }'; + $variables = ['uri' => '/sample-post/']; + + $response = new ExecutionResult(['query' => '{ test_query }']); + $instance->log_graphql_request_results($response, $schema, null, $query, $variables, new Request(), null); + $this->assert_log_count(0); + } + + + public function test_graphql_request_results_log_event(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::REQUEST_RESULTS], + ] + ); + + $schema_config = SchemaConfig::create(); + $schema = new WPSchema($schema_config, WPGraphQL::get_type_registry()); + $query = '{ query GetPost($uri: ID!) { + post(id: $uri, idType: URI) { + title + content + } + } }'; + $variables = ['uri' => '/sample-post/']; + + $response = new ExecutionResult(['query' => '{ test_query }']); + $instance->log_graphql_request_results($response, $schema, 'test_operation', $query, $variables, new Request(), null); + $this->assert_log_count(1); + } + + public function test_graphql_request_results_log_data_with_errors_array(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::REQUEST_RESULTS], + ] + ); + + $schema_config = SchemaConfig::create(); + $schema = new WPSchema($schema_config, WPGraphQL::get_type_registry()); + + $query = '{ query GetPost($uri: ID!) { + post(id: $uri, idType: URI) { + title + content + } + } }'; + $variables = ['uri' => '/sample-post/']; + + $response = [ + 'query' => '{ test_query }', + 'errors' => [new Error('Test error')] + ]; + $instance->log_graphql_request_results($response, $schema, 'test_operation', $query, $variables, new Request(), null); + $this->assert_log_count(1); + + + // Check for error level and context + $logs = $this->repository->get_logs([]); + $this->assertCount(1, $logs); + $log = $logs[0]; + $this->assertInstanceOf(DatabaseEntity::class, $log); + + $this->assertEquals(Level::Error->value, $log->get_level()); + $this->assertArrayHasKey('errors', $log->get_context()); + $this->assertEquals('WPGraphQL Response with Errors', $log->get_message()); + } + + + public function test_graphql_request_results_log_event_add_context_with_subscriber(): void { + + // Add a subscriber to modify the context data and log level. + Plugin::transform(Events::REQUEST_RESULTS, function(array $payload): array { + $payload['context']['meta_data'] = 'This is meta value.'; + $payload['level'] = Level::Debug; + + return $payload; + } + ); + + + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::REQUEST_RESULTS], + ] + ); + + $schema_config = SchemaConfig::create(); + $schema = new WPSchema($schema_config, WPGraphQL::get_type_registry()); + $query = '{ query GetPost($uri: ID!) { + post(id: $uri, idType: URI) { + title + content + } + } }'; + $variables = ['uri' => '/sample-post/']; + + $response = new ExecutionResult(['query' => '{ test_query }']); + $instance->log_graphql_request_results($response, $schema, 'test_operation', $query, $variables, new Request(), null); + $this->assert_log_count(1); + + + // Check for the new meta_data field in the log entry. + $logs = $this->repository->get_logs([], 10, 0); + $this->assertNotEmpty($logs); + $log_entry = $logs[0]; + $this->assertArrayHasKey('meta_data', $log_entry->get_context()); + $this->assertEquals('This is meta value.', $log_entry->get_context()['meta_data']); + $this->assertEquals(Level::Debug->value, $log_entry->get_level()); + } +} From 0f1986c3c948c13eaa6f6c5b82cf0c61b308ffdd Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 30 Sep 2025 16:22:55 +0100 Subject: [PATCH 15/18] Add event selection check for response headers logging Updated QueryFilterLogger to only add logging headers if the RESPONSE_HEADERS_TO_SEND event is selected in the configuration. Added is_selected_event helper and corresponding unit tests to ensure correct behavior based on configuration. --- .../src/Events/QueryFilterLogger.php | 4 ++ .../src/Logger/LoggingHelper.php | 11 +++++ .../wpunit/Events/QueryFilterLoggerTest.php | 43 +++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php b/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php index b2ac6f13..dd2669fe 100644 --- a/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php +++ b/plugins/wpgraphql-logging/src/Events/QueryFilterLogger.php @@ -166,6 +166,10 @@ public function add_logging_headers( array $headers ): array { return $headers; } + if ( ! $this->is_selected_event( Events::RESPONSE_HEADERS_TO_SEND ) ) { + return $headers; + } + $request_id = uniqid( 'wpgql_log_' ); $headers['X-WPGraphQL-Logging-ID'] = $request_id; diff --git a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php index c0c11069..ca67da78 100644 --- a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php +++ b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php @@ -53,6 +53,17 @@ public function should_log_event(string $event, ?string $query = null): bool { if ( ! $this->is_logging_enabled( $this->config, $query ) ) { return false; } + return $this->is_selected_event( $event ); + } + + /** + * Check if the event is selected in the configuration. + * + * @param string $event The event name to check. + * + * @return bool True if the event is selected, false otherwise. + */ + public function is_selected_event(string $event): bool { $selected_events = $this->config[ BasicConfigurationTab::EVENT_LOG_SELECTION ] ?? []; if ( ! is_array( $selected_events ) || empty( $selected_events ) ) { return false; diff --git a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php index 4e6c83a9..af117a7f 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Events/QueryFilterLoggerTest.php @@ -260,4 +260,47 @@ public function test_graphql_request_results_log_event_add_context_with_subscrib $this->assertEquals('This is meta value.', $log_entry->get_context()['meta_data']); $this->assertEquals(Level::Debug->value, $log_entry->get_level()); } + + + /************************************************************** + * add_response_headers + **************************************************************/ + public function test_graphql_response_headers_logging_disabled(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => false, + ] + ); + $headers = ['Content-Type' => 'application/json']; + $result = $instance->add_logging_headers($headers); + $this->assertSame($headers, $result); + } + + public function test_graphql_response_headers_logging_not_selected(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::PRE_REQUEST], + ] + ); + $headers = ['Content-Type' => 'application/json']; + $result = $instance->add_logging_headers($headers); + $this->assertSame($headers, $result); + } + + + public function test_graphql_response_headers_logging_add_header(): void { + $instance = $this->create_instance( + [ + BasicConfigurationTab::ENABLED => true, + BasicConfigurationTab::EVENT_LOG_SELECTION => [Events::RESPONSE_HEADERS_TO_SEND], + ] + ); + $headers = ['Content-Type' => 'application/json']; + $result = $instance->add_logging_headers($headers); + $this->assertNotSame($headers, $result); + $this->assertArrayHasKey('X-WPGraphQL-Logging-ID', $result); + $this->assertNotEmpty($result['X-WPGraphQL-Logging-ID']); + $this->assertCount(2, $result); + } } From 6a199f8a903c30fc1bd980b0bfc1253e8161cdb7 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 30 Sep 2025 17:41:31 +0100 Subject: [PATCH 16/18] Added missing tests to the plugin manager. --- plugins/wpgraphql-logging/src/Plugin.php | 6 +- .../tests/wpunit/Core/PluginTest.php | 93 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/plugins/wpgraphql-logging/src/Plugin.php b/plugins/wpgraphql-logging/src/Plugin.php index ab86a0b0..732d83ff 100644 --- a/plugins/wpgraphql-logging/src/Plugin.php +++ b/plugins/wpgraphql-logging/src/Plugin.php @@ -47,7 +47,7 @@ public static function init(): self { * * @param \WPGraphQL\Logging\Plugin $instance the instance of the plugin class. */ - do_action( 'wpgraphql_logging_init', self::$instance ); + do_action( 'wpgraphql_logging_plugin_init', self::$instance ); return self::$instance; } @@ -56,13 +56,13 @@ public static function init(): self { * Initialize the plugin admin, frontend & api functionality. */ public function setup(): void { - // Initialize configuration caching hooks. ConfigurationHelper::init_cache_hooks(); - SettingsPage::init(); ViewLogsPage::init(); QueryEventLifecycle::init(); DataDeletionScheduler::init(); + + do_action( 'wpgraphql_logging_plugin_setup', self::$instance ); } /** diff --git a/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php b/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php index d288f9cc..cdbbc264 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Core/PluginTest.php @@ -8,6 +8,7 @@ use lucatume\WPBrowser\TestCase\WPTestCase; use ReflectionClass; use WPGraphQL\Logging\Logger\Database\DatabaseEntity; +use WPGraphQL\Logging\Events\EventManager; /** @@ -54,4 +55,96 @@ public function test_clone_method_throws_error() { // Verify the clone exists to ensure the operation completed $this->assertInstanceOf( Plugin::class, $clone ); } + + public function test_wakeup_method_throws_error() { + $reflection = new ReflectionClass( Plugin::class ); + $plugin = $reflection->newInstanceWithoutConstructor(); + + $this->setExpectedIncorrectUsage( 'WPGraphQL\Logging\Plugin::__wakeup' ); + $plugin->__wakeup(); + + $this->assertInstanceOf( Plugin::class, $plugin ); + } + + public function test_clone_is_forbidden_and_triggers_doing_it_wrong() { + $reflection = new ReflectionClass( Plugin::class ); + $plugin = $reflection->newInstanceWithoutConstructor(); + + $this->setExpectedIncorrectUsage( 'WPGraphQL\Logging\Plugin::__clone' ); + $cloned = null; + try { + $cloned = clone $plugin; + } catch ( \Exception $e ) { + // Ignore, as __clone should not throw, just trigger doing_it_wrong. + } + $this->assertInstanceOf( Plugin::class, $plugin ); + } + + public function test_wakeup_is_forbidden_and_triggers_doing_it_wrong() { + $reflection = new ReflectionClass( Plugin::class ); + $plugin = $reflection->newInstanceWithoutConstructor(); + + $this->setExpectedIncorrectUsage( 'WPGraphQL\Logging\Plugin::__wakeup' ); + $plugin->__wakeup(); + + $this->assertInstanceOf( Plugin::class, $plugin ); + } + + + public function test_can_subscribe_and_emit_custom_event() { + $event_name = 'custom_event_test_' . uniqid(); + $received_payload = null; + + // Subscribe to the custom event. + Plugin::on( $event_name, function( $payload ) use ( &$received_payload ) { + $received_payload = $payload; + } ); + + $payload = [ 'foo' => 'bar', 'baz' => 123 ]; + + // Emit the event. + Plugin::emit( $event_name, $payload ); + + // The listener should have received the payload. + $this->assertSame( $payload, $received_payload ); + } + + public function test_transform_event_payload() { + $event_name = 'transform_event_test_' . uniqid(); + $received_payload = null; + + // Register an event listener. + Plugin::on( $event_name, function( $payload ) use ( &$received_payload ) { + $received_payload = $payload; + } ); + + // Subscribe to transform the payload. + Plugin::transform( $event_name, function( $payload ) { + $payload['context']['error'] = true; + return $payload; + }, 5 ); + + + // Simulate emitting the event with initial payload. + $level = 200; + $context = [ + 'query' => 'query { test }', + 'variables' => [ 'var1' => 'value' ], + 'operation_name' => 'TestOperation', + ]; + $payload = EventManager::transform( $event_name, [ + 'context' => $context, + 'level' => $level, + ] ); + + // Publish the event. + Plugin::emit( $event_name, $payload ); + + + // Check the listener received the transformed payload. + $this->assertSame( $received_payload, + array_merge( $payload, [ 'context' => array_merge( $context, [ 'error' => true ] ) ] ), + 'The listener should receive the transformed payload' + ); + } } From 8e8e204d54ac1dd6717c14c6edf537e0b1bd4353 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Tue, 30 Sep 2025 18:53:03 +0100 Subject: [PATCH 17/18] Fix for PHP 8.3 for sampling rate --- .../src/Logger/Rules/SamplingRateRule.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/wpgraphql-logging/src/Logger/Rules/SamplingRateRule.php b/plugins/wpgraphql-logging/src/Logger/Rules/SamplingRateRule.php index 0358469e..91c1954d 100644 --- a/plugins/wpgraphql-logging/src/Logger/Rules/SamplingRateRule.php +++ b/plugins/wpgraphql-logging/src/Logger/Rules/SamplingRateRule.php @@ -24,7 +24,12 @@ class SamplingRateRule implements LoggingRuleInterface { */ public function passes(array $config, ?string $query_string = null): bool { $sampling_rate = (int) ( $config[ BasicConfigurationTab::DATA_SAMPLING ] ?? 100 ); - $rand = wp_rand( 0, 100 ); + // If sampling rate is 0, never log. + if ( 1 > $sampling_rate ) { + return false; + } + + $rand = wp_rand( 1, 100 ); return $rand <= $sampling_rate; } From ef373fa9710dd05bde9c33b7eb758f73686f0bd3 Mon Sep 17 00:00:00 2001 From: Colin Murphy Date: Wed, 1 Oct 2025 09:46:31 +0100 Subject: [PATCH 18/18] Added Changeset --- .changeset/eight-moons-help.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eight-moons-help.md diff --git a/.changeset/eight-moons-help.md b/.changeset/eight-moons-help.md new file mode 100644 index 00000000..62cf9cf6 --- /dev/null +++ b/.changeset/eight-moons-help.md @@ -0,0 +1,5 @@ +--- +"@wpengine/wpgraphql-logging-wordpress-plugin": patch +--- + +chore: Fixed some snags for events and event manager. Added and updated PHPUnit tests and coverage now above 90%.