2

So I've developed a small module that creates a new CLI to be able to change the customer password.

Here's my DigitalPianism\ChangePassword\Console\Command\ChangePassword.php class:

<?php

namespace DigitalPianism\ChangePassword\Console\Command;

use DigitalPianism\ChangePassword\Model\Customer\AccountManagement;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Magento\Customer\Model\CustomerRegistry;
use Magento\Customer\Model\Customer;
use Symfony\Component\Console\Output\OutputInterface;
use Magento\Framework\App\State as AppState;
use Exception;

/**
 * Class ChangePassword
 * @package DigitalPianism\ChangePassword\Console\Command
 */
class ChangePassword extends Command
{

    /**#@+
     * Data keys
     */
    const CUSTOMER_ID_ARGUMENT = 'customer-id';
    const CUSTOMER_PASSWORD_ARGUMENT = 'customer-password';

    /**
     * @var CustomerRegistry
     */
    private $_customerRegistry;

    /**
     * @var Customer
     */
    private $_customer;

    /**
     * @var AppState
     */
    private $_appState;

    /**
     * @var AccountManagement
     */
    private $_accountManagement;

    /**
     * @param CustomerRegistry $customerRegistry
     * @param AppState $appState
     * @param AccountManagement $accountManagement
     */
    public function __construct(
        CustomerRegistry $customerRegistry,
        AppState $appState,
        AccountManagement $accountManagement
    ) {
        $this->_accountManagement = $accountManagement;
        $this->_appState = $appState;
        $this->_customerRegistry = $customerRegistry;
        parent::__construct();
    }

    /**
     * Initialization of the command
     *
     * @return void
     */
    protected function configure()
    {
        $this->setName('customer:changepassword')
            ->setDescription('Change customer password')
            ->setDefinition($this->getOptionsList());
        parent::configure();
    }

    /**
     * Get list of arguments for the command
     *
     * @return InputArgument[]
     */
    public function getOptionsList()
    {
        return [
            new InputArgument(
                self::CUSTOMER_ID_ARGUMENT,
                InputArgument::REQUIRED,
                '(Required) Customer ID'
            ),
            new InputArgument(
                self::CUSTOMER_PASSWORD_ARGUMENT,
                InputArgument::REQUIRED,
                '(Required) Customer password'
            )
        ];
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $this->_appState->setAreaCode('adminhtml');
        $errors = $this->validate($input);
        if ($errors) {
            $output->writeln('<error>' . implode('</error>' . PHP_EOL .  '<error>', $errors) . '</error>');
            // we must have an exit code higher than zero to indicate something was wrong
            return \Magento\Framework\Console\Cli::RETURN_FAILURE;
        }

        $this->_accountManagement->replaceCustomerPassword($this->_customer, $input->getArgument(self::CUSTOMER_PASSWORD_ARGUMENT));

        $output->writeln(
            '<info>Password for customer #' . $input->getArgument(self::CUSTOMER_ID_ARGUMENT) . ' has been successfully changed</info>'
        );
    }

    /**
     * Check if all options are provided
     *
     * @param InputInterface $input
     * @return string[]
     */
    public function validate(InputInterface $input)
    {
        $errors = [];

        try {
            $this->_accountManagement->passwordStrengthCheck($input->getArgument(self::CUSTOMER_PASSWORD_ARGUMENT));
            /** @var Customer $customer */
            $this->_customer = $this->_customerRegistry->retrieve($input->getArgument(self::CUSTOMER_ID_ARGUMENT));
        } catch (Exception $e) {
            $errors[] = $e->getMessage();
        }

        return $errors;
    }
}

And here is my DigitalPianism\ChangePassword\Model\Customer\AccountManagement.php class

<?php

namespace DigitalPianism\ChangePassword\Model\Customer;

use Magento\Framework\Exception\InputException;

/**
 * Class AccountManagement
 * @package DigitalPianism\ChangePassword\Model\Customer
 */
class AccountManagement extends \Magento\Customer\Model\AccountManagement
{
    /**
     * Make sure that password complies with minimum security requirements.
     *
     * @param string $password
     * @return void
     * @throws InputException
     */
    public function passwordStrengthCheck($password)
    {
        try {
            $result = $this->checkPasswordStrength($password);
            return $result;
        } catch(InputException $e) {
            return $e->getMessage();
        }
    }

    /**
     * Replace customer password without the old password
     *
     * @param $customer
     * @param $password
     */
    public function replaceCustomerPassword($customer, $password)
    {
        $customer
            ->changePassword($password)
            ->save();
    }
}

And if you need it the di.xml :

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <type name="Magento\Framework\Console\CommandListInterface">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="change_password" xsi:type="object">DigitalPianism\ChangePassword\Console\Command\ChangePassword</item>
            </argument>
        </arguments>
    </type>

</config>

So when I run the following from the CLI:

php bin/magento customer:changepassword 3 test

It is supposed to change the password to "test" for customer with id 3.

As I'm using the core Magento password validation via my Model\Customer\AccountManagement I get the following error which is totally ok:

[Magento\Eav\Model\Entity\Attribute\Exception]

Please enter a password with at least 6 characters.

So I wrote a small unit test for that in Test\Unit\Console\Command\ChangePasswordTest.php:

<?php

namespace DigitalPianism\ChangePassword\Test\Unit\Console\Command;

use DigitalPianism\ChangePassword\Console\Command\ChangePassword;
use Symfony\Component\Console\Tester\CommandTester;

/**
 * Class ChangePasswordTest
 * @package DigitalPianism\ChangePassword\Test\Unit\Console\Command
 */
class ChangePasswordTest extends \PHPUnit_Framework_TestCase
{

    /**
     * @var ChangePassword
     */
    private $command;

    /**
     * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject
     */
    private $objectManagerMock;

    protected function setUp()
    {
        $this->objectManagerMock = $this->getMockForAbstractClass('Magento\Framework\ObjectManagerInterface');

        $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
        $this->command = $objectManager->getObject(
            'DigitalPianism\ChangePassword\Console\Command\ChangePassword',
            ['objectManager' => $this->objectManagerMock]
        );
    }

    public function testWithWrongPassword()
    {
        $tester = new CommandTester($this->command);
        $tester->execute([1, "test"]);
        $this->assertContains(
            "Please enter a password with at least",
            $tester->getDisplay()
        );
    }
}

But when I run the test I get:

Failed asserting that 'Password for customer # has been successfully changed ' contains "Please enter a password with at least".

Which means the error I get when running from the CLI is not being triggered.

I know it's a long shot as I posted quite some code but I'm wondering if that's the right way of using the CommandTester class and what am I missing here ?

4

You are not getting the real behavior because the \Magento\Framework\TestFramework\Unit\Helper\ObjectManager creates a mock for each constructor argument dependency, not the real thing.
The CommandTester is not meant for unit tests.

However, given your code is using the validation of Model\Customer\AccountManagement and given that is what you want to test, you should write an integration test, not a unit test, since unit tests do not test object collaboration, especially not with core classes.

Side note: if you want to clean up your test further, it is better use the ::class instead of string literals for class names. Also remove the \Magento\Framework\TestFramework\Unit\Helper\ObjectManager as it only adds complexity because it is so complex. This "helper" is only useful for legacy classes with dozens of dependencies. Better to avoid creating such classes when writing new code. Just mock your dependencies manually and that will help protect you from creating classes with too many dependencies.

2
  • Oh yeah definitely. My question is more regarding the different behaviors between my test and running the command from CLI. Dec 1 '16 at 9:25
  • Updated my answer.
    – Vinai
    Dec 1 '16 at 10:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.