4

I have a method that makes a call to a builtin PHP function, openssl_random_pseudo_bytes.

public function generateRandomBytes()
{
    $crypto_secure = TRUE;

    // $crypto_secure is passed by reference and will be set to FALSE by
    // openssl_random_pseudo_bytes if it uses an insecure algorithm
    $random_bytes = openssl_random_pseudo_bytes(16, $crypto_secure);
    if (!$crypto_secure)
    {
        throw new Security_Exception('Random bytes not generated by a cryptographically secure PRNG algorithm');
    }
    return $random_bytes;
}

I have one PHPUnit test case to test this method (all it does is verify that the randomly generated string is 16 bytes long).

public function testRandomBytesLength()
{
    $myclass = new MyClass();

    $this->assertEquals(16, strlen($myclass->generateRandomBytes()));
}

My question is, how do I test the case in which $crypto_secure is FALSE and an exception must be thrown? Since this value is passed in and modified as a reference to openssl_random_pseudo_bytes, I'm not sure how I could get test coverage for this execution path. My first thought was that maybe there's a php.ini configuration that I could use to force openssl_random_pseudo_bytes to use a cryptographically insecure algorithm (via ini_set in the test case). Any suggestions?

rurouniwallace
  • 2,027
  • 6
  • 25
  • 47
  • 1
    A unit test is a blackbox. You should not care what a class is internally doing. Using `crypto_secure` is a config of the call, yet your `MyClass` is abstracting its usage away. You don't test for config / internals. You set `crypto_secure` to be true. Why would you ever set it to false in your case? Just for a contrived test case? Can you please explicate what you want to test? As this reads as an xy-problem. – k0pernikus Sep 27 '18 at 16:04
  • 1
    @k0pernikus I'm not sure what you mean? `crypto_secure` isn't a configuration of the call, it is a flag that openssl_pseudo_random_bytes uses to notify the caller whether or not it used a secure algorithm to generate the random bytes. If the algorithm used to generate the random numbers was insecure, openssl_pseudo_random_bytes will set `$crypto_secure` to `FALSE`. – rurouniwallace Sep 27 '18 at 16:53
  • My bad, I read it as a function argument. Now I see why you are passing it in as a variable and then want to check it. I still maintain that you do not need to create a unit test for your check though. You are fine by just throwing the exception. That being said, it's also a poster-child example of why side-effects are bad for unit testing. – k0pernikus Sep 27 '18 at 16:57
  • 1
    @k0pernikus It's an easy mistake to make since it's not a common pattern in PHP (one of my co-workers had the same confusion during code review :) And yea I agree this is a really bad pattern. Hopefully a future version of PHP will have a more TDD-friendly alternative – rurouniwallace Sep 27 '18 at 17:04

2 Answers2

5

One option is to abstract your code out so you can mock the return value of the openssl method:

public function generateRandomBytes()
{
    $crypto_secure = TRUE;
    $random_bytes = $this->randomPseudoBytes(16, $crypto_secure);
    if (!$crypto_secure)
    {
        throw new Security_Exception('Random bytes not generated by a cryptographically secure PRNG algorithm');
    }
    return $random_bytes;
}

protected function randomPseudoBytes($length, &$crypto_secure)
{
    return openssl_random_pseudo_bytes(16, $crypto_secure);
}

Then you can control the wrapper around the core function to test how your code reacts to it changing:

/**
 * @expectedException Security_Exception
 * @expectedExceptionMessage Random bytes not generated by a cryptographically secure PRNG algorithm
 */
public function testCryptoIsNotSecure()
{
    $myclass = $this->getMockBuilder(MyClass::class)->setMethods(['randomPseudoBytes'])->getMock();

    $myclass->expects($this->once())
        ->method('randomPseudoBytes')
        ->will($this->returnCallback(function ($length, &$secure) {
            // Mock variable assignment via reference
            $secure = false;
        });

    $myclass->generateRandomBytes();
}
k0pernikus
  • 60,309
  • 67
  • 216
  • 347
scrowler
  • 24,273
  • 9
  • 60
  • 92
  • 3
    While this one appears to be exactly what the OP wants, I argue: This doesn't test anything meaningful. A unit test should not mock internal protected functions / assignments. A unit test should test the public interface, and anything internally should be deemed a black box. – k0pernikus Sep 27 '18 at 16:12
  • Yep I agree but it can be useful as a reference – scrowler Sep 27 '18 at 16:16
  • If you add a test that don't mock the `randomPseudoBytes` method, will you have tested the normal behaviour of the `generateRandomBytes` method? – A.L Sep 27 '18 at 16:29
  • @A.L yeah. You could argue this is over engineering though but it does let you test the side effects in your own code – scrowler Sep 27 '18 at 16:30
  • 3
    You could also argue that the exception is part of your public API so worth testing – scrowler Sep 27 '18 at 16:31
  • 2
    I like this approach. Using a return callback to modify the reference makes sense. I agree with what @k0pernikus said though, so I'll make one modification: instead of making it a protected method of MyClass, I think it'd make sense to put generateRandomBytes into its own external helper class and inject an instance of it as a dependency into MyClass (my general rule is whenever you find yourself needing to mock a protected method, export it to its own helper class). Thanks for the input, everyone! – rurouniwallace Sep 27 '18 at 17:15
1

This answer below was written under the assumption that openssl-random-pseudo-bytes takes two immubtable arguments. Instead, the second argument is passed by reference and gives feedback if the random bytes were created by a strong algorithm. Given that information, the answer provided by Robbie Averill is a valid approach as one has to deal with basically two return-statements and a side-effect, which inherently complicate unit-testing.


You don't need your Security exception in your case.

You want to wrap openssl_random_pseudo_bytes into your own custom function and you want to hardcode the length to 16 chars and to always call openssl_random_pseudo_bytes with true. Hence you can write your class as:

class MyClass
{
    public function generateRandomBytes()
    {
        return openssl_random_pseudo_bytes(16, true);
    }
}

The only meaninful test here is to check that the returned string length is of 16 chars. And you have covered that case.


In order to show the needlessness of the exception throwing, you rather would inject the flag in the constructor or as a parameter:

class MyClass
{
    /**
     * @var bool
     */
    private $beSecure;

    public function __construct(bool $beSecure)
    {
        $this->beSecure = $beSecure;
    }


    /**
     * @return string
     * @throws Exception
     */
    public function generateRandomBytes(): string
    {
        if (!$this->beSecure) {
            // will always throw if false is injected, why would we do that?
            throw new Exception("I AM NOT SECURE!");
        }

        return openssl_random_pseudo_bytes(16, true);
    }
}

In your unit tests you can now create two test, one for the secure case, and one for the insecure case, yet why would you ever want to inject false to that class? It would then always fail.

k0pernikus
  • 60,309
  • 67
  • 216
  • 347
  • 3
    But `openssl_random_pseudo_bytes` sets the second parameter, you know? (I mean, it takes it by reference and potentially modifies it.) – Don't Panic Sep 27 '18 at 16:35
  • @Don'tPanic Good point. Then it may actually make sense to check for that, yet one also ought to note: `It's rare for [that boolean] to be FALSE, but some systems may be broken or old.` – k0pernikus Sep 27 '18 at 16:53
  • Yeah, definitely. It seems like the question is more about some way to mock an old, broken system in the test to verify that the exception is thrown in that case. I certainly don't know how to do that, though. – Don't Panic Sep 27 '18 at 17:01