Skip to content

Commit

Permalink
Add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
luismulinari committed Aug 21, 2023
1 parent df19032 commit 267e26f
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 24 deletions.
51 changes: 27 additions & 24 deletions security/user-last-seen.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ) );
Expand All @@ -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 ) {
Expand All @@ -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 );

Expand All @@ -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', __( '<strong>Error</strong>: 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();
Expand All @@ -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;
}
Expand All @@ -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( '<span>%s</span>', esc_html__( $formatted_date ) );
}

Expand All @@ -135,7 +140,7 @@ function add_last_seen_column_date( $default, $column_name, $user_id ) {
return sprintf( '<span class="wp-ui-text-notification">%s</span>' . $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(
Expand Down Expand Up @@ -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' ) {
Expand Down Expand Up @@ -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();
Expand All @@ -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';
}
}
219 changes: 219 additions & 0 deletions tests/security/test-user-last-seen.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<?php

namespace Automattic\VIP\Security;

use Automattic\Test\Constant_Mocker;
use Automattic\VIP\Security\User_Last_Seen;
use WP_UnitTestCase;

require_once __DIR__ . '/../../security/user-last-seen.php';

class User_Last_Seen_Test extends WP_UnitTestCase {
public function setUp(): void {
parent::setUp();

Constant_Mocker::clear();
}

public function tearDown(): void {
Constant_Mocker::clear();
parent::tearDown();
}

public function test__should_not_load_actions_and_filters_when_env_vars_are_not_defined() {
Constant_Mocker::undefine( 'VIP_SECURITY_INACTIVE_USERS_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__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 );
}
}

0 comments on commit 267e26f

Please sign in to comment.