-
dgvirtual Member
  
-
Posts: 128
Threads: 30
Joined: Jun 2016
Reputation:
7
08-05-2023, 02:41 PM
(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.
-
benedmunds CI Counsel Member
 
-
Posts: 10
Threads: 2
Joined: Nov 2014
Reputation:
1
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.
-
dgvirtual Member
  
-
Posts: 128
Threads: 30
Joined: Jun 2016
Reputation:
7
08-06-2023, 02:34 AM
(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.
-
dgvirtual Member
  
-
Posts: 128
Threads: 30
Joined: Jun 2016
Reputation:
7
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.
-
dgvirtual Member
  
-
Posts: 128
Threads: 30
Joined: Jun 2016
Reputation:
7
08-06-2023, 11:57 AM
(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
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
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.
-
dgvirtual Member
  
-
Posts: 128
Threads: 30
Joined: Jun 2016
Reputation:
7
(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  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.
-
dgvirtual Member
  
-
Posts: 128
Threads: 30
Joined: Jun 2016
Reputation:
7
08-07-2023, 02:57 AM
(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.
|