Python 在请求之间进行测试时cherrypy.session的奇怪行为
在测试CherryPy应用程序时,我遇到了一个奇怪的问题 基本上,测试时请求之间的会话数据会丢失,因为在运行服务器并手动测试时不会发生这种情况 应用程序本身非常简单,但是在处理请求之前,使用钩子机制来保护一些资源 让我们看看主文件: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
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表单
的所有内容?这应该对你有用。好的,它看起来像是setself.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