', $html );
- $this->assertStringContainsString( 'record-filter-form', $html );
- }
-
- public function test_render_settings_page() {
- ob_start();
- $this->admin->render_settings_page();
- $html = ob_get_clean();
-
- $this->assertStringContainsString( '
', $html );
-
- global $wp_scripts;
-
- $this->assertArrayHasKey( 'wp-stream-settings', $wp_scripts->registered );
- }
-
- public function test_register_list_table() {
- $this->admin->register_list_table();
-
- $this->assertNotEmpty( $this->admin->list_table );
- $this->assertInstanceOf( '\WP_Stream\List_Table', $this->admin->list_table );
- }
-
- /**
- * Also tests private method role_can_view
- */
- public function test_filter_user_caps() {
- $user = new \WP_User( $this->admin_user_id );
-
- $this->plugin->settings->options['general_role_access'] = array( 'administrator' );
- $this->assertTrue( $user->has_cap( $this->admin->view_cap ) );
-
- $this->plugin->settings->options['general_role_access'] = array( 'editor' );
- $this->assertFalse( $user->has_cap( $this->admin->view_cap ) );
- }
-
- /**
- * Also tests private method role_can_view
- */
- public function test_filter_role_caps() {
- $role = get_role( 'administrator' );
-
- $this->plugin->settings->options['general_role_access'] = array( 'administrator' );
- $this->assertTrue( $role->has_cap( $this->admin->view_cap ) );
-
- $this->plugin->settings->options['general_role_access'] = array( 'editor' );
- $this->assertFalse( $role->has_cap( $this->admin->view_cap ) );
- }
-
- /**
- * Test Ajax Filters
- *
- * @group ajax
- * @requires PHPUnit 5.7
- */
- public function test_ajax_filters() {
- $user = new \WP_User( $this->admin_user_id );
-
- $this->_setRole( 'subscriber' );
-
- $_POST['filter'] = 'user_id';
- $_POST['q'] = $user->display_name;
- $_POST['nonce'] = wp_create_nonce( 'stream_filters_user_search_nonce' );
-
- $this->expectException( 'WPAjaxDieStopException' );
-
- try {
- $this->_handleAjax( 'wp_stream_filters' );
- } catch ( WPAjaxDieStopException $e ) {
- // Do nothing.
- }
-
- // Check that the exception was thrown.
- $this->assertTrue( isset( $e ) );
-
- // The output should be a -1 for failure.
- $this->assertEquals( '-1', $e->getMessage() );
- unset( $e );
-
- $this->_setRole( 'administrator' );
-
- $this->_handleAjax( 'wp_stream_filters' );
- $json = $this->_last_response;
-
- $this->assertNotEmpty( $json );
- $data = json_decode( $json );
- $this->assertNotFalse( $data );
- $this->assertNotEmpty( $data );
- $this->assertIsArray( $data );
- }
-
- public function test_get_users_record_meta() {
- $user_id = $this->admin_user_id;
- $authors = array(
- $user_id => get_user_by( 'id', $user_id ),
- );
-
- $records = $this->admin->get_users_record_meta( $authors );
-
- $this->assertArrayHasKey( $user_id, $records );
- $this->assertArrayHasKey( 'text', $records[ $user_id ] );
- $this->assertEquals( 'test_admin', $records[ $user_id ]['text'] );
- }
-
- public function test_get_user_meta() {
- $key = 'message_1';
- $value = 'It is dangerous to remain here. You must leave within two days.';
- update_user_meta( $this->admin_user_id, $key, $value );
- $this->assertEquals( $this->admin->get_user_meta( $this->admin_user_id, $key, true ), $value );
- }
-
- public function test_update_user_meta() {
- $key = 'message_2';
- $value = 'I understand. It is important that you believe me. Look behind you.';
- $this->admin->update_user_meta( $this->admin_user_id, $key, $value );
- $this->assertEquals( get_user_meta( $this->admin_user_id, $key, true ), $value );
- }
-
- public function test_delete_user_meta() {
- $key = 'message_3';
- $value = 'I was David Bowman.';
-
- update_user_meta( $this->admin_user_id, $key, $value );
- $this->assertEquals( get_user_meta( $this->admin_user_id, $key, true ), $value );
-
- $this->admin->delete_user_meta( $this->admin_user_id, $key );
-
- $this->assertEmpty( get_user_meta( $this->admin_user_id, $key, true ) );
- }
-
- private function dummy_stream_data() {
- return array(
- 'object_id' => null,
- 'site_id' => '1',
- 'blog_id' => get_current_blog_id(),
- 'user_id' => '1',
- 'user_role' => 'administrator',
- 'created' => gmdate( 'Y-m-d H:i:s' ),
- 'summary' => '"Hello Dave" plugin activated',
- 'ip' => '192.168.0.1',
- 'connector' => 'installer',
- 'context' => 'plugins',
- 'action' => 'activated',
- );
- }
-
- private function dummy_stream_data_other_blog() {
- return array(
- 'object_id' => null,
- 'site_id' => '1',
- 'blog_id' => (int) get_current_blog_id() + 1,
- 'user_id' => '1',
- 'user_role' => 'administrator',
- 'created' => gmdate( 'Y-m-d H:i:s' ),
- 'summary' => '"Hello Dave" plugin activated',
- 'ip' => '192.168.0.1',
- 'connector' => 'installer',
- 'context' => 'plugins',
- 'action' => 'activated',
- );
- }
-
- private function dummy_meta_data( $stream_id ) {
- return array(
- 'record_id' => $stream_id,
- 'meta_key' => 'space_helmet',
- 'meta_value' => 'false',
- );
- }
-
- /**
- * Insert N stream rows aged $days_old days, optionally pinned to a blog id.
- *
- * @param int $count Number of rows to insert.
- * @param int $days_old How many days ago `created` should be set to.
- * @param int|null $blog_id Optional blog id override.
- * @return int[] Inserted stream IDs.
- */
- private function seed_aged_records( int $count, int $days_old, $blog_id = null ): array {
- global $wpdb;
- $ids = array();
- for ( $i = 0; $i < $count; $i++ ) {
- $row = $this->dummy_stream_data();
- $row['created'] = gmdate( 'Y-m-d H:i:s', strtotime( $days_old . ' days ago' ) );
- if ( null !== $blog_id ) {
- $row['blog_id'] = $blog_id;
- }
- $wpdb->insert( $wpdb->stream, $row );
- $stream_id = (int) $wpdb->insert_id;
- $ids[] = $stream_id;
- $wpdb->insert( $wpdb->streammeta, $this->dummy_meta_data( $stream_id ) );
- }
- return $ids;
- }
-
- /**
- * Set the records TTL in whichever option applies on this install.
- *
- * @param int $days Number of days to retain records for.
- */
- private function set_records_ttl( int $days ) {
- if ( is_multisite() && is_plugin_active_for_network( $this->plugin->locations['plugin'] ) ) {
- $options = (array) get_site_option( 'wp_stream_network', array() );
- $options['general_records_ttl'] = (string) $days;
- unset( $options['general_keep_records_indefinitely'] );
- update_site_option( 'wp_stream_network', $options );
- } else {
- $options = (array) get_option( 'wp_stream', array() );
- $options['general_records_ttl'] = (string) $days;
- unset( $options['general_keep_records_indefinitely'] );
- update_option( 'wp_stream', $options );
- }
- }
-
- public function test_ajax_clean_orphan_meta_schedules_reaper() {
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION );
- }
-
- $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $user_id );
-
- $_REQUEST['wp_stream_nonce_clean_orphan_meta'] = wp_create_nonce( 'stream_nonce_clean_orphan_meta' );
-
- $result = $this->admin->wp_ajax_clean_orphan_meta();
- $this->assertTrue( $result );
-
- $this->assertNotFalse(
- as_next_scheduled_action( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION ),
- 'Ajax handler must enqueue the reaper action'
- );
-
- unset( $_REQUEST['wp_stream_nonce_clean_orphan_meta'] );
- }
-
- /**
- * Security boundary: a user without WP_STREAM_SETTINGS_CAPABILITY must
- * be rejected before the handler reaches the AS enqueue. Mirrors the
- * capability check used by the reset/erase handlers in this class.
- *
- * Uses _handleAjax() so WP_Ajax_UnitTestCase's output-buffer machinery
- * runs (the handler calls wp_die(), which the testcase die handler
- * routes through ob_get_clean()); calling the method directly would
- * leave the buffer state ambiguous and PHPUnit would mark the test risky.
- *
- * @throws \WPAjaxDieStopException Thrown by the testcase die handler when
- * the rejected request triggers wp_die().
- */
- public function test_ajax_clean_orphan_meta_denies_users_without_settings_cap() {
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION );
+ $options = (array) get_site_option( 'wp_stream_network', array() );
+ $site_access = isset( $options['general_site_access'] ) ? absint( $options['general_site_access'] ) : 1;
+ $site_access_disabled = ! $site_access;
}
- $subscriber_id = self::factory()->user->create( array( 'role' => 'subscriber' ) );
- wp_set_current_user( $subscriber_id );
-
- $_REQUEST['wp_stream_nonce_clean_orphan_meta'] = wp_create_nonce( 'stream_nonce_clean_orphan_meta' );
-
- $this->expectException( \WPAjaxDieStopException::class );
-
- try {
- $this->_handleAjax( 'wp_stream_clean_orphan_meta' );
- } catch ( \WPAjaxDieStopException $e ) {
- unset( $_REQUEST['wp_stream_nonce_clean_orphan_meta'] );
- $this->assertFalse(
- as_next_scheduled_action( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION ),
- 'No work must be enqueued for a rejected request'
- );
- throw $e;
+ $has_admin_menu = has_action( 'admin_menu', array( $this->admin->menu, 'register_menu' ) );
+ if ( $site_access_disabled ) {
+ $this->assertFalse( $has_admin_menu );
+ } else {
+ $this->assertNotFalse( $has_admin_menu );
}
}
- public function test_auto_purge_reaper_deletes_orphaned_meta_only() {
- global $wpdb;
-
- // Seed a real record with meta, then a free-floating meta row pointing at
- // a non-existent record_id.
- $stream_data = $this->dummy_stream_data();
- $stream_data['created'] = gmdate( 'Y-m-d H:i:s', strtotime( '5 days ago' ) );
- $wpdb->insert( $wpdb->stream, $stream_data );
- $real_id = (int) $wpdb->insert_id;
- $wpdb->insert( $wpdb->streammeta, $this->dummy_meta_data( $real_id ) );
-
- // Orphan meta: record_id points nowhere.
- $orphan_record_id = $real_id + 999999;
- $wpdb->insert( $wpdb->streammeta, $this->dummy_meta_data( $orphan_record_id ) );
-
- $before_orphans = (int) $wpdb->get_var(
- $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->streammeta} WHERE record_id = %d", $orphan_record_id )
- );
- $this->assertSame( 1, $before_orphans );
-
- $this->admin->auto_purge_reaper();
-
- $after_orphans = (int) $wpdb->get_var(
- $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->streammeta} WHERE record_id = %d", $orphan_record_id )
- );
- $linked_meta = (int) $wpdb->get_var(
- $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->streammeta} WHERE record_id = %d", $real_id )
- );
+ public function test_init() {
+ $this->admin->init();
+ $this->assertNotEmpty( $this->admin->network );
+ $this->assertNotEmpty( $this->admin->live_update );
+ $this->assertNotEmpty( $this->admin->export );
- $this->assertSame( 0, $after_orphans, 'Reaper must delete meta rows whose parent stream row is absent' );
- $this->assertSame( 1, $linked_meta, 'Reaper must not touch meta rows whose parent still exists' );
+ $this->assertInstanceOf( '\WP_Stream\Network', $this->admin->network );
+ $this->assertInstanceOf( '\WP_Stream\Live_Update', $this->admin->live_update );
+ $this->assertInstanceOf( '\WP_Stream\Export', $this->admin->export );
}
- public function test_auto_purge_batch_deletes_window_and_chains_next_batch() {
- global $wpdb;
+ /**
+ * The user_has_cap filter is registered in the Admin constructor, but the
+ * Settings object is only built on init priority 9. A capability check for
+ * the view cap fired before then (e.g. a firewall plugin on plugins_loaded)
+ * must be denied, not fatal on the null options chain.
+ */
+ public function test_filter_user_caps_before_settings_initialized() {
+ $settings = $this->plugin->settings;
+ $this->plugin->settings = null;
- // Force a small batch size so we can chain twice without seeding huge data.
- add_filter(
- 'wp_stream_batch_size',
- function () {
- return 2;
- }
+ $user = get_user_by( 'id', $this->admin_user_id );
+ $allcaps = $this->admin->filter_user_caps(
+ array(),
+ array( $this->admin->view_cap ),
+ array( $this->admin->view_cap, $this->admin_user_id ),
+ $user
);
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION );
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION );
- }
-
- // Seed 5 aged rows. With batch_size=2 the chain runs 3 batches + reaper.
- $this->seed_aged_records( 5, 5 );
-
- $cutoff = ( new \DateTime( 'now', new \DateTimeZone( 'UTC' ) ) )
- ->sub( \DateInterval::createFromDateString( '1 days' ) )
- ->format( 'Y-m-d H:i:s' );
-
- $before = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->stream}" );
-
- $this->admin->auto_purge_batch( $cutoff, 0 );
+ $this->plugin->settings = $settings;
- $remaining = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->stream}" );
- $this->assertLessThan( $before, $remaining, 'Batch must delete at least one row' );
- $this->assertGreaterThan( 0, $remaining, 'Batch must not delete more than one window of rows' );
+ $this->assertArrayNotHasKey( $this->admin->view_cap, $allcaps );
+ }
- $this->assertNotFalse(
- as_next_scheduled_action( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION ),
- 'Next batch must be chained when more eligible rows remain'
+ /**
+ * Once Settings exists, the view cap is granted to allowed roles as before.
+ */
+ public function test_filter_user_caps_grants_view_cap_to_allowed_role() {
+ $user = get_user_by( 'id', $this->admin_user_id );
+ $allcaps = $this->admin->filter_user_caps(
+ array(),
+ array( $this->admin->view_cap ),
+ array( $this->admin->view_cap, $this->admin_user_id ),
+ $user
);
- remove_all_filters( 'wp_stream_batch_size' );
- }
-
- public function test_auto_purge_batch_throws_on_empty_cutoff() {
- $this->expectException( \InvalidArgumentException::class );
- $this->admin->auto_purge_batch( '', 0, 0 );
+ $this->assertArrayHasKey( $this->admin->view_cap, $allcaps );
+ $this->assertTrue( $allcaps[ $this->admin->view_cap ] );
}
- public function test_auto_purge_batch_enqueues_reaper_when_no_rows_remain() {
- global $wpdb;
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION );
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION );
- }
- // Wipe any leftover rows from earlier tests so nothing is eligible.
- $wpdb->query( "DELETE FROM {$wpdb->stream}" );
- $wpdb->query( "DELETE FROM {$wpdb->streammeta}" );
-
- $cutoff = ( new \DateTime( 'now', new \DateTimeZone( 'UTC' ) ) )
- ->sub( \DateInterval::createFromDateString( '1 days' ) )
- ->format( 'Y-m-d H:i:s' );
+ public function test_prepare_admin_notices() {
+ // Test no notices
+ $this->admin->notices = array();
+ $this->admin->prepare_admin_notices();
+ $this->assertEmpty( $this->admin->notices );
- $this->admin->auto_purge_batch( $cutoff, 0 );
+ // Test settings reset notice
+ $_GET['message'] = 'settings_reset';
+ $this->admin->prepare_admin_notices();
+ $this->assertNotEmpty( $this->admin->notices );
- $this->assertFalse(
- as_next_scheduled_action( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION ),
- 'No further batch must be chained when nothing is eligible'
- );
- $this->assertNotFalse(
- as_next_scheduled_action( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION ),
- 'Reaper must be enqueued as the terminal step of the chain'
- );
+ // Prevent output
+ $this->admin->notices = array();
}
- public function test_auto_purge_batch_chain_strides_down_by_window() {
- global $wpdb;
+ public function test_notice() {
+ // Start with nothing
+ $this->admin->notices = array();
+ $this->assertEmpty( $this->admin->notices );
- // Force a small batch size so we can chain multiple times.
- add_filter(
- 'wp_stream_batch_size',
- function () {
- return 3;
- }
- );
+ $message = 'Affirmative, Dave. I read you.';
+ $is_error = false;
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION );
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION );
- }
+ $this->admin->notice( $message, $is_error );
+ $this->assertNotEmpty( $this->admin->notices );
+ ob_start();
+ $this->admin->admin_notices();
+ $notice = ob_get_clean();
- $ids = $this->seed_aged_records( 4, 5 );
- sort( $ids );
- $top_id = end( $ids );
+ $this->assertStringContainsString( $message, $notice );
+ $this->assertStringContainsString( 'updated', $notice );
+ $this->assertStringNotContainsString( 'error', $notice );
- $cutoff = ( new \DateTime( 'now', new \DateTimeZone( 'UTC' ) ) )
- ->sub( \DateInterval::createFromDateString( '1 days' ) )
- ->format( 'Y-m-d H:i:s' );
+ // Clear notices and start again
+ $this->admin->notices = array();
+ $this->assertEmpty( $this->admin->notices );
- // First batch (last_entry=0) should pick the highest ID and pass
- // last_entry = top_id - batch_size to the next batch.
- $this->admin->auto_purge_batch( $cutoff, 0, 0 );
+ $is_error = true;
- $pending = as_get_scheduled_actions(
- array(
- 'hook' => \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION,
- 'status' => \ActionScheduler_Store::STATUS_PENDING,
- )
- );
- $this->assertNotEmpty( $pending );
- $next_args = array_shift( $pending )->get_args();
+ $this->admin->notice( $message, $is_error );
+ $this->assertNotEmpty( $this->admin->notices );
+ ob_start();
+ $this->admin->admin_notices();
+ $notice = ob_get_clean();
- $this->assertArrayHasKey( 'last_entry', $next_args );
- $this->assertSame(
- max( 0, $top_id - 3 ),
- (int) $next_args['last_entry'],
- 'Next batch must receive last_entry = top_id - batch_size'
- );
+ $this->assertStringContainsString( $message, $notice );
+ $this->assertStringContainsString( 'error', $notice );
+ $this->assertStringNotContainsString( 'updated', $notice );
- remove_all_filters( 'wp_stream_batch_size' );
+ // Prevent output
+ $this->admin->notices = array();
}
- /**
- * Acceptance criterion: "Per-site activations only purge the current blog."
- *
- * The batch worker scoping is covered above
- * ({@see test_auto_purge_batch_scopes_to_blog_id_when_non_zero}); this
- * test closes the gap on the routing decision in
- * {@see Admin::purge_scheduled_action()}:
- *
- * $blog_id = $this->plugin->is_multisite_not_network_activated()
- * ? (int) get_current_blog_id()
- * : 0;
- *
- * Forces is_multisite_not_network_activated() to return true via a Plugin
- * stub (CI's multisite lane runs with network-activated = true), then
- * asserts the enqueued batch carries the current blog_id rather than 0.
- */
- public function test_purge_scheduled_action_scopes_to_current_blog_when_not_network_activated() {
- if ( ! is_multisite() ) {
- $this->markTestSkipped( 'Per-site scoping is multisite-only' );
- }
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION );
- }
-
- // Force the batched path so the assertion can read $args['blog_id'].
- add_filter( 'wp_stream_is_large_records_table', '__return_true' );
-
- $current_blog = (int) get_current_blog_id();
- $this->seed_aged_records( 1, 5, $current_blog );
- $this->set_records_ttl( 1 );
-
- // Swap in a Plugin stub that reports per-site activation.
- $real_plugin = $this->admin->plugin;
- $stub = new class( $real_plugin ) {
- public $settings;
- public $db;
- public $locations;
- public $admin;
- public $connectors;
- public $scheduler;
- public function __construct( $real ) {
- $this->settings = $real->settings;
- $this->db = $real->db;
- $this->locations = $real->locations;
- $this->admin = $real->admin;
- $this->connectors = $real->connectors;
- $this->scheduler = $real->scheduler;
- }
- public function is_multisite_not_network_activated() {
- return true;
- }
- public function is_multisite_network_activated() {
- return false;
- }
- public function is_large_records_table( int $n ): bool {
- return apply_filters( 'wp_stream_is_large_records_table', $n > 1000000, $n );
- }
- public function __call( $name, $args ) {
- return call_user_func_array( array( $this->settings->plugin ?? null, $name ), $args );
- }
- };
- $this->admin->plugin = $stub;
+ public function test_admin_notices() {
+ $allowed_html = '
';
+ $disallowed_html = '
';
+ $this->admin->notices = array(
+ array(
+ 'message' => "I'm sorry, Dave. I'm afraid I can't do that. $disallowed_html",
+ 'is_error' => false,
+ ),
+ array(
+ 'message' => "This mission is too important for me to allow you to jeopardize it. $allowed_html",
+ 'is_error' => false,
+ ),
+ );
- try {
- $this->admin->purge_scheduled_action();
+ ob_start();
+ $this->admin->admin_notices();
+ $notices = ob_get_clean();
- $scheduled = as_get_scheduled_actions(
- array(
- 'hook' => \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION,
- 'status' => \ActionScheduler_Store::STATUS_PENDING,
- )
- );
- $this->assertNotEmpty(
- $scheduled,
- 'Per-site activation must still enqueue a batch when records are eligible'
- );
+ $this->assertStringContainsString( $allowed_html, $notices );
+ $this->assertStringNotContainsString( $disallowed_html, $notices );
+ $this->assertStringContainsString( str_replace( $disallowed_html, '', $this->admin->notices[0]['message'] ), $notices );
+ $this->assertStringContainsString( wpautop( $this->admin->notices[1]['message'] ), $notices );
- $action = array_shift( $scheduled );
- $args = $action->get_args();
- $this->assertSame(
- $current_blog,
- (int) $args['blog_id'],
- 'Per-site activation must scope the batch to the current blog (not 0 / all blogs)'
- );
- } finally {
- $this->admin->plugin = $real_plugin;
- remove_filter( 'wp_stream_is_large_records_table', '__return_true' );
- }//end try
+ // Prevent output
+ $this->admin->notices = array();
}
+ public function test_plugin_action_links() {
+ $links = array( '
Disconnect' );
+ $file = plugin_basename( $this->plugin->locations['dir'] . 'stream.php' );
- public function test_auto_purge_batch_scopes_to_blog_id_when_non_zero() {
- global $wpdb;
- if ( ! is_multisite() ) {
- $this->markTestSkipped( 'Multisite scoping test' );
- }
-
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION );
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION );
- }
-
- $current_blog = (int) get_current_blog_id();
- $other_blog = $current_blog + 1000;
- // arbitrary distinct id, no real blog required for SQL scoping.
-
- $this->seed_aged_records( 1, 5, $current_blog );
- $this->seed_aged_records( 1, 5, $other_blog );
-
- $cutoff = ( new \DateTime( 'now', new \DateTimeZone( 'UTC' ) ) )
- ->sub( \DateInterval::createFromDateString( '1 days' ) )
- ->format( 'Y-m-d H:i:s' );
-
- $this->admin->auto_purge_batch( $cutoff, $current_blog );
+ $action_links = $this->admin->plugin_action_links( $links, $file );
- $remaining_other = (int) $wpdb->get_var(
- $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->stream} WHERE blog_id = %d", $other_blog )
- );
- $this->assertSame( 1, $remaining_other, 'Per-blog scoping must leave sibling blogs untouched' );
+ $this->assertStringContainsString( 'Disconnect', $action_links[0] );
+ $this->assertStringContainsString( 'Settings', $action_links[1] );
}
+ /**
+ * Also tests private method role_can_view
+ */
+ public function test_filter_user_caps() {
+ $user = new \WP_User( $this->admin_user_id );
- public function test_is_running_auto_purge_reflects_chain_state() {
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION );
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION );
- }
- $this->assertFalse(
- \WP_Stream\Admin::is_running_auto_purge(),
- 'No scheduled actions means not running'
- );
-
- as_enqueue_async_action(
- \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION,
- array(
- 'cutoff' => '2020-01-01 00:00:00',
- 'blog_id' => 0,
- 'last_entry' => 0,
- ),
- \WP_Stream\Admin::AUTO_PURGE_GROUP
- );
- $this->assertTrue(
- \WP_Stream\Admin::is_running_auto_purge(),
- 'A pending batch action means running'
- );
-
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION );
- as_enqueue_async_action(
- \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION,
- array(),
- \WP_Stream\Admin::AUTO_PURGE_GROUP
- );
- $this->assertTrue(
- \WP_Stream\Admin::is_running_auto_purge(),
- 'A pending reaper action means running'
- );
+ $this->plugin->settings->options['general_role_access'] = array( 'administrator' );
+ $this->assertTrue( $user->has_cap( $this->admin->view_cap ) );
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION );
- $this->assertFalse(
- \WP_Stream\Admin::is_running_auto_purge(),
- 'Chain drained: not running'
- );
+ $this->plugin->settings->options['general_role_access'] = array( 'editor' );
+ $this->assertFalse( $user->has_cap( $this->admin->view_cap ) );
}
- public function test_is_running_auto_purge_includes_in_progress_actions() {
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION );
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION );
- }
-
- // Enqueue and then flip the action's status to IN-PROGRESS to simulate
- // the runner having dequeued an action and started executing it.
- // Without RUNNING-aware filtering, is_running_auto_purge() would
- // return false here and the overlap guard would let a second chain
- // stack against the same rows.
- $action_id = as_enqueue_async_action(
- \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION,
- array(
- 'cutoff' => '2020-01-01 00:00:00',
- 'blog_id' => 0,
- 'last_entry' => 0,
- ),
- \WP_Stream\Admin::AUTO_PURGE_GROUP
- );
- \ActionScheduler::store()->log_execution( $action_id );
+ /**
+ * Also tests private method role_can_view
+ */
+ public function test_filter_role_caps() {
+ $role = get_role( 'administrator' );
- $this->assertTrue(
- \WP_Stream\Admin::is_running_auto_purge(),
- 'In-progress (RUNNING) actions must count as running to prevent overlap'
- );
+ $this->plugin->settings->options['general_role_access'] = array( 'administrator' );
+ $this->assertTrue( $role->has_cap( $this->admin->view_cap ) );
- as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION );
+ $this->plugin->settings->options['general_role_access'] = array( 'editor' );
+ $this->assertFalse( $role->has_cap( $this->admin->view_cap ) );
}
/**
* Integration test for the running-state UI swap. Asserts that the
* "Clean Orphaned Meta" field in Settings::get_fields() flips from
* type=link to type=none and swaps its description when an auto-purge
- * chain is active. is_running_auto_purge() is covered in isolation
- * above; this test closes the loop on the consumer that drives the UI.
+ * chain is active. Admin_Purge::is_running_auto_purge() is covered in
+ * isolation in Admin_Purge_Test; this test closes the loop on the
+ * consumer that drives the UI.
*
* Replaces the e2e specs removed in b4c8f287 for activation-race
* fragility — same assertions, no browser/AS-runner timing surface.
@@ -1498,37 +298,6 @@ public function test_clean_orphan_meta_field_reflects_running_state() {
as_unschedule_all_actions( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION );
}
- public function test_is_running_async_deletion_reflects_scheduled_state() {
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( \WP_Stream\Admin::ASYNC_DELETION_ACTION );
- }
-
- $this->assertFalse(
- \WP_Stream\Admin::is_running_async_deletion(),
- 'No scheduled action means not running'
- );
-
- as_enqueue_async_action(
- \WP_Stream\Admin::ASYNC_DELETION_ACTION,
- array(
- 'total' => 1,
- 'done' => 0,
- 'last_entry' => 1,
- 'blog_id' => (int) get_current_blog_id(),
- )
- );
- $this->assertTrue(
- \WP_Stream\Admin::is_running_async_deletion(),
- 'A pending async-deletion action means running'
- );
-
- as_unschedule_all_actions( \WP_Stream\Admin::ASYNC_DELETION_ACTION );
- $this->assertFalse(
- \WP_Stream\Admin::is_running_async_deletion(),
- 'After unscheduling: not running'
- );
- }
-
/**
* Integration test for the "Reset Stream Database" running-state UI swap.
* Asserts that the delete_all_records field flips from type=link to
@@ -1622,25 +391,4 @@ public function test_get_deletion_warning_respects_precomputed_state() {
as_unschedule_all_actions( \WP_Stream\Admin::ASYNC_DELETION_ACTION );
}
-
- public function test_register_hooks_auto_purge_action_scheduler_callbacks() {
- // The Admin instance is constructed by the test bootstrap, so register()
- // has already run. Just assert the actions are wired up.
- $this->assertNotFalse(
- has_action( \WP_Stream\Admin::AUTO_PURGE_ACTION, array( $this->admin, 'purge_scheduled_action' ) ),
- 'Recurring auto-purge AS callback should be registered'
- );
- $this->assertNotFalse(
- has_action( \WP_Stream\Admin::AUTO_PURGE_BATCH_ACTION, array( $this->admin, 'auto_purge_batch' ) ),
- 'Auto-purge batch worker should be registered'
- );
- $this->assertNotFalse(
- has_action( \WP_Stream\Admin::AUTO_PURGE_REAPER_ACTION, array( $this->admin, 'auto_purge_reaper' ) ),
- 'Auto-purge reaper should be registered'
- );
- $this->assertFalse(
- has_action( 'wp_stream_auto_purge', array( $this->admin, 'purge_scheduled_action' ) ),
- 'Legacy wp_stream_auto_purge hook should no longer dispatch to purge_scheduled_action directly'
- );
- }
}
diff --git a/tests/phpunit/Scheduler_Handoff_Test.php b/tests/phpunit/Scheduler_Handoff_Test.php
index 0b5d630d1..395b44e99 100644
--- a/tests/phpunit/Scheduler_Handoff_Test.php
+++ b/tests/phpunit/Scheduler_Handoff_Test.php
@@ -22,6 +22,13 @@ class Scheduler_Handoff_Test extends WP_StreamTestCase {
*/
protected $admin;
+ /**
+ * Purge collaborator under test.
+ *
+ * @var Admin_Purge
+ */
+ protected $purge;
+
/**
* Scheduler active before a test swapped it.
*
@@ -32,6 +39,7 @@ class Scheduler_Handoff_Test extends WP_StreamTestCase {
public function setUp(): void {
parent::setUp();
$this->admin = $this->plugin->admin;
+ $this->purge = $this->get_admin_collaborator( $this->admin, 'purge' );
$this->original_scheduler = $this->plugin->scheduler;
$this->clear();
}
@@ -68,7 +76,7 @@ public function test_cron_active_clears_stray_action_scheduler_recurring() {
$this->assertNotFalse( as_next_scheduled_action( Admin::AUTO_PURGE_ACTION ) );
$this->plugin->scheduler = new Cron_Scheduler();
- $this->admin->purge_schedule_setup();
+ $this->purge->purge_schedule_setup();
$this->assertFalse(
as_next_scheduled_action( Admin::AUTO_PURGE_ACTION ),
@@ -90,7 +98,7 @@ public function test_action_scheduler_active_clears_stray_wp_cron_recurring() {
$this->assertNotFalse( wp_next_scheduled( Admin::AUTO_PURGE_ACTION ) );
$this->plugin->scheduler = new AS_Scheduler();
- $this->admin->purge_schedule_setup();
+ $this->purge->purge_schedule_setup();
$this->assertFalse(
wp_next_scheduled( Admin::AUTO_PURGE_ACTION ),
@@ -121,7 +129,7 @@ public function test_disable_clears_action_scheduler_store_when_cron_active() {
$this->plugin->scheduler = new Cron_Scheduler();
add_filter( 'wp_stream_enable_auto_purge', '__return_false' );
- $this->admin->purge_schedule_setup();
+ $this->purge->purge_schedule_setup();
remove_all_filters( 'wp_stream_enable_auto_purge' );
$this->assertFalse(
diff --git a/tests/testcase.php b/tests/testcase.php
index 21705315f..811bf0713 100644
--- a/tests/testcase.php
+++ b/tests/testcase.php
@@ -194,6 +194,20 @@ public function expectDeprecated(): void { // phpcs:ignore WordPress.NamingConve
add_action( 'doing_it_wrong_trigger_error', '__return_false' );
}
+ /**
+ * Resolve an Admin collaborator.
+ *
+ * Collaborators are public on Admin; this helper remains for tests that
+ * prefer a named lookup.
+ *
+ * @param Admin $admin Admin instance.
+ * @param string $name Property name (menu|assets|records|settings|ajax|purge).
+ * @return object
+ */
+ protected function get_admin_collaborator( Admin $admin, string $name ) {
+ return $admin->{$name};
+ }
+
/**
* Helper function to check validity of action
*