Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WooCommerce password change action. #585

Merged
merged 3 commits into from
Nov 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"phpcbf-tests": "./vendor/bin/phpcbf --standard=phpcs-test-ruleset.xml -s ./tests/",
"sniffs": "vendor/bin/phpcs --standard=phpcs-ruleset.xml -e",
"test": "\"vendor/bin/phpunit\" --coverage-text",
"test-ci": "vendor/bin/phpunit --debug --coverage-clover=coverage.xml"
"test-ci": "vendor/bin/phpunit --debug --coverage-clover=coverage.xml",
"pre-commit": [ "@compat", "@phpcbf", "@phpcbf-tests", "@phpcs-tests", "@test" ]
}
}
31 changes: 25 additions & 6 deletions lib/profile/WP_Auth0_Profile_Change_Password.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
<?php
/**
* Contains class WP_Auth0_Profile_Change_Password.
*
* @package WP-Auth0
*
* @since 3.8.0
*/

/**
* Class WP_Auth0_Profile_Change_Password.
Expand Down Expand Up @@ -27,31 +34,43 @@ public function __construct( WP_Auth0_Api_Change_Password $api_change_password )
* @codeCoverageIgnore - Tested in TestProfileChangePassword::testInitHooks()
*/
public function init() {

// Used during profile update in wp-admin.
add_action( 'user_profile_update_errors', array( $this, 'validate_new_password' ), 10, 2 );

// Used during password reset on wp-login.php
add_action( 'validate_password_reset', array( $this, 'validate_new_password' ), 10, 2 );

// Used during WooCommerce edit account save.
add_action( 'woocommerce_save_account_details_errors', array( $this, 'validate_new_password' ), 10, 2 );
}

