From 267e26f4367aadbd0c33f3d33b65a4ecec6c394e Mon Sep 17 00:00:00 2001 From: Luis Henrique Mulinari Date: Mon, 21 Aug 2023 17:37:30 -0300 Subject: [PATCH] Add unit tests --- security/user-last-seen.php | 51 +++--- tests/security/test-user-last-seen.php | 219 +++++++++++++++++++++++++ 2 files changed, 246 insertions(+), 24 deletions(-) create mode 100644 tests/security/test-user-last-seen.php diff --git a/security/user-last-seen.php b/security/user-last-seen.php index 4b8151ccc8..0a240d507f 100644 --- a/security/user-last-seen.php +++ b/security/user-last-seen.php @@ -8,15 +8,17 @@ class User_Last_Seen { const LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY = 'wpvip_last_seen_release_date_timestamp'; public function init() { - if ( ! defined( 'VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) || constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) === 'NO_ACTION' ) { + if ( ! defined( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) || constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) === 'NO_ACTION' ) { return; } // Use a global cache group to avoid having to the data for each site wp_cache_add_global_groups( array( self::LAST_SEEN_CACHE_GROUP ) ); + add_action( 'admin_init', array( $this, 'register_release_date' ) ); + add_filter( 'determine_current_user', array( $this, 'determine_current_user' ), 30, 1 ); - add_filter( 'authenticate', array( $this, 'determine_current_user' ), 20, 1 ); + add_filter( 'authenticate', array( $this, 'authenticate' ), 20, 1 ); if ( in_array( constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ), array( 'REPORT', 'BLOCK' ) ) ) { add_filter( 'wpmu_users_columns', array( $this, 'add_last_seen_column_head' ) ); @@ -25,17 +27,16 @@ public function init() { add_filter( 'manage_users_sortable_columns', array( $this, 'add_last_seen_sortable_column' ) ); add_filter( 'manage_users-network_sortable_columns', array( $this, 'add_last_seen_sortable_column' ) ); - add_filter( 'users_list_table_query_args', array( $this, 'last_seen_query_args') ); + add_filter( 'users_list_table_query_args', array( $this, 'last_seen_order_by_query_args') ); + } + if ( $this->is_block_action_enabled() ) { add_filter( 'views_users', array( $this, 'add_blocked_users_filter' ) ); add_filter( 'views_users-network', array( $this, 'add_blocked_users_filter' ) ); - } + add_filter( 'users_list_table_query_args', array( $this, 'last_seen_blocked_users_filter_query_args') ); - if ( constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) === 'BLOCK' ) { add_action( 'admin_init', array( $this, 'last_seen_unblock_action' ) ); } - - add_action( 'admin_init', array( $this, 'register_release_date' ) ); } public function determine_current_user( $user_id ) { @@ -48,7 +49,7 @@ public function determine_current_user( $user_id ) { return $user_id; } - if ( $this->is_considered_inactive( $user_id ) ) { + if ( $this->is_block_action_enabled() && $this->is_considered_inactive( $user_id ) ) { // Force current user to 0 to avoid recursive calls to this filter wp_set_current_user( 0 ); @@ -67,30 +68,34 @@ public function authenticate( $user ) { return $user; } - if ( $user->ID && $this->is_considered_inactive( $user->ID ) ) { + if ( $user->ID && $this->is_block_action_enabled() && $this->is_considered_inactive( $user->ID ) ) { return new \WP_Error( 'inactive_account', __( 'Error: Your account has been flagged as inactive. Please contact your site administrator.', 'inactive-account-lockdown' ) );; } return $user; } - function add_last_seen_column_head( $columns ) { + public function add_last_seen_column_head( $columns ) { $columns[ 'last_seen' ] = __( 'Last seen' ); return $columns; } - function add_last_seen_sortable_column( $columns ) { + public function add_last_seen_sortable_column( $columns ) { $columns['last_seen'] = 'last_seen'; return $columns; } - function last_seen_query_args( $vars ) { + public function last_seen_order_by_query_args( $vars ) { if ( isset( $vars['orderby'] ) && $vars['orderby'] === 'last_seen' ) { $vars[ 'meta_key' ] = self::LAST_SEEN_META_KEY; $vars[ 'orderby' ] = 'meta_value_num'; } + return $vars; + } + + public function last_seen_blocked_users_filter_query_args($vars ) { if ( isset( $_REQUEST[ 'last_seen_filter' ] ) && $_REQUEST[ 'last_seen_filter' ] === 'blocked' ) { $vars[ 'meta_key' ] = self::LAST_SEEN_META_KEY; $vars[ 'meta_value' ] = $this->get_inactivity_timestamp(); @@ -101,7 +106,7 @@ function last_seen_query_args( $vars ) { return $vars; } - function add_last_seen_column_date( $default, $column_name, $user_id ) { + public function add_last_seen_column_date( $default, $column_name, $user_id ) { if ( 'last_seen' !== $column_name ) { return $default; } @@ -118,7 +123,7 @@ function add_last_seen_column_date( $default, $column_name, $user_id ) { date_i18n( get_option('time_format'), $last_seen_timestamp ) ); - if ( ! $this->is_considered_inactive( $user_id ) ) { + if ( ! $this->is_block_action_enabled() || ! $this->is_considered_inactive( $user_id ) ) { return sprintf( '%s', esc_html__( $formatted_date ) ); } @@ -135,7 +140,7 @@ function add_last_seen_column_date( $default, $column_name, $user_id ) { return sprintf( '%s' . $unblock_link, esc_html__( $formatted_date ) ); } - function add_blocked_users_filter( $views ) { + public function add_blocked_users_filter( $views ) { $blog_id = is_network_admin() ? null : get_current_blog_id(); $users_query = new \WP_User_Query( @@ -166,7 +171,7 @@ function add_blocked_users_filter( $views ) { return $views; } - function last_seen_unblock_action() { + public function last_seen_unblock_action() { $admin_notices_hook_name = is_network_admin() ? 'network_admin_notices' : 'admin_notices'; if ( isset( $_GET['reset_last_seen_success'] ) && $_GET['reset_last_seen_success'] === '1' ) { @@ -233,10 +238,6 @@ public function register_release_date() { } public function is_considered_inactive( $user_id ) { - if ( constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) !== 'BLOCK' ) { - return false; - } - $last_seen_timestamp = get_user_meta( $user_id, self::LAST_SEEN_META_KEY, true ); if ( $last_seen_timestamp ) { return $last_seen_timestamp < $this->get_inactivity_timestamp(); @@ -252,10 +253,12 @@ public function is_considered_inactive( $user_id ) { } public function get_inactivity_timestamp() { - if ( ! defined( 'VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) ) { - return 0; - } - return strtotime( sprintf('-%d days', constant( 'VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) ) ) + self::LAST_SEEN_UPDATE_USER_META_CACHE_TTL; } + + private function is_block_action_enabled() { + return defined( 'VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS' ) && + defined( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) && + constant( 'VIP_SECURITY_INACTIVE_USERS_ACTION' ) === 'BLOCK'; + } } diff --git a/tests/security/test-user-last-seen.php b/tests/security/test-user-last-seen.php new file mode 100644 index 0000000000..8185b99d5f --- /dev/null +++ b/tests/security/test-user-last-seen.php @@ -0,0 +1,219 @@ +init(); + + $this->assertFalse( has_filter( 'determine_current_user' ) ); + $this->assertFalse( has_filter( 'authenticate' ) ); + } + + public function test__should_not_load_actions_and_filters_when_env_vars_is_set_to_no_action() { + Constant_Mocker::define( 'VIP_SECURITY_INACTIVE_USERS_ACTION', 'NO_ACTION' ); + + remove_all_filters( 'determine_current_user' ); + remove_all_filters( 'authenticate' ); + + $last_seen = new User_Last_Seen(); + $last_seen->init(); + + $this->assertFalse( has_filter( 'determine_current_user' ) ); + $this->assertFalse( has_filter( 'authenticate' ) ); + } + + public function test__is_considered_inactive__should_consider_user_meta() + { + Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 30); + + $user_inactive_id = $this->factory()->user->create([ + 'meta_input' => [ + 'wpvip_last_seen' => strtotime('-31 days'), + ], + ]); + + $user_active_id = $this->factory()->user->create([ + 'meta_input' => [ + 'wpvip_last_seen' => strtotime('-29 days'), + ], + ]); + + $last_seen = new User_Last_Seen(); + $last_seen->init(); + + $this->assertTrue( $last_seen->is_considered_inactive( $user_inactive_id ) ); + $this->assertFalse( $last_seen->is_considered_inactive( $user_active_id ) ); + } + + public function test__is_considered_inactive__should_return_false_if_user_meta_and_option_are_not_present() + { + Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 30); + + delete_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY ); + + $user_without_meta = $this->factory()->user->create(); + + $last_seen = new \Automattic\VIP\Security\User_Last_Seen(); + $last_seen->init(); + + $this->assertFalse( $last_seen->is_considered_inactive( $user_without_meta ) ); + } + + public function test__is_considered_inactive__should_use_release_date_option_when_user_meta_is_not_defined() + { + Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 15); + + add_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY, strtotime('-16 days') ); + + $user_without_meta = $this->factory()->user->create(); + + $last_seen = new \Automattic\VIP\Security\User_Last_Seen(); + $last_seen->init(); + + $this->assertTrue( $last_seen->is_considered_inactive( $user_without_meta ) ); + + update_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY, strtotime('-10 days') ); + + $this->assertFalse( $last_seen->is_considered_inactive( $user_without_meta ) ); + } + + public function test__determine_current_user_should_record_once_last_seen_meta() + { + Constant_Mocker::define('VIP_SECURITY_INACTIVE_USERS_ACTION', 'BLOCK' ); + Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 15); + + remove_all_filters( 'determine_current_user' ); + + $previous_last_seen = sprintf('%d', strtotime('-10 days') ); + + $user_id = $this->factory()->user->create([ + 'meta_input' => [ + 'wpvip_last_seen' => $previous_last_seen, + ], + ]); + + $last_seen = new \Automattic\VIP\Security\User_Last_Seen(); + $last_seen->init(); + + apply_filters( 'determine_current_user', $user_id, $user_id ); + + $current_last_seen = get_user_meta( $user_id, User_Last_Seen::LAST_SEEN_META_KEY, true ); + + $new_user_id = apply_filters( 'determine_current_user', $user_id, $user_id ); + + $cached_last_seen = get_user_meta( $user_id, User_Last_Seen::LAST_SEEN_META_KEY, true ); + + $this->assertTrue( $current_last_seen > $previous_last_seen ); + $this->assertSame( $current_last_seen, $cached_last_seen ); + $this->assertSame( $new_user_id, $user_id ); + } + + public function test__determine_current_user_should_return_an_error_when_user_is_inactive() + { + Constant_Mocker::define('VIP_SECURITY_INACTIVE_USERS_ACTION', 'BLOCK' ); + Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 15); + + remove_all_filters( 'determine_current_user' ); + + $user_id = $this->factory()->user->create([ + 'meta_input' => [ + 'wpvip_last_seen' => strtotime('-100 days'), + ], + ]); + + $last_seen = new \Automattic\VIP\Security\User_Last_Seen(); + $last_seen->init(); + + $user = apply_filters( 'determine_current_user', $user_id, $user_id ); + + $this->assertWPError( $user, 'Expected WP_Error object to be returned' ); + } + + public function test__authenticate_should_not_return_error_when_user_is_active() + { + Constant_Mocker::define('VIP_SECURITY_INACTIVE_USERS_ACTION', 'BLOCK' ); + Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 15); + + remove_all_filters( 'authenticate' ); + + $user_id = $this->factory()->user->create([ + 'meta_input' => [ + 'wpvip_last_seen' => strtotime('-5 days'), + ], + ]); + + $user = get_user_by( 'id', $user_id ); + + $last_seen = new \Automattic\VIP\Security\User_Last_Seen(); + $last_seen->init(); + + $new_user = apply_filters( 'authenticate', $user, $user ); + + $this->assertSame( $user->ID, $new_user->ID ); + } + + public function test__authenticate_should_return_an_error_when_user_is_inactive() + { + Constant_Mocker::define('VIP_SECURITY_INACTIVE_USERS_ACTION', 'BLOCK' ); + Constant_Mocker::define('VIP_SECURITY_CONSIDER_USERS_INACTIVE_AFTER_DAYS', 15); + + remove_all_filters( 'authenticate' ); + + $user_id = $this->factory()->user->create([ + 'meta_input' => [ + 'wpvip_last_seen' => strtotime('-100 days'), + ], + ]); + $user = get_user_by( 'id', $user_id ); + + $last_seen = new \Automattic\VIP\Security\User_Last_Seen(); + $last_seen->init(); + + $user = apply_filters( 'authenticate', $user, $user ); + + $this->assertWPError( $user, 'Expected WP_Error object to be returned' ); + } + + public function test__register_release_date_should_register_release_date_only_once() + { + Constant_Mocker::define('VIP_SECURITY_INACTIVE_USERS_ACTION', 'RECORD_LAST_SEEN' ); + + remove_all_actions( 'admin_init' ); + delete_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY ); + $last_seen = new \Automattic\VIP\Security\User_Last_Seen(); + $last_seen->register_release_date(); + + $release_date = get_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY ); + + $last_seen->register_release_date(); + + $new_release_date = get_option( User_Last_Seen::LAST_SEEN_RELEASE_DATE_TIMESTAMP_OPTION_KEY ); + + $this->assertIsNumeric( $release_date ); + $this->assertSame( $release_date, $new_release_date ); + } +}