Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do these tests fail for this custom Flask session interface?

I am writing a hybrid single page web/PhoneGap application in Flask. Since cookies in a PhoneGap application are basically unavailable, I have implemented a custom session interface that completely avoids cookies. It stores session data in the application database and passes the session ID explicitly in the HTTP request and response bodies.

I have created a GitHub repository with a reduced testcase. It's still a sizeable project in its own right, but the Readme should help you to quickly find your way. The repo includes seven tests that all succeed when using Flask's default cookie-based session interface and all fail with my custom session interface. The main problem appears to be that data are sometimes not retained on the session object, but this is mysterious because the session object inherits from Python's builtin dict, which shouldn't spontaneously forget data. In addition, the session interface is straightforward and doesn't seem to make any obvious mistakes compared to Flask's example Redis session snippet.

To make matters more frustrating, the custom session interface seems to work correctly in the actual application. Only the unit tests are failing. However, for this reason it is unsafe to assume that the session interface works correctly in all circumstances.

Help will be much appreciated.

Edit: Gist is not accepting the reduced testcase because it includes directories. I am now moving it to a full-blown GitHub repository. I'll update this post again when done.

New edit: moved the reduced testcase to a proper GitHub repo. The Readme still mentions "this Gist", sorry.

like image 449
Julian Avatar asked Sep 09 '15 15:09

Julian


People also ask

How do you handle a session in Flask?

Flask-Session is an extension for Flask that supports Server-side Session to your application. The Session is the time between the client logs in to the server and logs out of the server. The data that is required to be saved in the Session is stored in a temporary directory on the server.

How do you define a session in Flask?

The session can be defined as the duration for which a user logs into the server and logs out. The data which is used to track this session is stored into the temporary directory on the server. The session data is stored on the top of cookies and signed by the server cryptographically.

How do I reset a session variable in Flask?

One must simply change the app. config["SECRET_KEY"] and the contents in session dictionary will get erased.


1 Answers

Your problems mostly come down to providing the session token in your test requests. If you don't provide the token the session is blank.

I assume your actual application is correctly sending the session token and thus appears to work.

It doesn't take much to fix up the test cases to pass correctly.

Every request attempts to load a session based on a post param

In your session implementation:

def open_session(self, app, request):
    s = Session()
    if 't' in request.form:
        ....

    return s

This means that every request that is not POST (or PUT) and doesn't have t sent will have a blank session.

Whereas a cookies based implementation will always have the session token available and will be able to load previous requests' sessions.

Here is one of your sample tests:

def test_authorize_captcha_expired(self):
    with self.client as c:
        with c.session_transaction() as s:
            s['captcha-answer'] = u'one two three'.split()
            s['captcha-expires'] = datetime.today() - timedelta(minutes=1)
        self.assertEqual(c.post('/test', data={
            'ca': 'one two three',
        }).status_code, 400)

You have not supplied a t value for the post to /test. Thus it gets a blank session which does not have a captcha-expires key and a KeyError is raised.

Your session requires a 'token' key for it to be saved

In your session implementation:

def save_session(self, app, session, response):
    if session.modified and 'token' in session:
        ...
        # save session to database
        ...

Thus when you have:

with c.session_transaction() as s:
    s['captcha-answer'] = u'one two three'.split()
    s['captcha-expires'] = datetime.today() - timedelta(minutes=1)

No session actually gets written to the database. For any subsequent request to use. Note that it really does need to be written to the database since open_session will attempt to load something from the database on every request.

To fix the most of those cases you need to supply a 'token' when creating the session and a 't' with that token for any requests that use it.

Thus the sample test I used above would end up like:

def test_authorize_captcha_expired(self):
    with self.client as c:
        token = generate_key(SystemRandom())
        with c.session_transaction() as s:
            s['token'] = token
            s['captcha-answer'] = u'one two three'.split()
            s['captcha-expires'] = datetime.today() - timedelta(minutes=1)
        self.assertEqual(c.post('/test', data={
            'ca': 'one two three',
            't': token
        }).status_code, 400)

You change the token when you respond with json

...but you are not using the new token when you make a subsequent request

def test_reply_to_reflection_passthrough(self):
    with self.client as c:
        token = 'abcdef'

        ...

        response2 = c.post('/reflection/1/reply', data={
            'p': 'test4',
            'r': 'testmessage',
            't': token,
        }, ...

By here, the post to /reflection/1/reply has generated a new token and saved it, thus the critical key last-reply is not in the session identified by abcdef. If this were a cookies based session then last-reply would be available to the next request.

So to fix this test... use the new token

def test_reply_to_reflection_passthrough(self):
    with self.client as c:

        ...

        response2 = c.post('/reflection/1/reply', data={

        ...

        token = session['token']
        with c.session_transaction(method="POST", data={'t':token}) as s:
            s['token'] = token
            s['last-request'] = datetime.now() - timedelta(milliseconds=1001)

        response3 = c.post('/reflection/1/reply', data={

        ...

A redirect will lose the session token

In the test test_bump:

def test_bump(self):
    response = self.client.post(
        '/admin/tip/action/',
        data = {'action': 'Bump', 'rowid': '1',},
        follow_redirects=True )
    self.assertIn(' tips have been bumped.', response.data)

The post to /admin/tip/action returns a redirect.

Here you are checking for the presence of a flash message. And flash messages get stored in the session.

With a cookie based session the session id is sent again with the subsequent redirected request.

Since your session id is specified as a post value it does not get sent again, the session and the flash messages are lost.

The way to fix this is not to follow redirects but to check the session for data set by flasks flash method.

def test_bump(self):
    with self.client as c:
        token = generate_key(SystemRandom())
        with c.session_transaction() as s:
            s['token'] = token
        c.post('/admin/tip/action/',
               data={'action': 'Bump', 'rowid': '1', 't': token})

        with c.session_transaction(method="POST", data={'t': token}) as s:
            self.assertIn(' tips have been bumped.', s['_flashes'][0][1])

And thats all

I have sent a pull request with the changes as I have described above, you will find that the tests now pass for both the default flask session and your session implementation.

like image 71
Jeremy Allen Avatar answered Nov 16 '22 00:11

Jeremy Allen