code prettify

Showing posts with label codeigniter. Show all posts
Showing posts with label codeigniter. Show all posts

Sunday, 27 March 2016

Testing APIs in CodeIgniter 2.x using phpunit and Guzzle Http client

Sometimes we have apis implemented in our application and there are different levels at which these can be tested.

1. Unit tested at model level to check the logic is working fine
2. Tested at API call level to ascertain whether all the apis as expected are working and are returning data as expected.


Today, we will be learning how to test APIs in CodeIgniter 2.x version using phpunit and Guzzle Http client.

Basically Guzzle Http client is a client used to make http client requests.

Ref: https://github.com/guzzle/guzzle

"Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and trivial to integrate with web services."

We will basically be creating an API for a blog so as to get blog posts and add blog post. When testing APIs, we are basically testing the output format, expected fields in the output and their data type and value if needed.

Sample code can be found at git repository here: https://github.com/satejkumar/CodeIgniter_API_Testing

We are using composer to install the dependencies: phpunit and Guzzle http client.

So download the latest CodeIgniter 2.x version and create a composer.json file with the following content:

{
    "require": {
        "guzzlehttp/guzzle": "^6.1",
        "phpunit/phpunit": "*",
        "phpunit/php-invoker": "*",
        "phpunit/dbunit": "*"
    }
}

and then install the dependencies using the below command:

$ composer install

Then we create our test file inside application/tests/ folder called PostsTest.php

All the dependencies are included by the autoload file.

require('../../vendor/autoload.php');

Before doing each test case, we setup the Guzzle Http client and set the base uri of the API request as below inside setUp() function which is called before each test function call.

protected function setUp()
{
        $this->client = new GuzzleHttp\Client([
            'base_uri' => 'http://localhost/'
        ]);
}

One function is to test get post API request, so we are making a get request to url http://localhost/posts/index/1 to get the first post and are expecting a json content and the json content should have some fields and should have an id value of 1.

public function testGet_ValidInput_PostObject()
{
        $response = $this->client->get('posts/index/1');
        $this->assertEquals(200, $response->getStatusCode());

        $data = json_decode($response->getBody(), true);

        $this->assertArrayHasKey('id', $data);
        $this->assertArrayHasKey('title', $data);
        $this->assertArrayHasKey('content', $data);
        $this->assertArrayHasKey('author_id', $data);
        $this->assertArrayHasKey('emails_sent', $data);
        $this->assertArrayHasKey('created_at', $data);
        $this->assertArrayHasKey('updated_at', $data);
        $this->assertEquals(1, $data['id']);
}

So we are using assertArrayHasKey() function to check if a particular field is present in the data set and assertEquals() to check for value.

Another function is to test POST request to create a new blog post.

public function testPost_NewBlog_Post()
{
        $response = $this->client->post('posts/index', [
            'form_params' => [
                'title' => 'My Random Post',
                'content' => 'Content',
                'author_id' => 1
            ]
        ]);

        $this->assertEquals(200, $response->getStatusCode());
        $data = json_decode($response->getBody(), true);
        $this->assertGreaterThanOrEqual(1, $data['id']);
}

In the above function we are creating a post request to the url http://localhost/posts/index with post parameters and are then expecting a 200 status code output and a json content with a field "id" which should have an integer value greater than or equal to 1.

Now that we are done with the tests, we can run them from the project root directory using below command:

$ ./vendor/bin/phpunit application/tests/

Basically phpunit and Guzzle Http client help us in making requests and verify responses thus enabling us to test APIs.

Ref: https://ole.michelsen.dk/blog/testing-your-api-with-phpunit.html
Excerpt: "Remember it's just as important to test failing/edge cases as testing when things go well. Also you should run these tests against an isolated testing environment if they modify your data."

Sunday, 6 September 2015

Session logout fix with CodeIgniter framework when dealing with ajax requests and iframes in SPA applications

