在请求之间进行测试时,cherrypy.session的奇怪行为

时间:2017-05-02 09:44:42

标签: python python-3.x unit-testing session cherrypy

我在测试时测试CherryPy应用程序时遇到了一个奇怪的问题。

基本上,会话数据在测试时会在请求之间丢失,因为在运行服务器和手动测试时,这种情况不会发生。

应用程序本身非常简单,但是在处理请求之前使用钩子机制来保护某些资源。

让我们看一下主文件:

import cherrypy
import hashlib
import json
import sys
from bson import json_util

from cr.db.store import global_settings as settings
from cr.db.store import connect

SESSION_KEY = 'user'
main = None


def protect(*args, **kwargs):
    """
    Just a hook for checking protected resources
    :param args:
    :param kwargs:
    :return: 401 if unauthenticated access found (based on session id)
    """
    # Check if provided endpoint requires authentication
    condition = cherrypy.request.config.get('auth.require', None)
    if condition is not None:
        try:
            # Try to get the current session
            cherrypy.session[SESSION_KEY]
            # cherrypy.session.regenerate()
            cherrypy.request.login = cherrypy.session[SESSION_KEY]
        except KeyError:
            raise cherrypy.HTTPError(401, u'Not authorized to access this resource. Please login.')

# Specify the hook
cherrypy.tools.crunch = cherrypy.Tool('before_handler', protect)


class Root(object):

    def __init__(self, db_settings):
        self.db = connect(db_settings)

    @cherrypy.expose
    @cherrypy.config(**{'auth.require': True, 'tools.crunch.on': False})
    def index(self):
        # If authenticated, return to users view
        if SESSION_KEY in cherrypy.session:
            raise cherrypy.HTTPRedirect(u'/users', status=301)
        else:
            return 'Welcome to this site.  Please <a href="/login">login</a>.'


    @cherrypy.tools.allow(methods=['GET', 'POST'])
    @cherrypy.expose
    @cherrypy.config(**{'auth.require': True})
    @cherrypy.tools.json_in()
    def users(self, *args, **kwargs):
        if cherrypy.request.method == 'GET':
            return json.dumps({'users': [u for u in self.db.users.find()]}, default=json_util.default)
        elif cherrypy.request.method == 'POST':
            # Get post form data and create a new user
            input_json = cherrypy.request.json
            new_id = self.db.users.insert_one(input_json)
            new_user = self.db.users.find_one(new_id.inserted_id)
            cherrypy.response.status = 201
            return json.dumps(new_user, default=json_util.default)

    @cherrypy.tools.allow(methods=['GET', 'POST'])
    @cherrypy.expose
    @cherrypy.config(**{'tools.crunch.on': False})
    def login(self, *args, **kwargs):

        if cherrypy.request.method == 'GET':
            # Check if user is logged in already
            if SESSION_KEY in cherrypy.session:
                return """<html>
                  <head></head>
                  <body>
                    <form method="post" action="logout">
                      <label>Click button to logout</label>
                      <button type="submit">Logout</button>
                    </form>
                  </body>
                 </html>"""

            return """<html>
          <head></head>
          <body>
            <form method="post" action="login">
              <input type="text" value="Enter email" name="username" />
              <input type="password" value="Enter password" name="password" />
              <button type="submit">Login</button>
            </form>
          </body>
        </html>"""

        elif cherrypy.request.method == 'POST':
            # Get post form data and create a new user
            if 'password' and 'username' in kwargs:
                user = kwargs['username']
                password = kwargs['password']
                if self.user_verify(user, password):
                    cherrypy.session.regenerate()
                    cherrypy.session[SESSION_KEY] = cherrypy.request.login = user
                    # Redirect to users
                    raise cherrypy.HTTPRedirect(u'/users', status=301)
                else:
                    raise cherrypy.HTTPError(u'401 Unauthorized')
            else:
                raise cherrypy.HTTPError(u'401 Please provide username and password')


    @cherrypy.tools.allow(methods=['GET'])
    @cherrypy.expose
    def logout(self):
        if SESSION_KEY in cherrypy.session:
            cherrypy.session.regenerate()
            return 'Logged out, we will miss you dearly!.'
        else:
            raise cherrypy.HTTPRedirect(u'/', status=301)

    def user_verify(self, username, password):
        """
        Simply checks if a user with provided email and pass exists in db
        :param username: User email
        :param password:  User pass
        :return: True if user found
        """
        users = self.db.users
        user = users.find_one({"email": username})
        if user:
            password = hashlib.sha1(password.encode()).hexdigest()
            return password == user['hash']
        return False

if __name__ == '__main__':
    config_root = {'/': {
        'tools.crunch.on': True,
        'tools.sessions.on': True,
        'tools.sessions.name': 'crunch', }
    }
    # This simply specifies the URL for the Mongo db
    settings.update(json.load(open(sys.argv[1])))
    main = Root(settings)
    cherrypy.quickstart(main, '/', config=config_root)

cr.db是一个非常简单的pymongo包装器,它暴露了db功能,没什么特别的。

