Welcome Guest, Not a member yet? Register   Sign In
phpunit testing routes only available after login
#1

(This post was last modified: 08-05-2023, 02:49 PM by dgvirtual. Edit Reason: update )

I need to create unit tests for controllers / routes that are only accessible for logged in users. I am using IonAuth.
PHP Code:
public function testValidateField()
{
    $route route_to('invoice_validate_field''name');
    $getArray = ['name' => 'Name Surname'];
    $result $this->call('get'$route$getArray);
    $this->assertTrue($result->isOK());
    $result->assertRedirectTo('login');

This works, but, as you can see, I can only check if the route gets redirected, because the route is only available for logged in users.

I am using IonAuth for authentication. The login check is performed by a filter.

How do I imitate logged in user in this case? How do I log in?

I see Shield has AuthenticationTesting trait for that. But how do I deal with IonAuth here? Maybe @benedmunds could suggest something?
==

Donatas G.
Reply
#2

There’s not a great way. IonAuth really wasn’t the most testable bit of code since testing wasn’t big in CodeIgniter when it was originally written.

To do this you’d either need to mock the IonAuth->loggedIn() method.
- Ben Edmunds
 CI Counsel Member  |  @benedmunds
 Ion Auth  |  Securing PHP Apps
Reply
#3

Yes, it is difficult to write tests when certification is involved.
It is a kind of integration test, not a unit test.

After all, you need to pre-modify the state so that the filter determines that the user is authenticated.

See code in Shield as a reference: https://github.com/codeigniter4/shield/b...ng.php#L24
Reply
#4

(This post was last modified: 08-06-2023, 02:37 AM by dgvirtual. Edit Reason: code correction )

(08-05-2023, 03:56 PM)benedmunds Wrote: There’s not a great way. IonAuth really wasn’t the most testable bit of code since testing wasn’t big in CodeIgniter when it was originally written.

To do this you’d either need to mock the IonAuth->loggedIn() method.


I use IonAuth as a service, defined in my Config/Services.php, here is the method that defines the service:

PHP Code:
public static function ionAuth($getShared true)
{
    if ($getShared) {
        return static::getSharedInstance('ionAuth');
    }
    return new \IonAuth\Libraries\IonAuth();

So I was able to use the mocking services functionality as described in CI4 documentation.



With some help (and much misdirection) from Google Bard I came up with this thing that works:
PHP Code:
public function testValidateField()
{
    // case of user not being logged in:
    $route route_to('invoice_validate_field''date');
    $getArray = ['date' => '2023-01-03'];
    $result $this->call('get'$route$getArray);
    $this->assertTrue($result->isOK());
    $result->assertRedirect();
    $result->assertRedirectTo('login');

    // mock login
    $ionAuth $this->getMockBuilder('IonAuth\Libraries\IonAuth')
        ->setMethods(['loggedIn'])
        ->getMock();
    $ionAuth->expects($this->exactly(2)) // will have two checks
        ->method('loggedIn')
        ->willReturn(true);
      
    Services
::injectMock('ionAuth'$ionAuth);
  
    
// test without a validation error:
    // this->sess is an array of session variables, perhaps not actually needed here
    $result $this->withSession($this->sess)->call('get'$route$getArray);

    // assert that we no longer get redirect to login page:
    $this->assertFalse($result->isRedirect());
    // assert that the result does not include '„' symbol, which, in Lithuanian language,
    // surrounds the field name in error messages (alternative to checking for empty string)
    $result->assertDontSee('„');

    // lets introduce a wrong date to trigger validation error:
    $getArray = ['date' => '2023'];
    $result $this->withSession($this->sess)->call('get'$route$getArray);
    // again, no redirect here:
    $this->assertFalse($result->isRedirect());
    
    
// assert that we see part of Lithuanian error string:
    $result->assertSee('turi būti tiksliai 10 ženklų ilgio.');


Hope this will be useful for someone.


Thanks @benedmunds  and @kenjis for helping me reason through this.
==

Donatas G.
Reply
#5

And then I ran into another issue: testing post forms with CSRF protection enabled. So, I have scrf enabled on all site through the Config/Filters.php $globals array.
I get a 302 error if I run the post without csrf_token:

PHP Code:
public function testAdd()
{
    // Case1. Display add form
    $route route_to('invoice_add');
    //<skipping first part>

    // Case2. Save submitted data
    $postArray = [
        'client_id' => '57',
        'month' => date('Y-m'),
        'service[]' => 'Konsultacijos',
        'measure[]' => 'vnt.',
        'quantity[]' => '2',
        'price[]' => '20',
        'category[]' => '17',
    ];
    $ionAuth $this->getMockBuilder('IonAuth\Libraries\IonAuth')
        ->setMethods(['loggedIn'])
        ->getMock();
    $ionAuth->method('loggedIn')->willReturn(true);
    Services::injectMock('ionAuth'$ionAuth);

    $result $this->withSession($this->sess)->call('post'$route);

    //assert checks should be here, but for now only dump $result
    var_dump($result);

Is there, perhaps, a smart way to deal with CSRF in tests?
==

Donatas G.
Reply
#6

(This post was last modified: 08-06-2023, 12:56 PM by dgvirtual.)

And again, answering my own question, CI4 class mocking helps (only posting the Case2 test part):



PHP Code:
// Case2. form submitted with errors
$security $this->getMockBuilder('CodeIgniter\Security\Security')
    ->disableOriginalConstructor()->setMethods(['verify''getHash'])
    ->getMock();
// this is used in csrf_hash() to generate the hash for the forms
$security->method('getHash')->willReturn('15872aabb');
// this is used in the filter service, will claim the verification succeeded
$security->method('verify')->willReturn(true);

$postArray = [
    'client_id' => '57',
    'month' => date('Y-m'),
    // this field should be filled, so form should reload
    // with error expected to be displayed
    'service[]' => '',
    'measure[]' => 'vnt.',
    'quantity[]' => '2',
    'price[]' => '20',
    'category[]' => '17',
];
$ionAuth $this->getMockBuilder('IonAuth\Libraries\IonAuth')
    ->setMethods(['loggedIn'])
    ->getMock();
$ionAuth->method('loggedIn')->willReturn(true);
Services::injectMock('ionAuth'$ionAuth);
Services::injectMock('security'$security);

$result $this->withSession($this->sess)->call('post'$route$postArray);

//run through assertions now:
$result->assertNotRedirect();
$this->assertTrue($result->isOK());
// check if one of the headings are seen: they are
$result->assertSee('už kurį sąskaita');
//check if error message is displayed on empty field: it is not, for some reason :(
//$result->assertSee('yra privalomas');
// check if privacy policy link is there on page:
$result->assertSee('Privatumo politika'); 

So, as far as csrf mocking goes, this works.


Problem is, I am not getting error messages on unfilled fields, as I do on the live site. What could be the issue here?

OK, so another reply to myself, and also related to tests.

So, the first problem, why the errors were (still guessing) not showing, was this. In my controller add() method I did a check if there were values sent via post with the non-CI method
PHP Code:
if (!empty($_POST)) {} 
And it appears that this array is not filled with values when testing and mocking post request, so my $postArray array had no effect. As a result, the form simply reloaded (and, as I WAS using the $this->request->getPost() method later in the form, some of the values did get reassigned to the form.
So I changed the $_POST into $this->request->getPost() in the if check.
However, my form has (or can have) multiple fields with the same name (like 'services[]' which, when received through a real post request, is treated as array. So in my code I have
PHP Code:
foreach ($this->request->getPost('service[]') as $key => $item) {} 
control structure to cycle through each service and create a multidimentional data array.
However, the call() method of feature test case seems to treat the keys with [] differently: as simple values and not arrays. Therefore, next thing was an error on the foreach loop complaining that I supplied it with a string and not an array Sad
So here is what I got when I var_dumped the newly created post array:

Code:
array(7) {
  'client_id' =>
  string(2) "57"
  'month' =>
  string(7) "2023-08"
  'service[]' =>
  string(12) "Konsultacija"
  'measure[]' =>
  string(4) "vnt."
  'quantity[]' =>
  string(1) "2"
  'price[]' =>
  string(2) "20"
  'category[]' =>
  string(2) "17"
}

And var_dump with real post data looks like this:
Code:
array (size=8)
  'submit' => string 'Įrašyti' (length=9)
  'client_id' => string '57' (length=2)
  'month' => string '2023-07' (length=7)
  'service' =>
    array (size=1)
      0 => string 'Psichoterapinės konsultacijos' (length=30)
  'measure' =>
    array (size=1)
      0 => string 'vnt.' (length=4)
  'quantity' =>
    array (size=1)
      0 => string '3' (length=1)
  'price' =>
    array (size=1)
      0 => string '4' (length=1)
  'category' =>
    array (size=1)
      0 => string '1' (length=1)

If I try to modify the $postArray, I do not go very far; this in the test file:
PHP Code:
$postArray = [
    'client_id' => '57',
    'month' => date('Y-m'),
    'service[]' => ['Konsultacija'], // this is an error, so errors should be displayed...
    'measure[]' => ['vnt.'],
    'quantity[]' => ['2'],
    'price[]' => ['20'],
    'category[]' => ['17'],
]; 

results in this when var_dumped from the controller being tested:

Code:
array(7) {
  'client_id' =>
  string(2) "57"
  'month' =>
  string(7) "2023-08"
  'service[]' =>
  NULL
  'measure[]' =>
  NULL
  'quantity[]' =>
  NULL
  'price[]' =>
  NULL
  'category[]' =>
  NULL
}
Removing [] from variable names does not help either (and my foreach loop has to use variable name with [] to be able to grab real post data).
So, it looks like the CI4 Feature test case would benefit from more sophistication when dealing with post data...

I am not quite sure how to proceed from here. Any suggestions?
==

Donatas G.
Reply
#7

service[] is not an array key.

Try $this->request->getPost('service')
Reply
#8

(08-06-2023, 05:50 PM)kenjis Wrote: service[] is not an array key.

Try $this->request->getPost('service')

Thanks @kenjis, actually it started to work Smile The post array now looks like this:

PHP Code:
$postArray = [
    'client_id' => '57',
    'month' => date('Y-m-d'),
    'service' => ['Konsultacija'], 
    'measure' => ['vnt.'],
    'quantity' => ['2'],
    'price' => ['20'],
    'category' => ['17'],
]; 
and it started to work after I removed the [] from array keys in my code.

Am I correct to assume that in case of successful form submission, when the page gets redirected afterwards, I can only check for the redirect response, and not the content of the page that should be loaded afterwards? In that case, it is all working fine!
==

Donatas G.
Reply
#9

Yes, we can test only one request and the response at a time.
When the page gets redirected, the browser will send a new request and gets the page you see.

So if you want to test the page, you need to send another request in the test.
You need to do what the browser normally does in the test code.
To actually do this is tedious because you must write the code to make the state like the session state, etc., the same. Therefore, I don't recommend it.
Reply
#10

(This post was last modified: 08-07-2023, 03:22 AM by dgvirtual.)

(08-07-2023, 02:25 AM)kenjis Wrote: Yes, we can test only one request and the response at a time.
When the page gets redirected, the browser will send a new request and gets the page you see.

So if you want to test the page, you need to send another request in the test.
You need to do what the browser normally does in the test code.
To actually do this is tedious because you must write the code to make the state like the session state, etc., the same. Therefore, I don't recommend it.
Thanks @kenjis.

I must have discovered a bug in CI4... when run through tests the function in the code being tested uri_string() (and the underlying Services::request()->getPath(); ) somehow omits the last segment of the uri. Same function when used in the test itself returns the full uri...

Code in test:
PHP Code:
$route route_to('invoice_add'); //(resolves to invoices/add);
$result $this->call('post'$route$postArray); 
The route resolves correctly (and I get the page I want to test). However, I have a breadcrumbs library that uses uri_string() to determine what breadcrumbs to show, so it shows the breadcrumb for invoices/ page.
==

Donatas G.
Reply




Theme © iAndrew 2016 - Forum software by © MyBB