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

Python 在请求之间进行测试时cherrypy.session的奇怪行为,python,python-3.x,unit-testing,session,cherrypy,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

在测试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)
不会引发KeyError,因为它已在/login中正确设置。一切都很酷

现在这是我的测试文件,基于关于测试的官方文档,和上面的文件位于同一级别

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())
导入urllib
从unittest.mock导入修补程序
进口樱桃
来自cherrypy.test导入帮助程序
从cherrypy.lib.sessions导入RamSession
从.server导入根目录
从cr.db.store导入全局_设置作为设置
从cr.db.loader导入load_数据
DB_URL=mongodb://localhost:27017/test_db'
服务器http://127.0.0.1'
类SimpleCPTest(helper.CPWebCase):
def设置_服务器():
cherrypy.config.update({'environment':“test_suite”,
'tools.sessions.on':True,
'tools.sessions.name':'crunch',
“tools.crunch.on”:没错,
})
db={
“url”:DB_url
}
设置更新(db)
main=Root(设置)
#只需将一些虚拟用户加载到测试数据库中
加载\u数据(设置,真)
樱桃树挂载(主“/”)
setup\u server=staticmethod(setup\u server)
#辅助方法
def get_重定向_路径(自身、数据):
"""
尝试从响应中获取的cookie数据中提取路径
:param data:响应中的cookie数据
:return:如果可能,则为路径,否则为无
"""
路径=无
位置=无
#如果可能,从响应中获取位置
对于数据中的元组:
如果元组[0]=“位置”:
位置=元组[1]
打破
如果地点:
如果服务器位于以下位置:
index=location.find(服务器)
#路径加相等
索引=索引+len(服务器)+6
#获取实际路径
路径=位置[索引:]
返回路径
def测试\u登录\u显示\u如果未登录\u(自我):
response=self.getPage(“/”)
self.assertStatus('200 OK')
self.assertIn('欢迎来到Crunch.Please',响应[2].decode())
def测试\u登录\u重定向\u至\u用户(自我):
#尝试使用错误的密码进行身份验证
数据={
“用户名”:”john@doe.com',
“密码”:“管理员”,
}
query_string=urllib.parse.urlencode(数据)
getPage(“/login”,method='POST',body=query\u字符串)
#登录应显示401
self.assertStatus('401 Unauthorized')
#尝试使用正确的密码进行身份验证
数据={
“用户名”:”john@doe.com',
“密码”:“123456”,
}
query_string=urllib.parse.urlencode(数据)
#登录应该工作,并重定向到用户
getPage('/login',method='POST',body=query\u字符串)
self.assertStatus('301永久移动')
def测试\登录\无\凭证\抛出\ 401(自我):
#登录应显示401
response=self.getPage('/login',method='POST')
self.assertStatus('401请提供用户名和密码')
def测试\登录\显示\登录\注销\表单(自我):
#未经验证的GET应显示登录表单
response=self.getPage('/login',method='GET')
self.assertStatus('200 OK')
self.assertIn(“”,响应[2]。解码())
#尝试验证
数据={
“用户名”:”john@doe.com',
“密码”:“123456”,
}
query_string=urllib.parse.urlencode(数据)
#登录应该工作,并重定向到用户
response=self.getPage('/login',method='POST',body=query\u字符串)
self.assertStatus('301永久移动')
#FIXME:不得不模拟会话,不知道测试会话时请求之间为什么会丢失
#值,这将需要更多的调查,因为当启动真正的服务器时效果良好
#现在让我们来嘲笑它
sess_mock=RamSession()
sess_mock['user']='john@doe.com'
使用补丁('cherrypy.session',sess\u mock,create=True):
#再接再厉
response=self.getPage('/login',method='GET')
self.assertStatus('200 OK')
self.assertIn(“”,响应[2]。解码())
正如您在上一个方法中所看到的,在登录之后,我们应该设置cherrpy.session[session_KEY],但由于某些原因,会话没有密钥。这就是我不得不嘲笑它的原因…这是可行的,但黑客攻击的东西应该是可行的

在我看来,在测试会话时,请求之间似乎没有保留会话。在深入研究CherrPy的内部结构之前,我想问一下这个问题,以防有人在过去偶然发现类似的东西

注意,我在这里使用的是Python 3.4

谢谢

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

我制作了一个简单的示例,说明如何在下一个请求中持久化会话:


>>>test\u cp.py您是否尝试过使用会话模拟来包装
test\u login\u shows\u login\u logout\u表单
的所有内容?这应该对你有用。好的,它看起来像是set
self.cookies
,所以你可能想在第二次调用它时绕过它,比如
response=self.getPage('/login',headers=self.cookies,method='GET')
awesome@webKnjaZ,它会尝试一下,让你保持联系,干杯!我的意思是我会的
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())
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