Recently, we had an issue in our application which is a single page application(SPA). We have all the functionality in a single page and most of the data and view changes happen through ajax requests and iframe page displays.
We are using CodeIgniter framework for this application. So sometimes what happened was the user was logged out while accessing the application and this occurred once a while for 1 or 2 users, not a reproducible case one would say.
Before starting on the case, I would like to specify the steps which CodeIgniter takes for checking session for any request.
Steps:
1. Read session cookie sent from browser.
2. Read session cookie process proceeds with below step checks (If any one step fails below, it results in create one session cookie)
2a. Checks if session cookie is present
2b. Checks if it is a valid session cookie by using it's decryption algorithm check
2c. If  valid session, then check if it is expired
2d. Check for config related options if enabled like check ip, check useragent etc.
2e. If database enabled then check session is present in database
3. If a valid cookie is found, then check if it needs update since its last activity update which is determined by below condition, update database if enabled with updated cookie and send the updated cookie (even if not updated in database) to browser.
($this->userdata['last_activity'] + $this->sess_time_to_update) < $this->now
The problem which occurs is related to step 3 when update of session cookie happens.
There are two versions of CodeIgniter I would like to discuss here for step 3:
1. CodeIgniter Version 2.2.0: Session update happens for all requests that is page reloads and ajax requests.
2. CodeIgniter Version 2.2.1 and above: Session update happens only for page reload requests and not for ajax requests.
So for case 1, if the issue would occur if there are many ajax requests happening from browser at same time and for case 2, it would occur if you are making multiple iframe requests at same time from SPA application.
Note this issue happens only if there are simultaneous requests happening related to respective versions as described above and the server detects a session cookie update case. Given that these are the two conditions which need to be satisfied for the issue to occur, it happens sometimes(very rarely).
Request flow and session cookie setting process flow
Explanation:
Let me explain the issue. I will be using the term requests instead of the respective detailed requests for respective CodeIgniter versions.
So lets say in our SPA application, "A" is the session cookie value stored in the browser and two requests R1 and R2 happen at same time (both will have cookie request value "A") and reach the CodeIgniter server at same time.
Note the session value in database is "A" which must match browser cookie one.
Lets say R1 is first processed by the server. So server checks it is a valid cookie and detects that it needs an update. So it generates a random cookie "B" and updates database session where old session is A. So in database, new value is now "B" and then sends updated cookie value to browser. So now the cookie value in browser is updated from "A" to "B".
After that R2 is then being processed by the server. The request still contains the old cookie session value "A" and same old last updated time. So server detects that it needs an update and generates a random session value "C" and tries to update the value in database where old_value is "A", which is not present any where. So the update does not occur, but it nevertheless sends the update cookie data with "C" to the browser. So now the cookie value in browser is updated from "B" to "C".
So now when you make any new request from the SPA application, it will use cookie session value "C" and while trying to read the session from database, it won't find the value in database(since the update had not been done to "C" and the current value in database is "B") and will destroy your session.
Solution:
The main issue here is with the second request R2, which could not update the value in database but sent an updated cookie value to browser which it should not. So this can be solved by checking if any rows were affected in database and then only sent updated cookie session data to browser. So in this case generated random value "C" would not be sent to browser and new requests would use "B" value which is still present in the browser.
Original code in sess_update() function in Session library:

// Update the session ID and last_activity field in the DB if needed
if ($this->sess_use_database === TRUE)
{
    // set cookie explicitly to only have our session data
    $cookie_data = array();
    foreach (array('session_id','ip_address','user_agent','last_activity') as $val)
    {
        $cookie_data[$val] = $this->userdata[$val];
    }

    $this->CI->db->query($this->CI->db->update_string($this->sess_table_name, array('last_activity' => $this->now, 'session_id' => $new_sessid), array('session_id' => $old_sessid)));
}

// Write the cookie
$this->_set_cookie($cookie_data);
Modified fixed code:

// Update the session ID and last_activity field in the DB if needed
if ($this->sess_use_database === TRUE)
{
    // set cookie explicitly to only have our session data
    $cookie_data = array();
    foreach (array('session_id','ip_address','user_agent','last_activity') as $val)
    {
        $cookie_data[$val] = $this->userdata[$val];
    }

    $this->CI->db->query($this->CI->db->update_string($this->sess_table_name, array('last_activity' => $this->now, 'session_id' => $new_sessid), array('session_id' => $old_sessid)));

    if ($this->CI->db->affected_rows())
    {
        // Write the cookie
        $this->_set_cookie($cookie_data);
    }
}
else
{
    // Write the cookie
    $this->_set_cookie($cookie_data);
}
This finally fixed the issue. Hope it helps :)