/**
* Update the user's password at Auth0
* Hooked to: user_profile_update_errors, validate_password_reset
* IMPORTANT: Internal callback use only, do not call this function directly!
*
* @param WP_Error $errors - WP_Error object to use if validation fails.
* @param boolean|WP_User $user - Boolean update or WP_User instance, depending on action.
* @param WP_Error $errors - WP_Error object to use if validation fails.
* @param boolean|stdClass $user - Boolean update or WP_User instance, depending on action.
*
* @return boolean
*/
public function validate_new_password( $errors, $user ) {

// Exit if we're not changing the password.
if ( empty( $_POST['pass1'] ) ) {
// The pass1 key is for core WP, password_1 is WooCommerce.
if ( empty( $_POST['pass1'] ) && empty( $_POST['password_1'] ) ) {
return false;
}
$new_password = $_POST['pass1'];

$field_name = ! empty( $_POST['pass1'] ) ? 'pass1' : 'password_1';
lbalmaceda marked this conversation as resolved.
Show resolved Hide resolved
$new_password = $_POST[ $field_name ];

if ( isset( $_POST['user_id'] ) ) {
// Input field from user edit or profile update.
$wp_user_id = absint( $_POST['user_id'] );
} elseif ( is_object( $user ) && $user instanceof WP_User ) {
} elseif ( is_object( $user ) && ! empty( $user->ID ) ) {
lbalmaceda marked this conversation as resolved.
Show resolved Hide resolved
// User object passed in from an action.
$wp_user_id = absint( $user->ID );
} else {
return false;
Expand Down Expand Up @@ -83,7 +102,7 @@ public function validate_new_password( $errors, $user ) {

// Add an error message to appear at the top of the page.
$error_msg = is_string( $result ) ? $result : __( 'Password could not be updated.', 'wp-auth0' );
$errors->add( 'auth0_password', $error_msg, array( 'form-field' => 'pass1' ) );
$errors->add( 'auth0_password', $error_msg, array( 'form-field' => $field_name ) );
return false;
}
}
7 changes: 7 additions & 0 deletions lib/profile/WP_Auth0_Profile_Delete_Data.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
<?php
/**
* Contains class WP_Auth0_Profile_Delete_Data.
*
* @package WP-Auth0
*
* @since 3.8.0
*/

/**
* Class WP_Auth0_Profile_Delete_Data.
Expand Down
7 changes: 7 additions & 0 deletions lib/profile/WP_Auth0_Profile_Delete_Mfa.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
<?php
/**
* Contains class WP_Auth0_Profile_Delete_Mfa.
*
* @package WP-Auth0
*
* @since 3.8.0
*/

/**
* Class WP_Auth0_Profile_Delete_Mfa.
Expand Down
16 changes: 8 additions & 8 deletions tests/testApiChangePassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public static function setUpBeforeClass() {
}

/**
* Test the request sent by the Client Credentials call.
* Test the request sent by the change password call.
*/
public function testRequest() {
$this->startHttpHalting();
Expand Down Expand Up @@ -100,42 +100,42 @@ public function testRequest() {
}

/**
* Test a basic Delete MFA call against a mock API server.
* Test a basic change password call against a mock API server.
*/
public function testCall() {
$this->startHttpMocking();
self::$options->set( 'domain', self::TEST_DOMAIN );

// Mock for a successful API call.
$delete_mfa = $this->getStub( true );
$change_password = $this->getStub( true );

// 1. Make sure that a transport returns the default failed response and logs an error.
$this->http_request_type = 'wp_error';
$this->assertFalse( $delete_mfa->call( uniqid(), uniqid() ) );
$this->assertFalse( $change_password->call( uniqid(), uniqid() ) );
$log = self::$error_log->get();
$this->assertCount( 1, $log );
$this->assertEquals( 'Caught WP_Error.', $log[0]['message'] );

// 2. Make sure that an Auth0 API error returns the default failed response and logs an error.
$this->http_request_type = 'auth0_api_error';
$this->assertFalse( $delete_mfa->call( uniqid(), uniqid() ) );
$this->assertFalse( $change_password->call( uniqid(), uniqid() ) );
$log = self::$error_log->get();
$this->assertCount( 2, $log );
$this->assertEquals( 'caught_api_error', $log[0]['code'] );

// 4. Make sure that a weak password error returns the correct message.
// 3. Make sure that a weak password error returns the correct message.
$this->http_request_type = 'failed_weak_password';
$this->assertEquals(
'Password is too weak, please choose a different one.',
$delete_mfa->call( uniqid(), uniqid() )
$change_password->call( uniqid(), uniqid() )
);
$log = self::$error_log->get();
$this->assertCount( 3, $log );
$this->assertEquals( '400', $log[0]['code'] );

// 4. Make sure it succeeds.
$this->http_request_type = 'success_empty_body';
$this->assertTrue( $delete_mfa->call( uniqid(), uniqid() ) );
$this->assertTrue( $change_password->call( uniqid(), uniqid() ) );
$this->assertCount( 3, self::$error_log->get() );
}

Expand Down
179 changes: 151 additions & 28 deletions tests/testProfileChangePassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public static function setUpBeforeClass() {
}

/**
* Test that correct hooks are loaded
* Test that correct hooks are loaded.
*/
public function testInitHooks() {

Expand All @@ -81,58 +81,181 @@ public function testInitHooks() {
'accepted_args' => 2,
],
];
// Same method hooked to both actions.
$this->assertHooked( 'user_profile_update_errors', 'WP_Auth0_Profile_Change_Password', $expect_hooked );
$this->assertHooked( 'validate_password_reset', 'WP_Auth0_Profile_Change_Password', $expect_hooked );
// Same method hooked to all 3 actions.
$class_name = 'WP_Auth0_Profile_Change_Password';
$this->assertHooked( 'user_profile_update_errors', $class_name, $expect_hooked );
$this->assertHooked( 'validate_password_reset', $class_name, $expect_hooked );
$this->assertHooked( 'woocommerce_save_account_details_errors', $class_name, $expect_hooked );
}

/**
* Test that the validate_new_password method works as expected.
* Test that password update succeeds when run in the user_profile_update_errors hook.
*/
public function testValidateNewPassword() {
public function testSuccessfulPasswordChangeDuringProfileUpdate() {
$user_id = $this->createUser()->ID;
$errors = new WP_Error();
$password = uniqid();

// Core WP profile update fields.
$_POST['pass1'] = $password;
$_POST['pass2'] = $password;
$_POST['user_id'] = $user_id;

// API call mocked to succeed.
$change_password = $this->getStub( true );

// Store userinfo for a DB strategy user.
$this->storeAuth0Data( $user_id, 'auth0' );

$this->assertTrue( $change_password->validate_new_password( $errors, true ) );
$this->assertEquals( $password, $_POST['pass1'] );
$this->assertEquals( $password, $_POST['pass2'] );
$this->assertEmpty( $errors->get_error_messages() );
}

/**
* Test that password update succeeds when run in the validate_password_reset hook.
*/
public function testSuccessfulPasswordChangeDuringPasswordReset() {
$password = uniqid();
$user = $this->createUser();
$errors = new WP_Error();

// API call mocked to succeed.
$change_password = $this->getStub( true );

// Core WP form fields sent for password update.
$_POST['pass1'] = $password;
$_POST['pass2'] = $password;

// Store userinfo for a DB strategy user.
$this->storeAuth0Data( $user->ID, 'auth0' );

$this->assertTrue( $change_password->validate_new_password( $errors, $user ) );
$this->assertEquals( $password, $_POST['pass1'] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this change in anyway when validate_new_password is called? If that doesn't happen, what I see is:

var a = 1;
assert(a).equals(1);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this change in anyway when validate_new_password is called?

Yes, alters the global var

$this->assertEquals( $password, $_POST['pass2'] );
$this->assertEmpty( $errors->get_error_messages() );
}

/**
* Test that password update succeeds when run in the woocommerce_save_account_details_errors hook.
*/
public function testSuccessfulPasswordChangeDuringWooAccountEdit() {
$user = $this->createUser();
$errors = new WP_Error();

$user_data = $this->createUser();
$user_id = $user_data->ID;
$user_obj = get_user_by( 'id', $user_id );
// API call mocked to succeed.
$change_password = $this->getStub( true );

// Create a stub for the WP_Auth0_Api_Change_Password class.
$mock_api_test_password = $this
->getMockBuilder( WP_Auth0_Api_Change_Password::class )
->setMethods( [ 'call' ] )
->setConstructorArgs( [ self::$options, self::$api_client_creds ] )
->getMock();
$mock_api_test_password->method( 'call' )->willReturn( true, false );
// WooCommerce form fields sent for password update.
$_POST['password_1'] = uniqid();

// Store userinfo for a DB strategy user.
$this->storeAuth0Data( $user->ID, 'auth0' );

$this->assertTrue( $change_password->validate_new_password( $errors, $user ) );
$this->assertEmpty( $errors->get_error_messages() );
}

/**
* Test that empty password fields will skip password update.
*/
public function testThatEmptyPasswordFieldSkipsUpdate() {
$user_id = $this->createUser()->ID;
$errors = new WP_Error();

$change_password = new WP_Auth0_Profile_Change_Password( $mock_api_test_password );
// Provide everything except a password field.
$change_password = $this->getStub( true );
$_POST['user_id'] = $user_id;
$this->storeAuth0Data( $user_id );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testThatMissingUserDataSkipsUpdate

this is not empty, or what do you mean by "missing user data"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either missing form field with a user ID or missing when passed in by WP.


// Call should fail because of a missing password.
$this->assertFalse( $change_password->validate_new_password( $errors, false ) );
$this->assertEmpty( $errors->get_error_messages() );
}

$_POST['pass1'] = uniqid();
/**
* Test that the password update is skipped if no user record is provided.
*/
public function testThatMissingUserRecordSkipsUpdate() {
$user_id = $this->createUser()->ID;
$errors = new WP_Error();

// Provide everything except a user record.
$change_password = $this->getStub( true );
$_POST['pass1'] = uniqid();
$this->storeAuth0Data( $user_id );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"missing user record" but here you're passing a user id, from a user that was created right at the beginning of the test. What I am missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I am missing?

A lot 😆 ... $this->storeAuth0Data() creates meta data for some user, $_POST['user_id'] is a global POST var that clues the method in that there is a user to change.


// Call should fail with a password because of a missing user ID.
$this->assertFalse( $change_password->validate_new_password( $errors, false ) );
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you need to do $this->assertEmpty( $errors->get_error_messages() ); here, as done on the test above? applies to other tests as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to make sure in some cases. I added it where I thought it was needed.


// Call should fail with a user object or user_id in $_POST because of no Auth0 data stored.
$this->assertFalse( $change_password->validate_new_password( $errors, $user_obj ) );
/**
* Test that a user without Auth0 data skips update.
*/
public function testThatNonAuth0UserSkipsUpdate() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sug. rename to testThatMissingAuth0UserDataSkipsUpdate

$user_id = $this->createUser()->ID;
$errors = new WP_Error();

// Provide everything except Auth0 userinfo.
$change_password = $this->getStub( true );
$_POST['pass1'] = uniqid();
$_POST['user_id'] = $user_id;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should you call something more explicit like $this->storeAuth0Data( null );?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No

$this->assertFalse( $change_password->validate_new_password( $errors, false ) );
}

/**
* Test that a user with a non-DB strategy skips update.
*/
public function testThatNonDbStrategySkipsUpdate() {
$user_id = $this->createUser()->ID;
$errors = new WP_Error();

$this->storeAuth0Data( $user_id, 'not-auth0' );
// Provide everything except Auth0 userinfo for a DB user.
$change_password = $this->getStub( true );
$_POST['pass1'] = uniqid();
$_POST['user_id'] = $user_id;
$this->storeAuth0Data( $user_id, 'not-a-db-strategy' );

// Call should fail with Auth0 data stored because of a wrong strategy.
$this->assertFalse( $change_password->validate_new_password( $errors, false ) );
}

$this->storeAuth0Data( $user_id );
/**
* Test that an API failure will set UI banners and cancel the password change.
*/
public function testThatApiFailureSetsErrorsUnsetsPassword() {
$user_id = $this->createUser()->ID;
$errors = new WP_Error();

// API call mocked to fail.
$change_password = $this->getStub( false );

// Call should succeed with a mocked API.
$this->assertTrue( $change_password->validate_new_password( $errors, false ) );
// Setup correct user data.
$_POST['pass1'] = uniqid();
lbalmaceda marked this conversation as resolved.
Show resolved Hide resolved
$_POST['pass2'] = $_POST['pass1'];
$_POST['user_id'] = $user_id;
$this->storeAuth0Data( $user_id );

// Call should fail on the second call with a mocked API.
$this->assertFalse( $change_password->validate_new_password( $errors, false ) );
$this->assertEquals( 'Password could not be updated.', $errors->errors['auth0_password'][0] );
$this->assertEquals( 'pass1', $errors->error_data['auth0_password']['form-field'] );
$this->assertFalse( isset( $_POST['pass1'] ) );
lbalmaceda marked this conversation as resolved.
Show resolved Hide resolved
$this->assertFalse( isset( $_POST['pass2'] ) );
}

/**
* Get an API stub set to pass or fail.
*
* @param boolean $success - True for the API call to succeed, false for it to fail.
*
* @return WP_Auth0_Profile_Change_Password
*/
public function getStub( $success ) {
$mock_api_test_password = $this
->getMockBuilder( WP_Auth0_Api_Change_Password::class )
->setMethods( [ 'call' ] )
->setConstructorArgs( [ self::$options, self::$api_client_creds ] )
->getMock();
$mock_api_test_password->method( 'call' )->willReturn( $success );
return new WP_Auth0_Profile_Change_Password( $mock_api_test_password );
}
}