正如您所看到的,用户视图受到保护,基本上如果未设置SESSION [&#39; user&#39;]键,我们会要求登录。

如果我启动服务器并尝试直接访问/用户,我将重定向到/ login。一旦进入,访问/用户再次正常工作,因为

cherrypy.session[SESSION_KEY]

由于在/ login中正确设置,因此不会引发KeyError。一切都很酷。

现在这是我的测试文件,基于有关测试的官方文档,位于与上述文件相同的级别。

import urllib
from unittest.mock import patch
import cherrypy
from cherrypy.test import helper
from cherrypy.lib.sessions import RamSession
from .server import Root
from cr.db.store import global_settings as settings
from cr.db.loader import load_data

DB_URL = 'mongodb://localhost:27017/test_db'
SERVER = 'http://127.0.0.1'


class SimpleCPTest(helper.CPWebCase):

    def setup_server():
        cherrypy.config.update({'environment': "test_suite",
                                'tools.sessions.on': True,
                                'tools.sessions.name': 'crunch',
                                'tools.crunch.on': True,
                                })
        db = {
            "url": DB_URL
        }
        settings.update(db)
        main = Root(settings)
        # Simply loads some dummy users into test db
        load_data(settings, True)
        cherrypy.tree.mount(main, '/')
    setup_server = staticmethod(setup_server)

    # HELPER METHODS
    def get_redirect_path(self, data):
        """
        Tries to extract the path from the cookie data obtained in a response
        :param data: The cookie data from the response
        :return: The path if possible, None otherwise
        """
        path = None
        location = None
        # Get the Location from response, if possible
        for tuples in data:
            if tuples[0] == 'Location':
               location = tuples[1]
               break
        if location:
            if SERVER in location:
                index = location.find(SERVER)
                # Path plus equal
                index = index + len(SERVER) + 6
                # Get the actual path
                path = location[index:]
        return path

    def test_login_shown_if_not_logged_in(self):
        response = self.getPage('/')
        self.assertStatus('200 OK') 
        self.assertIn('Welcome to Crunch.  Please <a href="/login">login</a>.', response[2].decode())

    def test_login_redirect_to_users(self):
        # Try to authenticate with a wrong password
        data = {
            'username': 'john@doe.com',
            'password': 'admin',
        }
        query_string = urllib.parse.urlencode(data)
        self.getPage("/login", method='POST', body=query_string)
        # Login should show 401
        self.assertStatus('401 Unauthorized')
        # Try to authenticate with a correct password
        data = {
            'username': 'john@doe.com',
            'password': '123456',
        }
        query_string = urllib.parse.urlencode(data)
        # Login should work and be redirected to users
        self.getPage('/login', method='POST', body=query_string)
        self.assertStatus('301 Moved Permanently')

    def test_login_no_credentials_throws_401(self):
        # Login should show 401
        response = self.getPage('/login', method='POST')
        self.assertStatus('401 Please provide username and password')

    def test_login_shows_login_logout_forms(self):
        # Unauthenticated GET should show login form
        response = self.getPage('/login', method='GET')
        self.assertStatus('200 OK')
        self.assertIn('<form method="post" action="login">', response[2].decode())
        # Try to authenticate
        data = {
            'username': 'john@doe.com',
            'password': '123456',
        }
        query_string = urllib.parse.urlencode(data)
        # Login should work and be redirected to users
        response = self.getPage('/login', method='POST', body=query_string)
        self.assertStatus('301 Moved Permanently')
        # FIXME: Had to mock the session, not sure why between requests while testing the session loses
        # values, this would require more investigation, since when firing up the real server works fine
        # For now let's just mock it
        sess_mock = RamSession()
        sess_mock['user'] = 'john@doe.com'
        with patch('cherrypy.session', sess_mock, create=True):
            # Make a GET again
            response = self.getPage('/login', method='GET')
            self.assertStatus('200 OK')
            self.assertIn('<form method="post" action="logout">', response[2].decode())

正如您在上一个方法中所看到的,在登录后,我们应该设置cherrpy.session [SESSION_KEY],但由于某种原因,会话没有密钥。这就是我实际上不得不嘲笑它的原因......这是有效的,但却是黑客入侵应该真正起作用的东西......

对我而言,在请求之间没有保持测试会话时看起来像。在深入了解CherrPy内部之前,我想问一下,以防有人在过去偶然发现类似事情。

请注意我在这里使用Python 3.4。

由于

1 个答案:

答案 0 :(得分:1)

getPage()接受headers参数并生成self.cookies iterable。但它不会自动将其传递给下一个请求,因此它不会获得相同的会话cookie。

我已经制作了一个如何使用下一个请求保持会话的简单示例:

&GT;&GT;&GT; test_cp.py&lt;&lt;&lt;

import cherrypy
from cherrypy.test import helper

class SimpleCPTest(helper.CPWebCase):

    @staticmethod
    def setup_server():
        class Root:
            @cherrypy.expose
            def login(self):
                if cherrypy.request.method == 'POST':
                    cherrypy.session['test_key'] = 'test_value'
                    return 'Hello'
                elif cherrypy.request.method in ['GET', 'HEAD']:
                    try:
                        return cherrypy.session['test_key']
                    except KeyError:
                        return 'Oops'

        cherrypy.config.update({'environment': "test_suite",
                                'tools.sessions.on': True,
                                'tools.sessions.name': 'crunch',
                                })
        main = Root()
        # Simply loads some dummy users into test db
        cherrypy.tree.mount(main, '')

    def test_session_sharing(self):
        # Unauthenticated GET
        response = self.getPage('/login', method='GET')
        self.assertIn('Oops', response[2].decode())

        # Authenticate
        response = self.getPage('/login', method='POST')
        self.assertIn('Hello', response[2].decode())

        # Make a GET again <<== INCLUDE headers=self.cookies below:
        response = self.getPage('/login', headers=self.cookies, method='GET')
        self.assertIn('test_value', response[2].decode())

运行它

$ pytest
Test session starts (platform: linux, Python 3.6.1, pytest 3.0.7, pytest-sugar 0.8.0)
rootdir: ~/src/test, inifile:
plugins: sugar-0.8.0, backports.unittest-mock-1.3

 test_cp.py ✓✓                                                                                                                                                           100% ██████████

Results (0.41s):
       2 passed

P.S。当然,理想情况下我会继承testcase类并添加其他方法来封装它;)