From ac8ebb333f8d5227e059916e20c4ecb313b1497b Mon Sep 17 00:00:00 2001 From: Shadi Sharaf Date: Sat, 5 Sep 2026 04:16:19 +0300 Subject: [PATCH] Extract Alerts admin UI and trigger engine into collaborators so forms, AJAX, and record matching can be unit-tested without booting WordPress. --- classes/class-alerts-admin-ui.php | 579 +++++++++++++++++ classes/class-alerts-list.php | 2 +- classes/class-alerts-trigger-engine.php | 98 +++ classes/class-alerts.php | 613 +----------------- tests/phpunit/Alerts_Test.php | 91 ++- .../unit/Alerts_Admin_UI_Unit_Test.php | 313 +++++++++ .../unit/Alerts_Trigger_Engine_Unit_Test.php | 226 +++++++ 7 files changed, 1280 insertions(+), 642 deletions(-) create mode 100644 classes/class-alerts-admin-ui.php create mode 100644 classes/class-alerts-trigger-engine.php create mode 100644 tests/phpunit/unit/Alerts_Admin_UI_Unit_Test.php create mode 100644 tests/phpunit/unit/Alerts_Trigger_Engine_Unit_Test.php diff --git a/classes/class-alerts-admin-ui.php b/classes/class-alerts-admin-ui.php new file mode 100644 index 000000000..d01075424 --- /dev/null +++ b/classes/class-alerts-admin-ui.php @@ -0,0 +1,579 @@ +register_hooks(); + } + + /** + * Register menu, script, AJAX, and action-link hooks. + * + * @return void + */ + public function register_hooks() { + // Add custom post type to menu. + add_action( 'wp_stream_admin_menu', array( $this, 'register_menu' ) ); + + // Add scripts to post screens. + add_action( + 'admin_enqueue_scripts', + array( + $this, + 'register_scripts', + ) + ); + + add_action( + 'network_admin_menu', + array( + $this, + 'change_menu_link_url', + ), + 99 + ); + + add_action( + 'wp_ajax_load_alerts_settings', + array( + $this, + 'load_alerts_settings', + ) + ); + add_action( 'wp_ajax_get_actions', array( $this, 'get_actions' ) ); + add_action( + 'wp_ajax_save_new_alert', + array( + $this, + 'save_new_alert', + ) + ); + add_action( + 'wp_ajax_get_new_alert_triggers_notifications', + array( + $this, + 'get_new_alert_triggers_notifications', + ) + ); + + add_filter( + 'wp_stream_action_links_posts', + array( + $this, + 'change_alert_action_links', + ), + 11, + 2 + ); + } + + /** + * Register scripts for page load + * + * @action admin_enqueue_scripts + * + * @return void + */ + public function register_scripts() { + $screen = get_current_screen(); + if ( 'edit-wp_stream_alerts' !== $screen->id ) { + return; + } + + $this->plugin->enqueue_asset( + 'alerts', + array( + $this->plugin->with_select2(), + 'inline-edit-post', + ), + array( + 'any' => __( 'Any', 'stream' ), + 'anyContext' => __( 'Any Context', 'stream' ), + 'getActionsNonce' => wp_create_nonce( 'stream_get_actions' ), + ) + ); + } + + /** + * Add custom post type to menu + * + * @action admin_menu + * + * @return void + */ + public function register_menu() { + add_submenu_page( + $this->plugin->admin->records_page_slug, + __( 'Alerts', 'stream' ), + __( 'Alerts', 'stream' ), + Alerts::CAPABILITY, + 'edit.php?post_type=wp_stream_alerts' + ); + } + + /** + * Modify the Stream > Alerts Network Admin Menu link. + * + * In self::register_menu(), the Alerts submenu item + * is essentially set to go to the Site's admin area. + * + * However, on the Network admin, we need to redirect + * it to the first site in the network, as this is + * where the true Network Alerts settings page is located. + * + * @action network_admin_menu + * @return bool + */ + public function change_menu_link_url() { + global $submenu; + + $parent = 'wp_stream'; + $page = 'edit.php?post_type=wp_stream_alerts'; + + // If we're not on the Stream menu item, return. + if ( ! isset( $submenu[ $parent ] ) ) { + return false; + } + + // Get the first existing Site in the Network. + $sites = wp_stream_get_sites( + array( + 'limit' => 5, // Limit the size of the query. + ) + ); + + $site_id = '1'; + + // Function wp_get_sites() can return an empty array if the network is too large. + if ( ! empty( $sites ) && ! empty( $sites[0]->blog_id ) ) { + $site_id = $sites[0]->blog_id; + } + + $new_url = get_admin_url( $site_id, $page ); + + foreach ( $submenu[ $parent ] as $key => $value ) { + // Set correct URL for the menu item. + if ( $page === $value[2] ) { + // This hack is not kosher, see the docblock for an explanation. + $submenu[ $parent ][ $key ][2] = $new_url; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + break; + } + } + + return true; + } + + /** + * Display Alert Type Meta Box + * + * @param \WP_Post|array $post Post object for current alert. + * + * @return void + */ + public function display_notification_box( $post = array() ) { + $alert = null; + $alert_type = 'none'; + if ( is_object( $post ) ) { + $alert = $this->plugin->alerts->get_alert( $post->ID ); + if ( false !== $alert ) { + $alert_type = $alert->alert_type; + } + } + $form = new Form_Generator(); + + echo ''; + $form->render_field( + 'select', + array( + 'id' => 'wp_stream_alert_type', + 'name' => 'wp_stream_alert_type', + 'value' => $alert_type, + 'options' => $this->get_notification_values(), + 'placeholder' => __( 'No Alert', 'stream' ), + 'title' => 'Alert Type:', + ) + ); + + echo '
'; + if ( is_object( $alert ) ) { + $alert->get_alert_type_obj()->display_fields( $alert ); + } else { + $this->plugin->alerts->alert_types['none']->display_fields( array() ); + } + + echo '
'; + } + + /** + * Returns settings form HTML for AJAX use + * + * @action wp_ajax_load_alerts_settings + * + * @return void + */ + public function load_alerts_settings() { + if ( ! current_user_can( Alerts::CAPABILITY ) ) { + wp_send_json_error( + array( + 'message' => 'You do not have permission to do this.', + ) + ); + } + $alert = array(); + $post_id = wp_stream_filter_input( INPUT_POST, 'post_id' ); + if ( ! empty( $post_id ) && 'new' !== $post_id ) { + $alert = $this->plugin->alerts->get_alert( $post_id ); + if ( false === $alert ) { + wp_send_json_error( + array( + 'message' => 'Could not find alert.', + ) + ); + } + } + + $alert_type = wp_stream_filter_input( INPUT_POST, 'alert_type' ); + if ( empty( $alert_type ) ) { + $alert_type = 'none'; + } + if ( ! array_key_exists( $alert_type, $this->plugin->alerts->alert_types ) ) { + wp_send_json_error( + array( + 'message' => 'Could not find alert type.', + ) + ); + } + + ob_start(); + $this->plugin->alerts->alert_types[ $alert_type ]->display_fields( $alert ); + $output = ob_get_contents(); + ob_end_clean(); + + $data = array( + 'html' => $output, + ); + wp_send_json_success( $data ); + } + + /** + * Display Trigger Meta Box + * + * @param \WP_Post|array $post Post object for current alert. + * + * @return void + */ + public function display_triggers_box( $post = array() ) { + $alert = false; + if ( is_object( $post ) ) { + $alert = $this->plugin->alerts->get_alert( $post->ID ); + } + if ( false === $alert ) { + $alert = array(); + } + + $form = new Form_Generator(); + do_action( 'wp_stream_alert_trigger_form_display', $form, $alert ); + // @TODO use human readable text. + echo ''; + $form->render_fields(); + wp_nonce_field( 'save_alert', 'wp_stream_alerts_nonce' ); + + if ( $post instanceof \WP_Post ) : + /** + * These fields are required for the post to be saved, as the Admin AJAX inline_save action is fired. + * + * @see get_inline_data() + * @see wp_ajax_inline_save() + */ + ?> + + + + + + + + post_status; + if ( 'auto-draft' === $post_status ) { + $post_status = 'wp_stream_enabled'; + } + ?> +
+
+
+
+ + +
+
+
+
+ +
+
+ ID ) ) { + if ( ! EMPTY_TRASH_DAYS ) { + $delete_text = __( 'Delete Permanently', 'stream' ); + } else { + $delete_text = __( 'Move to Trash', 'stream' ); + } + ?> + + + + +
+
+ + +
+
+
+
+ +
+
+
+ +
+
+
+
+ plugin->alerts->alert_types, 'name', 'slug' ); + foreach ( $names as $slug => $name ) { + $result[ $slug ] = $name; + } + + return $result; + } + + /** + * Update actions dropdown options based on the connector selected. + */ + public function get_actions() { + if ( ! current_user_can( Alerts::CAPABILITY ) ) { + wp_send_json_error( + array( + 'message' => 'You do not have permission to do this.', + ) + ); + } + + check_ajax_referer( 'stream_get_actions', 'nonce' ); + + $connector_name = wp_stream_filter_input( INPUT_POST, 'connector' ); + $stream_connectors = wp_stream_get_instance()->connectors; + if ( ! empty( $connector_name ) ) { + if ( isset( $stream_connectors->connectors[ $connector_name ] ) ) { + $connector = $stream_connectors->connectors[ $connector_name ]; + if ( method_exists( $connector, 'get_action_labels' ) ) { + $actions = $connector->get_action_labels(); + } + } + } else { + $actions = $stream_connectors->term_labels['stream_action']; + } + ksort( $actions ); + wp_send_json_success( $actions ); + } + + /** + * Save a new alert + */ + public function save_new_alert() { + check_ajax_referer( 'save_alert', 'wp_stream_alerts_nonce' ); + + if ( ! current_user_can( $this->plugin->admin->settings_cap ) ) { + wp_die( + esc_html__( "You don't have sufficient privileges to do this action.", 'stream' ) + ); + } + + $trigger_author = wp_stream_filter_input( INPUT_POST, 'wp_stream_trigger_author' ); + $trigger_connector_and_context = wp_stream_filter_input( INPUT_POST, 'wp_stream_trigger_context' ); + if ( false !== strpos( $trigger_connector_and_context, '-' ) ) { + // This is a connector with a context such as posts-post. + $trigger_connector_and_context_split = explode( '-', $trigger_connector_and_context ); + $trigger_connector = $trigger_connector_and_context_split[0]; + $trigger_context = $trigger_connector_and_context_split[1]; + } elseif ( ! empty( $trigger_connector_and_context ) ) { + // This is a parent connector with no dash such as posts. + $trigger_connector = $trigger_connector_and_context; + $trigger_context = ''; + } else { + // There is no connector or context. + $trigger_connector = ''; + $trigger_context = ''; + } + + $trigger_action = wp_stream_filter_input( INPUT_POST, 'wp_stream_trigger_action' ); + $alert_type = wp_stream_filter_input( INPUT_POST, 'wp_stream_alert_type' ); + $alert_status = wp_stream_filter_input( INPUT_POST, 'wp_stream_alert_status' ); + + // Insert the post into the database. + $item = (object) array( + 'alert_type' => $alert_type, + 'alert_meta' => array( + 'trigger_author' => $trigger_author, + 'trigger_connector' => $trigger_connector, + 'trigger_action' => $trigger_action, + 'trigger_context' => $trigger_context, + ), + 'alert_status' => $alert_status, + ); + $alert = new Alert( $item, $this->plugin ); + $title = $alert->get_title(); + $post_id = wp_insert_post( + array( + 'post_status' => $alert_status, + 'post_type' => 'wp_stream_alerts', + 'post_title' => $title, + ) + ); + if ( empty( $post_id ) ) { + wp_send_json_error(); + } + add_post_meta( $post_id, 'alert_type', $alert_type ); + + $alert_meta = array( + 'trigger_author' => $trigger_author, + 'trigger_connector' => $trigger_connector, + 'trigger_action' => $trigger_action, + 'trigger_context' => $trigger_context, + ); + $alert_meta = apply_filters( 'wp_stream_alerts_save_meta', $alert_meta, $alert_type ); + add_post_meta( $post_id, 'alert_meta', $alert_meta ); + wp_send_json_success( + array( + 'success' => true, + ) + ); + } + + /** + * Return HTML string of the Alert page controls. + */ + public function get_new_alert_triggers_notifications() { + if ( ! current_user_can( $this->plugin->admin->settings_cap ) ) { + wp_die( + esc_html__( "You don't have sufficient privileges to do this action.", 'stream' ) + ); + } + + ob_start(); + ?> +
+ Add New + display_triggers_box(); ?> +
+
+ display_notification_box(); ?> +
+
+ display_status_box(); ?> +
+ true, + 'html' => $html, + ) + ); + } + + /** + * Add action links to Stream drop row in admin list screen + * + * @filter wp_stream_action_links_{connector} + * + * @param array $links Previous links registered. + * @param Record $record Stream record. + * + * @return array Action links + */ + public function change_alert_action_links( $links, $record ) { + $post = get_post( $record->object_id ); + + if ( $post && Alerts::POST_TYPE === $post->post_type && $post->post_status === $record->get_meta( 'new_status', true ) ) { + if ( 'trash' !== $post->post_status ) { + $connector_posts = new \WP_Stream\Connector_Posts(); + $post_type_name = $connector_posts->get_post_type_name( get_post_type( $post->ID ) ); + + /* translators: %s: the post type singular name (e.g. "Post") */ + $links[ sprintf( esc_html_x( 'Edit %s', 'Post type singular name', 'stream' ), $post_type_name ) ] = admin_url( 'edit.php?post_type=wp_stream_alerts#post-' . $post->ID ); + unset( $links[ esc_html__( 'View', 'stream' ) ] ); + } + } + + return $links; + } +} diff --git a/classes/class-alerts-list.php b/classes/class-alerts-list.php index e5a60a317..753e1ad22 100644 --- a/classes/class-alerts-list.php +++ b/classes/class-alerts-list.php @@ -312,7 +312,7 @@ public function display_custom_quick_edit() { plugin->alerts, $function_name ), $the_post ); + call_user_func( array( $this->plugin->alerts->admin_ui, $function_name ), $the_post ); ?> register_hooks(); + } + + /** + * Register record-insert matching hooks. + * + * @return void + */ + public function register_hooks() { + add_filter( + 'wp_stream_record_inserted', + array( + $this, + 'check_records', + ), + 10, + 2 + ); + } + + /** + * Checks record being processed against active alerts. + * + * @filter wp_stream_record_inserted + * + * @param int $record_id The record being processed. + * @param array $recordarr Record data. + * + * @return array + */ + public function check_records( $record_id, $recordarr ) { + $args = array( + 'post_type' => Alerts::POST_TYPE, + 'post_status' => 'wp_stream_enabled', + ); + + $alerts = new \WP_Query( $args ); + $hydrated = array(); + foreach ( $alerts->posts as $alert ) { + $hydrated[] = $this->plugin->alerts->get_alert( $alert->ID ); + } + + foreach ( $this->matching_alerts( $hydrated, $record_id, $recordarr ) as $alert ) { + $alert->send_alert( $record_id, $recordarr ); // @todo send_alert expects int, not array. + } + + return $recordarr; + } + + /** + * Return alerts whose check_record() result is true. + * + * Matching goes through Alert::check_record() so the + * wp_stream_alert_trigger_check filter still applies. + * + * @param array $alerts Hydrated Alert objects (false entries are skipped). + * @param int $record_id The record being processed. + * @param array $recordarr Record data. + * @return array Alert objects that matched. + */ + public function matching_alerts( array $alerts, $record_id, array $recordarr ) { + $matches = array(); + foreach ( $alerts as $alert ) { + if ( false === $alert ) { + continue; + } + + $status = $alert->check_record( $record_id, $recordarr ); + if ( $status ) { + $matches[] = $alert; + } + } + + return $matches; + } +} diff --git a/classes/class-alerts.php b/classes/class-alerts.php index 0d34ed7d5..a2c75ea99 100644 --- a/classes/class-alerts.php +++ b/classes/class-alerts.php @@ -41,6 +41,8 @@ class Alerts { /** * Post meta prefix + * + * @var string */ public string $meta_prefix = 'wp_stream'; @@ -58,81 +60,38 @@ class Alerts { */ public $alert_triggers = array(); + /** + * Record-matching collaborator. + * + * Public so in-plugin callers can match alerts without an Alerts façade. + * + * @var Alerts_Trigger_Engine + */ + public Alerts_Trigger_Engine $trigger_engine; + + /** + * Admin forms / AJAX / menu collaborator. + * + * Public so in-plugin callers can render alert UI without an Alerts façade. + * + * @var Alerts_Admin_UI + */ + public Alerts_Admin_UI $admin_ui; + /** * Class constructor. * * @param Plugin $plugin Instance of plugin object. */ public function __construct( public $plugin ) { + $this->trigger_engine = new Alerts_Trigger_Engine( $this->plugin ); + $this->admin_ui = new Alerts_Admin_UI( $this->plugin ); + // Register custom post type. add_action( 'init', array( $this, 'register_post_type' ) ); - // Add custom post type to menu. - add_action( 'wp_stream_admin_menu', array( $this, 'register_menu' ) ); - - // Add scripts to post screens. - add_action( - 'admin_enqueue_scripts', - array( - $this, - 'register_scripts', - ) - ); - - add_action( - 'network_admin_menu', - array( - $this, - 'change_menu_link_url', - ), - 99 - ); - - add_filter( - 'wp_stream_record_inserted', - array( - $this, - 'check_records', - ), - 10, - 2 - ); - - add_action( - 'wp_ajax_load_alerts_settings', - array( - $this, - 'load_alerts_settings', - ) - ); - add_action( 'wp_ajax_get_actions', array( $this, 'get_actions' ) ); - add_action( - 'wp_ajax_save_new_alert', - array( - $this, - 'save_new_alert', - ) - ); - add_action( - 'wp_ajax_get_new_alert_triggers_notifications', - array( - $this, - 'get_new_alert_triggers_notifications', - ) - ); - $this->load_alert_types(); $this->load_alert_triggers(); - - add_filter( - 'wp_stream_action_links_posts', - array( - $this, - 'change_alert_action_links', - ), - 11, - 2 - ); } /** @@ -263,66 +222,6 @@ public function is_valid_alert_trigger( $alert_trigger ) { return true; } - /** - * Checks record being processed against active alerts. - * - * @filter wp_stream_record_inserted - * - * @param int $record_id The record being processed. - * @param array $recordarr Record data. - * - * @return array - */ - public function check_records( $record_id, $recordarr ) { - $args = array( - 'post_type' => self::POST_TYPE, - 'post_status' => 'wp_stream_enabled', - ); - - $alerts = new \WP_Query( $args ); - foreach ( $alerts->posts as $alert ) { - $alert = $this->get_alert( $alert->ID ); - - if ( false === $alert ) { - continue; - } - - $status = $alert->check_record( $record_id, $recordarr ); - if ( $status ) { - $alert->send_alert( $record_id, $recordarr ); // @todo send_alert expects int, not array. - } - } - - return $recordarr; - } - - /** - * Register scripts for page load - * - * @action admin_enqueue_scripts - * - * @return void - */ - public function register_scripts() { - $screen = get_current_screen(); - if ( 'edit-wp_stream_alerts' !== $screen->id ) { - return; - } - - $this->plugin->enqueue_asset( - 'alerts', - array( - $this->plugin->with_select2(), - 'inline-edit-post', - ), - array( - 'any' => __( 'Any', 'stream' ), - 'anyContext' => __( 'Any Context', 'stream' ), - 'getActionsNonce' => wp_create_nonce( 'stream_get_actions' ), - ) - ); - } - /** * Register custom post type * @@ -456,470 +355,4 @@ public function get_alerts( array $statuses = array() ) { return $alerts; } - - /** - * Add custom post type to menu - * - * @action admin_menu - * - * @return void - */ - public function register_menu() { - add_submenu_page( - $this->plugin->admin->records_page_slug, - __( 'Alerts', 'stream' ), - __( 'Alerts', 'stream' ), - self::CAPABILITY, - 'edit.php?post_type=wp_stream_alerts' - ); - } - - /** - * Modify the Stream > Alerts Network Admin Menu link. - * - * In self::register_menu(), the Alerts submenu item - * is essentially set to go to the Site's admin area. - * - * However, on the Network admin, we need to redirect - * it to the first site in the network, as this is - * where the true Network Alerts settings page is located. - * - * @action network_admin_menu - * @return bool - */ - public function change_menu_link_url() { - global $submenu; - - $parent = 'wp_stream'; - $page = 'edit.php?post_type=wp_stream_alerts'; - - // If we're not on the Stream menu item, return. - if ( ! isset( $submenu[ $parent ] ) ) { - return false; - } - - // Get the first existing Site in the Network. - $sites = wp_stream_get_sites( - array( - 'limit' => 5, // Limit the size of the query. - ) - ); - - $site_id = '1'; - - // Function wp_get_sites() can return an empty array if the network is too large. - if ( ! empty( $sites ) && ! empty( $sites[0]->blog_id ) ) { - $site_id = $sites[0]->blog_id; - } - - $new_url = get_admin_url( $site_id, $page ); - - foreach ( $submenu[ $parent ] as $key => $value ) { - // Set correct URL for the menu item. - if ( $page === $value[2] ) { - // This hack is not kosher, see the docblock for an explanation. - $submenu[ $parent ][ $key ][2] = $new_url; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - break; - } - } - - return true; - } - - /** - * Display Alert Type Meta Box - * - * @param \WP_Post|array $post Post object for current alert. - * - * @return void - */ - public function display_notification_box( $post = array() ) { - $alert = null; - $alert_type = 'none'; - if ( is_object( $post ) ) { - $alert = $this->get_alert( $post->ID ); - if ( false !== $alert ) { - $alert_type = $alert->alert_type; - } - } - $form = new Form_Generator(); - - echo ''; - $form->render_field( - 'select', - array( - 'id' => 'wp_stream_alert_type', - 'name' => 'wp_stream_alert_type', - 'value' => $alert_type, - 'options' => $this->get_notification_values(), - 'placeholder' => __( 'No Alert', 'stream' ), - 'title' => 'Alert Type:', - ) - ); - - echo '
'; - if ( is_object( $alert ) ) { - $alert->get_alert_type_obj()->display_fields( $alert ); - } else { - $this->plugin->alerts->alert_types['none']->display_fields( array() ); - } - - echo '
'; - } - - /** - * Returns settings form HTML for AJAX use - * - * @action wp_ajax_load_alerts_settings - * - * @return void - */ - public function load_alerts_settings() { - if ( ! current_user_can( self::CAPABILITY ) ) { - wp_send_json_error( - array( - 'message' => 'You do not have permission to do this.', - ) - ); - } - $alert = array(); - $post_id = wp_stream_filter_input( INPUT_POST, 'post_id' ); - if ( ! empty( $post_id ) && 'new' !== $post_id ) { - $alert = $this->get_alert( $post_id ); - if ( false === $alert ) { - wp_send_json_error( - array( - 'message' => 'Could not find alert.', - ) - ); - } - } - - $alert_type = wp_stream_filter_input( INPUT_POST, 'alert_type' ); - if ( empty( $alert_type ) ) { - $alert_type = 'none'; - } - if ( ! array_key_exists( $alert_type, $this->alert_types ) ) { - wp_send_json_error( - array( - 'message' => 'Could not find alert type.', - ) - ); - } - - ob_start(); - $this->alert_types[ $alert_type ]->display_fields( $alert ); - $output = ob_get_contents(); - ob_end_clean(); - - $data = array( - 'html' => $output, - ); - wp_send_json_success( $data ); - } - - /** - * Display Trigger Meta Box - * - * @param \WP_Post|array $post Post object for current alert. - * - * @return void - */ - public function display_triggers_box( $post = array() ) { - $alert = false; - if ( is_object( $post ) ) { - $alert = $this->get_alert( $post->ID ); - } - if ( false === $alert ) { - $alert = array(); - } - - $form = new Form_Generator(); - do_action( 'wp_stream_alert_trigger_form_display', $form, $alert ); - // @TODO use human readable text. - echo ''; - $form->render_fields(); - wp_nonce_field( 'save_alert', 'wp_stream_alerts_nonce' ); - - if ( $post instanceof \WP_Post ) : - /** - * These fields are required for the post to be saved, as the Admin AJAX inline_save action is fired. - * - * @see get_inline_data() - * @see wp_ajax_inline_save() - */ - ?> - - - - - - - - post_status; - if ( 'auto-draft' === $post_status ) { - $post_status = 'wp_stream_enabled'; - } - ?> -
-
-
-
- - -
-
-
-
- -
-
- ID ) ) { - if ( ! EMPTY_TRASH_DAYS ) { - $delete_text = __( 'Delete Permanently', 'stream' ); - } else { - $delete_text = __( 'Move to Trash', 'stream' ); - } - ?> - - - - -
-
- - -
-
-
-
- -
-
-
- -
-
-
-
- alert_types, 'name', 'slug' ); - foreach ( $names as $slug => $name ) { - $result[ $slug ] = $name; - } - - return $result; - } - - /** - * Update actions dropdown options based on the connector selected. - */ - public function get_actions() { - if ( ! current_user_can( self::CAPABILITY ) ) { - wp_send_json_error( - array( - 'message' => 'You do not have permission to do this.', - ) - ); - } - - check_ajax_referer( 'stream_get_actions', 'nonce' ); - - $connector_name = wp_stream_filter_input( INPUT_POST, 'connector' ); - $stream_connectors = wp_stream_get_instance()->connectors; - if ( ! empty( $connector_name ) ) { - if ( isset( $stream_connectors->connectors[ $connector_name ] ) ) { - $connector = $stream_connectors->connectors[ $connector_name ]; - if ( method_exists( $connector, 'get_action_labels' ) ) { - $actions = $connector->get_action_labels(); - } - } - } else { - $actions = $stream_connectors->term_labels['stream_action']; - } - ksort( $actions ); - wp_send_json_success( $actions ); - } - - /** - * Save a new alert - */ - public function save_new_alert() { - check_ajax_referer( 'save_alert', 'wp_stream_alerts_nonce' ); - - if ( ! current_user_can( $this->plugin->admin->settings_cap ) ) { - wp_die( - esc_html__( "You don't have sufficient privileges to do this action.", 'stream' ) - ); - } - - $trigger_author = wp_stream_filter_input( INPUT_POST, 'wp_stream_trigger_author' ); - $trigger_connector_and_context = wp_stream_filter_input( INPUT_POST, 'wp_stream_trigger_context' ); - if ( false !== strpos( $trigger_connector_and_context, '-' ) ) { - // This is a connector with a context such as posts-post. - $trigger_connector_and_context_split = explode( '-', $trigger_connector_and_context ); - $trigger_connector = $trigger_connector_and_context_split[0]; - $trigger_context = $trigger_connector_and_context_split[1]; - } elseif ( ! empty( $trigger_connector_and_context ) ) { - // This is a parent connector with no dash such as posts. - $trigger_connector = $trigger_connector_and_context; - $trigger_context = ''; - } else { - // There is no connector or context. - $trigger_connector = ''; - $trigger_context = ''; - } - - $trigger_action = wp_stream_filter_input( INPUT_POST, 'wp_stream_trigger_action' ); - $alert_type = wp_stream_filter_input( INPUT_POST, 'wp_stream_alert_type' ); - $alert_status = wp_stream_filter_input( INPUT_POST, 'wp_stream_alert_status' ); - - // Insert the post into the database. - $item = (object) array( - 'alert_type' => $alert_type, - 'alert_meta' => array( - 'trigger_author' => $trigger_author, - 'trigger_connector' => $trigger_connector, - 'trigger_action' => $trigger_action, - 'trigger_context' => $trigger_context, - ), - 'alert_status' => $alert_status, - ); - $alert = new Alert( $item, $this->plugin ); - $title = $alert->get_title(); - $post_id = wp_insert_post( - array( - 'post_status' => $alert_status, - 'post_type' => 'wp_stream_alerts', - 'post_title' => $title, - ) - ); - if ( empty( $post_id ) ) { - wp_send_json_error(); - } - add_post_meta( $post_id, 'alert_type', $alert_type ); - - $alert_meta = array( - 'trigger_author' => $trigger_author, - 'trigger_connector' => $trigger_connector, - 'trigger_action' => $trigger_action, - 'trigger_context' => $trigger_context, - ); - $alert_meta = apply_filters( 'wp_stream_alerts_save_meta', $alert_meta, $alert_type ); - add_post_meta( $post_id, 'alert_meta', $alert_meta ); - wp_send_json_success( - array( - 'success' => true, - ) - ); - } - - /** - * Return HTML string of the Alert page controls. - */ - public function get_new_alert_triggers_notifications() { - if ( ! current_user_can( $this->plugin->admin->settings_cap ) ) { - wp_die( - esc_html__( "You don't have sufficient privileges to do this action.", 'stream' ) - ); - } - - ob_start(); - ?> -
- Add New - alerts->display_triggers_box(); ?> -
-
- alerts->display_notification_box(); ?> -
-
- alerts->display_status_box(); ?> -
- true, - 'html' => $html, - ) - ); - } - - /** - * Add action links to Stream drop row in admin list screen - * - * @filter wp_stream_action_links_{connector} - * - * @param array $links Previous links registered. - * @param Record $record Stream record. - * - * @return array Action links - */ - public function change_alert_action_links( $links, $record ) { - $post = get_post( $record->object_id ); - - if ( $post && self::POST_TYPE === $post->post_type && $post->post_status === $record->get_meta( 'new_status', true ) ) { - if ( 'trash' !== $post->post_status ) { - $connector_posts = new \WP_Stream\Connector_Posts(); - $post_type_name = $connector_posts->get_post_type_name( get_post_type( $post->ID ) ); - - /* translators: %s: the post type singular name (e.g. "Post") */ - $links[ sprintf( esc_html_x( 'Edit %s', 'Post type singular name', 'stream' ), $post_type_name ) ] = admin_url( 'edit.php?post_type=wp_stream_alerts#post-' . $post->ID ); - unset( $links[ esc_html__( 'View', 'stream' ) ] ); - } - } - - return $links; - } } diff --git a/tests/phpunit/Alerts_Test.php b/tests/phpunit/Alerts_Test.php index 958595322..638d85088 100644 --- a/tests/phpunit/Alerts_Test.php +++ b/tests/phpunit/Alerts_Test.php @@ -18,6 +18,8 @@ public function tearDown(): void { public function test_construct() { $alerts = new Alerts( $this->plugin ); $this->assertNotEmpty( $alerts->plugin ); + $this->assertInstanceOf( Alerts_Trigger_Engine::class, $alerts->trigger_engine ); + $this->assertInstanceOf( Alerts_Admin_UI::class, $alerts->admin_ui ); } public function test_load_alert_types() { @@ -100,21 +102,22 @@ public function test_is_valid_alert_trigger() { } public function test_check_records() { - $this->markTestIncomplete( - 'This test is incomplete.' - ); - // WP_Query not finding active alerts. + $alerts = $this->plugin->alerts; - $alerts = new Alerts( $this->plugin ); - $alert = new Alert( $this->dummy_alert_data(), $this->plugin ); - $alert->save(); + $data = $this->dummy_alert_data(); + $data->ID = 0; + $alert = new Alert( $data, $this->plugin ); + $post_id = $alert->save(); + $this->assertNotEmpty( $post_id ); $action = new \MockAction(); add_filter( 'wp_stream_alert_trigger_check', array( $action, 'filter' ) ); - $alerts->check_records( 0, $this->dummy_stream_data() ); + $record = $this->dummy_stream_data(); + $result = $alerts->trigger_engine->check_records( 0, $record ); - $this->assertEquals( 1, $action->get_call_count() ); + $this->assertSame( $record, $result ); + $this->assertGreaterThan( 0, $action->get_call_count() ); } public function test_register_post_type() { @@ -186,15 +189,18 @@ public function test_get_alert_blank() { public function test_register_menu() { global $submenu; - $this->markTestIncomplete(); + $slug = $this->plugin->admin->records_page_slug; + $this->plugin->alerts->admin_ui->register_menu(); - $this->assertEquals( array(), $submenu[ $this->plugin->admin->records_page_slug ] ); - $submenu[ $this->plugin->admin->records_page_slug ] = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $this->assertEmpty( $submenu[ $this->plugin->admin->records_page_slug ] ); - - $alerts = new Alerts( $this->plugin ); - $alerts->register_menu(); - $this->assertNotEmpty( $submenu[ $this->plugin->admin->records_page_slug ] ); + $this->assertArrayHasKey( $slug, $submenu ); + $found = false; + foreach ( $submenu[ $slug ] as $item ) { + if ( isset( $item[2] ) && 'edit.php?post_type=wp_stream_alerts' === $item[2] ) { + $found = true; + break; + } + } + $this->assertTrue( $found, 'Alerts submenu points at the alerts post type list.' ); } public function test_display_notification_box() { @@ -206,18 +212,12 @@ public function test_display_notification_box() { $post_id = $alert->save(); ob_start(); - $alerts->display_notification_box( get_post( $alert->ID ) ); - $output = ob_get_contents(); - ob_end_clean(); - - $len_test = strlen( $output ) > 0; - $this->assertTrue( $len_test, 'Output length greater than zero.' ); - - $field_test = strpos( $output, 'wp_stream_alert_type' ) !== -1; - $this->assertTrue( $len_test, 'Alert type field is present.' ); + $alerts->admin_ui->display_notification_box( get_post( $alert->ID ) ); + $output = ob_get_clean(); - $form_test = strpos( $output, 'wp_stream_alert_type_form' ) !== -1; - $this->assertTrue( $form_test, 'Alert type settings form is present' ); + $this->assertNotSame( '', $output ); + $this->assertStringContainsString( 'wp_stream_alert_type', $output ); + $this->assertStringContainsString( 'wp_stream_alert_type_form', $output ); } public function test_load_alerts_settings() { @@ -316,15 +316,11 @@ public function test_display_triggers_box() { $post_id = $alert->save(); ob_start(); - $alerts->display_triggers_box( get_post( $alert->ID ) ); - $output = ob_get_contents(); - ob_end_clean(); + $alerts->admin_ui->display_triggers_box( get_post( $alert->ID ) ); + $output = ob_get_clean(); - $len_test = strlen( $output ) > 0; - $this->assertTrue( $len_test, 'Output length greater than zero.' ); - - $field_test = strpos( $output, 'wp_stream_alerts_nonce' ) !== -1; - $this->assertTrue( $len_test, 'Nonce field is present.' ); + $this->assertNotSame( '', $output ); + $this->assertStringContainsString( 'wp_stream_alerts_nonce', $output ); } public function test_display_submit_box() { @@ -336,31 +332,21 @@ public function test_display_submit_box() { $post_id = $alert->save(); ob_start(); - $alerts->display_submit_box( get_post( $alert->ID ) ); - $output = ob_get_contents(); - ob_end_clean(); - - $len_test = strlen( $output ) > 0; - $this->assertTrue( $len_test, 'Output length greater than zero.' ); + $alerts->admin_ui->display_submit_box( get_post( $alert->ID ) ); + $output = ob_get_clean(); - $field_test = strpos( $output, 'wp_stream_enabled' ) !== -1; - $this->assertTrue( $len_test, 'Alert is shown as enabled.' ); + $this->assertNotSame( '', $output ); + $this->assertStringContainsString( 'wp_stream_enabled', $output ); } public function test_get_notification_values() { $alerts = new Alerts( $this->plugin ); $count = count( $alerts->alert_types ); - $output = $alerts->get_notification_values(); + $output = $alerts->admin_ui->get_notification_values(); $this->assertEquals( $count, count( $output ) ); } - public function test_save_post_info() { - $this->markTestIncomplete( - 'This test is incomplete' - ); - } - public function test_get_actions() { $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); wp_set_current_user( $user_id ); @@ -565,6 +551,9 @@ public function test_get_new_alert_triggers_notifications() { $this->assertIsObject( $response ); $this->assertObjectHasProperty( 'success', $response ); $this->assertTrue( $response->success ); + $this->assertObjectHasProperty( 'html', $response->data ); + $this->assertStringContainsString( 'wp_stream_alert_status', $response->data->html ); + $this->assertStringContainsString( 'wp_stream_alert_type', $response->data->html ); } public function test_get_new_alert_triggers_notifications_missing_caps() { diff --git a/tests/phpunit/unit/Alerts_Admin_UI_Unit_Test.php b/tests/phpunit/unit/Alerts_Admin_UI_Unit_Test.php new file mode 100644 index 000000000..ce2cd4328 --- /dev/null +++ b/tests/phpunit/unit/Alerts_Admin_UI_Unit_Test.php @@ -0,0 +1,313 @@ +stubTranslationFunctions(); + $this->stubEscapeFunctions(); + + Functions\when( 'wp_list_pluck' )->alias( array( self::class, 'wp_list_pluck_stub' ) ); + Functions\when( 'wp_parse_args' )->alias( array( self::class, 'wp_parse_args_stub' ) ); + Functions\when( 'selected' )->alias( array( self::class, 'selected_stub' ) ); + Functions\when( 'wp_nonce_field' )->alias( array( self::class, 'wp_nonce_field_stub' ) ); + + $this->plugin = Mockery::mock( Plugin::class ); + $alerts = Mockery::mock( Alerts::class ); + $alerts->alert_types = $this->sample_alert_types(); + $this->plugin->alerts = $alerts; + + $this->admin_ui = new Alerts_Admin_UI( $this->plugin ); + } + + /** + * Merge defaults like wp_parse_args. + * + * @param array $args Incoming args. + * @param array $defaults Defaults. + * @return array + */ + public static function wp_parse_args_stub( $args, $defaults = array() ) { + return array_merge( $defaults, (array) $args ); + } + + /** + * selected() stand-in; third arg false returns the attribute. + * + * @param mixed $selected Compared value. + * @param mixed $current Current value. + * @param bool $echo Unused. + * @return string + */ + public static function selected_stub( $selected, $current = true, $echo = true ) { + unset( $echo ); + return ( $selected === $current ) ? ' selected="selected"' : ''; + } + + /** + * Echo a nonce field marker. + * + * @param string $action Action name. + * @param string $name Field name. + * @return void + */ + public static function wp_nonce_field_stub( $action, $name ) { + unset( $action ); + printf( '', esc_attr( $name ) ); + } + + /** + * WordPress wp_list_pluck stand-in. + * + * @param array $input_list List of objects or arrays. + * @param string $field Field to pluck. + * @param string|null $index_key Optional index field. + * @return array + */ + public static function wp_list_pluck_stub( $input_list, $field, $index_key = null ) { + $result = array(); + foreach ( $input_list as $key => $item ) { + $value = is_object( $item ) ? $item->{$field} : $item[ $field ]; + if ( null !== $index_key ) { + $idx = is_object( $item ) ? $item->{$index_key} : $item[ $index_key ]; + $result[ $idx ] = $value; + } else { + $result[ $key ] = $value; + } + } + + return $result; + } + + /** + * Sample alert types with slug/name, matching production objects. + * + * @return array + */ + protected function sample_alert_types() { + $none = new \stdClass(); + $none->slug = 'none'; + $none->name = 'Do Nothing'; + $highlight = new \stdClass(); + $highlight->slug = 'highlight'; + $highlight->name = 'Highlight'; + $email = new \stdClass(); + $email->slug = 'email'; + $email->name = 'Email'; + + return array( + 'none' => $none, + 'highlight' => $highlight, + 'email' => $email, + ); + } + + /** + * Maps type slug to display name. + * + * @param string $slug Type slug. + * @param string $expected Expected display name. + */ + #[DataProvider( 'data_notification_slugs' )] + public function test_get_notification_values_maps_slug_to_name( $slug, $expected ) { + $values = $this->admin_ui->get_notification_values(); + + $this->assertArrayHasKey( $slug, $values ); + $this->assertSame( $expected, $values[ $slug ] ); + } + + /** + * Slug → name pairs for get_notification_values. + * + * @return array + */ + public static function data_notification_slugs() { + return array( + 'none' => array( 'none', 'Do Nothing' ), + 'highlight' => array( 'highlight', 'Highlight' ), + 'email' => array( 'email', 'Email' ), + ); + } + + public function test_get_notification_values_count_matches_alert_types() { + $values = $this->admin_ui->get_notification_values(); + + $this->assertCount( count( $this->plugin->alerts->alert_types ), $values ); + } + + public function test_display_status_box_outputs_enabled_and_disabled() { + ob_start(); + $this->admin_ui->display_status_box(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'wp_stream_alert_status', $output ); + $this->assertStringContainsString( 'wp_stream_enabled', $output ); + $this->assertStringContainsString( 'wp_stream_disabled', $output ); + } + + public function test_display_submit_box_returns_early_when_post_empty() { + ob_start(); + $this->admin_ui->display_submit_box( null ); + $this->assertSame( '', ob_get_clean() ); + } + + public function test_display_notification_box_without_post_renders_type_select() { + $none = Mockery::mock(); + $none->slug = 'none'; + $none->name = 'Do Nothing'; + $none->shouldReceive( 'display_fields' )->once()->with( array() ); + $this->plugin->alerts->alert_types['none'] = $none; + + ob_start(); + $this->admin_ui->display_notification_box(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'wp_stream_alert_type', $output ); + $this->assertStringContainsString( 'wp_stream_alert_type_form', $output ); + $this->assertStringContainsString( 'Do Nothing', $output ); + } + + public function test_display_triggers_box_without_post_prints_nonce() { + ob_start(); + $this->admin_ui->display_triggers_box(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'wp_stream_alerts_nonce', $output ); + $this->assertStringContainsString( 'Alert me when', $output ); + } + + public function test_change_menu_link_url_returns_false_when_stream_menu_missing() { + $GLOBALS['submenu'] = array(); + + $this->assertFalse( $this->admin_ui->change_menu_link_url() ); + } + + public function test_change_menu_link_url_rewrites_alerts_item_to_first_site() { + $GLOBALS['submenu'] = array( + 'wp_stream' => array( + array( 'Alerts', 'manage_options', 'edit.php?post_type=wp_stream_alerts' ), + ), + ); + + Functions\when( 'wp_stream_get_sites' )->justReturn( + array( + (object) array( + 'blog_id' => '7', + ), + ) + ); + Functions\when( 'get_admin_url' )->alias( + static function ( $site_id, $page ) { + return 'https://example.test/site-' . $site_id . '/' . $page; + } + ); + + $this->assertTrue( $this->admin_ui->change_menu_link_url() ); + $this->assertSame( + 'https://example.test/site-7/edit.php?post_type=wp_stream_alerts', + $GLOBALS['submenu']['wp_stream'][0][2] + ); + } + + public function test_change_menu_link_url_falls_back_to_site_one_when_sites_empty() { + $GLOBALS['submenu'] = array( + 'wp_stream' => array( + array( 'Alerts', 'manage_options', 'edit.php?post_type=wp_stream_alerts' ), + ), + ); + + Functions\when( 'wp_stream_get_sites' )->justReturn( array() ); + Functions\when( 'get_admin_url' )->alias( + static function ( $site_id, $page ) { + return 'https://example.test/site-' . $site_id . '/' . $page; + } + ); + + $this->assertTrue( $this->admin_ui->change_menu_link_url() ); + $this->assertSame( + 'https://example.test/site-1/edit.php?post_type=wp_stream_alerts', + $GLOBALS['submenu']['wp_stream'][0][2] + ); + } + + public function test_register_scripts_skips_enqueue_off_alerts_screen() { + Functions\when( 'get_current_screen' )->justReturn( + (object) array( + 'id' => 'edit-post', + ) + ); + $this->plugin->shouldReceive( 'enqueue_asset' )->never(); + + $this->admin_ui->register_scripts(); + } + + public function test_register_scripts_enqueues_alerts_asset_on_list_screen() { + Functions\when( 'get_current_screen' )->justReturn( + (object) array( + 'id' => 'edit-wp_stream_alerts', + ) + ); + Functions\when( 'wp_create_nonce' )->justReturn( 'stream-nonce' ); + $this->plugin->shouldReceive( 'with_select2' )->once()->andReturn( array( 'select2' ) ); + $this->plugin->shouldReceive( 'enqueue_asset' ) + ->once() + ->with( + 'alerts', + array( array( 'select2' ), 'inline-edit-post' ), + Mockery::on( + static function ( $l10n ) { + return isset( $l10n['getActionsNonce'] ) && 'stream-nonce' === $l10n['getActionsNonce']; + } + ) + ); + + $this->admin_ui->register_scripts(); + } + + public function test_change_alert_action_links_unchanged_when_post_missing() { + Functions\when( 'get_post' )->justReturn( null ); + $record = Mockery::mock( Record::class ); + $record->object_id = 12; + $links = array( + 'View' => 'https://example.test/view', + ); + + $this->assertSame( $links, $this->admin_ui->change_alert_action_links( $links, $record ) ); + } + + public function test_change_alert_action_links_unchanged_for_other_post_types() { + Functions\when( 'get_post' )->justReturn( + (object) array( + 'ID' => 4, + 'post_type' => 'post', + 'post_status' => 'publish', + ) + ); + $record = Mockery::mock( Record::class ); + $record->object_id = 4; + $links = array( + 'View' => 'https://example.test/view', + ); + + $this->assertSame( $links, $this->admin_ui->change_alert_action_links( $links, $record ) ); + } +} diff --git a/tests/phpunit/unit/Alerts_Trigger_Engine_Unit_Test.php b/tests/phpunit/unit/Alerts_Trigger_Engine_Unit_Test.php new file mode 100644 index 000000000..eb2c4000e --- /dev/null +++ b/tests/phpunit/unit/Alerts_Trigger_Engine_Unit_Test.php @@ -0,0 +1,226 @@ +plugin = Mockery::mock( Plugin::class ); + $this->plugin->alerts = Mockery::mock( Alerts::class ); + $this->engine = new Alerts_Trigger_Engine( $this->plugin ); + } + + protected function tear_down() { + Alerts_Trigger_Engine_Wp_Query_Stub::$posts_to_return = array(); + Alerts_Trigger_Engine_Wp_Query_Stub::$last_args = null; + parent::tear_down(); + } + + /** + * Alias a test-local WP_Query so check_records() can run on the host. + * + * @return void + */ + private static function stub_wp_query() { + if ( ! class_exists( \WP_Query::class, false ) ) { + class_alias( Alerts_Trigger_Engine_Wp_Query_Stub::class, 'WP_Query' ); + } + } + + public function test_register_hooks_attaches_check_records_to_record_inserted() { + $this->assertSame( + 10, + has_filter( 'wp_stream_record_inserted', array( $this->engine, 'check_records' ) ) + ); + } + + /** + * Returns only alerts whose check_record() is true. + * + * @param array $outcomes Per-alert check_record results. + * @param int $expected_count Expected match count. + */ + #[DataProvider( 'data_matching_outcomes' )] + public function test_matching_alerts_filters_by_check_record( $outcomes, $expected_count ) { + $record_id = 5; + $recordarr = array( + 'action' => 'updated', + ); + $alerts = array(); + foreach ( $outcomes as $matches ) { + $alert = Mockery::mock( Alert::class )->makePartial(); + $alert->shouldReceive( 'check_record' ) + ->once() + ->with( $record_id, $recordarr ) + ->andReturn( $matches ); + $alerts[] = $alert; + } + + $result = $this->engine->matching_alerts( $alerts, $record_id, $recordarr ); + + $this->assertCount( $expected_count, $result ); + foreach ( $result as $alert ) { + $this->assertContains( $alert, $alerts ); + } + } + + /** + * Match / no-match / mixed lists. Scalars only; mocks stay in the test. + * + * @return array, 1: int}> + */ + public static function data_matching_outcomes() { + return array( + 'all_match' => array( array( true, true ), 2 ), + 'none_match' => array( array( false, false ), 0 ), + 'mixed' => array( array( true, false, true ), 2 ), + ); + } + + public function test_matching_alerts_empty_input_returns_empty() { + $this->assertSame( + array(), + $this->engine->matching_alerts( array(), 1, array() ) + ); + } + + public function test_matching_alerts_skips_false_entries() { + $recordarr = array( + 'action' => 'updated', + ); + $matching = Mockery::mock( Alert::class )->makePartial(); + $matching->shouldReceive( 'check_record' ) + ->once() + ->with( 7, $recordarr ) + ->andReturn( true ); + + $result = $this->engine->matching_alerts( array( false, $matching ), 7, $recordarr ); + + $this->assertSame( array( $matching ), $result ); + } + + public function test_check_records_returns_recordarr_when_no_enabled_alerts() { + $recordarr = array( + 'action' => 'activated', + ); + + $this->plugin->alerts->shouldReceive( 'get_alert' )->never(); + + $result = $this->engine->check_records( 11, $recordarr ); + + $this->assertSame( $recordarr, $result ); + $this->assertSame( + array( + 'post_type' => Alerts::POST_TYPE, + 'post_status' => 'wp_stream_enabled', + ), + Alerts_Trigger_Engine_Wp_Query_Stub::$last_args + ); + } + + /** + * Hydrates query posts and sends only matching alerts. + * + * @param array $outcomes Per-alert check_record results. + * @param int $expected_count Unused; shared provider with matching_alerts. + */ + #[DataProvider( 'data_matching_outcomes' )] + public function test_check_records_sends_only_matching_alerts( $outcomes, $expected_count ) { + unset( $expected_count ); + $record_id = 9; + $recordarr = array( + 'connector' => 'installer', + 'action' => 'activated', + ); + $hydrated = array(); + + foreach ( $outcomes as $index => $matches ) { + $post_id = $index + 1; + Alerts_Trigger_Engine_Wp_Query_Stub::$posts_to_return[] = (object) array( + 'ID' => $post_id, + ); + + $alert = Mockery::mock( Alert::class )->makePartial(); + $alert->shouldReceive( 'check_record' ) + ->once() + ->with( $record_id, $recordarr ) + ->andReturn( $matches ); + + if ( $matches ) { + $alert->shouldReceive( 'send_alert' ) + ->once() + ->with( $record_id, $recordarr ); + } else { + $alert->shouldReceive( 'send_alert' )->never(); + } + + $hydrated[ $post_id ] = $alert; + } + + $this->plugin->alerts->shouldReceive( 'get_alert' ) + ->times( count( $outcomes ) ) + ->andReturnUsing( + static function ( $post_id ) use ( $hydrated ) { + return $hydrated[ $post_id ]; + } + ); + + $result = $this->engine->check_records( $record_id, $recordarr ); + + $this->assertSame( $recordarr, $result ); + } +} + +/** + * Test-local WP_Query for Alerts_Trigger_Engine::check_records(). + */ +class Alerts_Trigger_Engine_Wp_Query_Stub { + + /** + * Posts returned by the next query. + * + * @var array + */ + public static $posts_to_return = array(); + + /** + * Arguments passed to the last constructor call. + * + * @var array|null + */ + public static $last_args; + + /** + * Query posts. + * + * @var array + */ + public $posts = array(); + + /** + * @param array $args WP_Query arguments. + */ + public function __construct( $args = array() ) { + self::$last_args = $args; + $this->posts = self::$posts_to_return; + } +}