""".strip ())
def testXMLTemplate(self):
page_template = """"""
pt = RoundupPageTemplate()
pt.pt_edit(page_template, 'application/xml')
cl = self.setupClient({ }, 'issue',
env_addon = {'HTTP_REFERER': 'http://whoami.com/path/'})
out = pt.render(cl, 'issue', MockNull())
self.assertEqual(out, '\n')
def testCsrfProtection(self):
# need to set SENDMAILDEBUG to prevent
# downstream issue when email is sent on successful
# issue creation. Also delete the file afterwards
# just tomake sure that someother test looking for
# SENDMAILDEBUG won't trip over ours.
if 'SENDMAILDEBUG' not in os.environ:
os.environ['SENDMAILDEBUG'] = 'mail-test1.log'
SENDMAILDEBUG = os.environ['SENDMAILDEBUG']
page_template = """
""".strip ()
self.db.keyword.create (name = 'key1')
self.db.keyword.create (name = 'key2')
nodeid = self.db.issue.create (title = 'Title', priority = '1',
status = '1', nosy = ['1'], keyword = ['1'])
self.db.commit ()
form = {':note': 'msg-content', 'title': 'New title',
'priority': '2', 'status': '2', 'nosy': '1,2', 'keyword': '',
':action': 'edit'}
cl = self.setupClient(form, 'issue', '1')
pt = RoundupPageTemplate()
pt.pt_edit(page_template, 'text/html')
out = []
def wh(s):
out.append(s)
cl.write_html = wh
# Enable the following if we get a templating error:
#def send_error (*args, **kw):
# import pdb; pdb.set_trace()
#cl.send_error_to_admin = send_error
# Need to rollback the database on error -- this usually happens
# in web-interface (and for other databases) anyway, need it for
# testing that the form values are really used, not the database!
# We do this together with the setup of the easy template above
def load_template(x):
cl.db.rollback()
return pt
cl.instance.templates.load = load_template
cl.selectTemplate = MockNull()
cl.determine_context = MockNull ()
def hasPermission(s, p, classname=None, d=None, e=None, **kw):
return True
actions.Action.hasPermission = hasPermission
e1 = _HTMLItem.is_edit_ok
_HTMLItem.is_edit_ok = lambda x : True
e2 = HTMLProperty.is_edit_ok
HTMLProperty.is_edit_ok = lambda x : True
# test with no headers and config by default requires 1
cl.inner_main()
match_at=out[0].find('Unable to verify sufficient headers')
print("result of subtest 1:", out[0])
self.assertNotEqual(match_at, -1)
del(out[0])
# all the rest of these allow at least one header to pass
# and the edit happens with a redirect back to issue 1
cl.env['HTTP_REFERER'] = 'http://whoami.com/path/'
cl.inner_main()
match_at=out[0].find('Redirecting to Csrf token is missing.
')
print("result of subtest 6a:", out[0], match_at)
self.assertEqual(match_at, 33)
del(out[0])
cl.db.config['WEB_CSRF_ENFORCE_TOKEN'] = 'yes'
import copy
form2 = copy.copy(form)
form2.update({'@csrf': 'booogus'})
# add a bogus csrf field to the form and rerun the inner_main
cl.form = db_test_base.makeForm(form2)
cl.inner_main()
match_at=out[0].find('Invalid csrf token found: booogus')
print("result of subtest 7:", out[0])
self.assertEqual(match_at, 36)
del(out[0])
form2 = copy.copy(form)
nonce = anti_csrf_nonce(cl)
# verify that we can see the nonce
otks = cl.db.getOTKManager()
isitthere = otks.exists(nonce)
print("result of subtest 8:", isitthere)
print("otks: user, session", otks.get(nonce, 'uid', default=None),
otks.get(nonce, 'session', default=None))
self.assertEqual(isitthere, True)
form2.update({'@csrf': nonce})
# add a real csrf field to the form and rerun the inner_main
cl.form = db_test_base.makeForm(form2)
cl.inner_main()
# csrf passes and redirects to the new issue.
match_at=out[0].find('Redirecting to Invalid request')
print("result of subtest 11:", out[0])
self.assertEqual(match_at, 33)
del(out[0])
# the token should be gone
isitthere = otks.exists(nonce)
print("result of subtest 12:", isitthere)
self.assertEqual(isitthere, False)
# change to post and should fail w/ invalid csrf
# since get deleted the token.
cl.env.update({'REQUEST_METHOD': 'POST'})
print(cl.env)
cl.inner_main()
match_at=out[0].find('Invalid csrf token found: %s'%nonce)
print("post failure after get", out[0])
print("result of subtest 13:", out[0])
self.assertEqual(match_at, 36)
del(out[0])
del(cl.env['HTTP_REFERER'])
# clean up from email log
if os.path.exists(SENDMAILDEBUG):
os.remove(SENDMAILDEBUG)
#raise ValueError
def testRestCsrfProtection(self):
import json
# set the password for admin so we can log in.
passwd=password.Password('admin')
self.db.user.set('1', password=passwd)
out = []
def wh(s):
out.append(s)
# rest has no form content
form = cgi.FieldStorage()
form.list = [
cgi.MiniFieldStorage('title', 'A new issue'),
cgi.MiniFieldStorage('status', '1'),
cgi.MiniFieldStorage('@pretty', 'false'),
cgi.MiniFieldStorage('@apiver', '1'),
]
cl = client.Client(self.instance, None,
{'REQUEST_METHOD':'POST',
'PATH_INFO':'rest/data/issue',
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
'HTTP_REFERER': 'http://whoami.com/path/',
'HTTP_ACCEPT': "application/json;version=1"
}, form)
cl.db = self.db
cl.base = 'http://whoami.com/path/'
cl._socket_op = lambda *x : True
cl._error_message = []
cl.request = MockNull()
h = { 'content-type': 'application/json',
'accept': 'application/json' }
cl.request.headers = MockNull(**h)
cl.write = wh # capture output
# Should return explanation because content type is text/plain
# and not text/xml
cl.handle_rest()
self.assertEqual(b2s(out[0]), ": Required Header Missing\n")
del(out[0])
cl = client.Client(self.instance, None,
{'REQUEST_METHOD':'POST',
'PATH_INFO':'rest/data/issue',
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
'HTTP_REFERER': 'http://whoami.com/path/',
'HTTP_X_REQUESTED_WITH': 'rest',
'HTTP_ACCEPT': "application/json;version=1"
}, form)
cl.db = self.db
cl.base = 'http://whoami.com/path/'
cl._socket_op = lambda *x : True
cl._error_message = []
cl.request = MockNull()
h = { 'content-type': 'application/json',
'accept': 'application/json;version=1' }
cl.request.headers = MockNull(**h)
cl.write = wh # capture output
# Should work as all required headers are present.
cl.handle_rest()
answer='{"data": {"link": "http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/issue/1", "id": "1"}}\n'
# check length to see if pretty is turned off.
self.assertEqual(len(out[0]), 99)
# compare as dicts not strings due to different key ordering
# between python versions.
response=json.loads(b2s(out[0]))
expected=json.loads(answer)
self.assertEqual(response,expected)
del(out[0])
def testXmlrpcCsrfProtection(self):
# set the password for admin so we can log in.
passwd=password.Password('admin')
self.db.user.set('1', password=passwd)
out = []
def wh(s):
out.append(s)
# xmlrpc has no form content
form = {}
cl = client.Client(self.instance, None,
{'REQUEST_METHOD':'POST',
'PATH_INFO':'xmlrpc',
'CONTENT_TYPE': 'text/plain',
'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
'HTTP_REFERER': 'http://whoami.com/path/',
'HTTP_X_REQUESTED_WITH': "XMLHttpRequest"
}, form)
cl.db = self.db
cl.base = 'http://whoami.com/path/'
cl._socket_op = lambda *x : True
cl._error_message = []
cl.request = MockNull()
cl.write = wh # capture output
# Should return explanation because content type is text/plain
# and not text/xml
cl.handle_xmlrpc()
self.assertEqual(out[0], b"This is the endpoint of Roundup XML-RPC interface.")
del(out[0])
# Should return admin user indicating auth works and
# header checks succeed (REFERER and X-REQUESTED-WITH)
cl.env['CONTENT_TYPE'] = "text/xml"
# ship the form with the value holding the xml value.
# I have no clue why this works but ....
cl.form = MockNull(file = True, value = "\n\ndisplay\n\n\nuser1\n\n\nusername\n\n\n\n" )
answer = b"\n\n\n\n\n\nusername\nadmin\n\n\n\n\n\n"
cl.handle_xmlrpc()
print(out)
self.assertEqual(out[0], answer)
del(out[0])
# remove the X-REQUESTED-WITH header and get an xmlrpc fault returned
del(cl.env['HTTP_X_REQUESTED_WITH'])
cl.handle_xmlrpc()
frag_faultCode = "\nfaultCode\n1\n\n"
frag_faultString = "\nfaultString\n<class 'roundup.exceptions.UsageError'>:Required Header Missing\n\n"
output_fragments = ["\n",
"\n",
"\n",
"\n",
(frag_faultCode + frag_faultString,
frag_faultString + frag_faultCode),
"\n",
"\n",
"\n"]
print(out[0])
self.compareStringFragments(out[0], output_fragments)
del(out[0])
# change config to not require X-REQUESTED-WITH header
cl.db.config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH'] = 'logfailure'
cl.handle_xmlrpc()
print(out)
self.assertEqual(out[0], answer)
del(out[0])
#
# SECURITY
#
# XXX test all default permissions
def _make_client(self, form, classname='user', nodeid='1',
userid='2', template='item'):
cl = client.Client(self.instance, None, {'PATH_INFO':'/',
'REQUEST_METHOD':'POST'}, db_test_base.makeForm(form))
cl.classname = classname
if nodeid is not None:
cl.nodeid = nodeid
cl.db = self.db
#cl.db.Otk = MockNull()
#cl.db.Otk.data = {}
#cl.db.Otk.getall = self.data_get
#cl.db.Otk.set = self.data_set
cl.userid = userid
cl.language = ('en',)
cl._error_message = []
cl._ok_message = []
cl.template = template
return cl
def data_get(self, key):
return self.db.Otk.data[key]
def data_set(self, key, **value):
self.db.Otk.data[key] = value
def testClassPermission(self):
cl = self._make_client(dict(username='bob'))
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl.nodeid = '1'
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
def testCheckAndPropertyPermission(self):
self.db.security.permissions = {}
def own_record(db, userid, itemid):
return userid == itemid
p = self.db.security.addPermission(name='Edit', klass='user',
check=own_record, properties=("password", ))
self.db.security.addPermissionToRole('User', p)
cl = self._make_client(dict(username='bob'))
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(roles='User,Admin'), userid='4', nodeid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(roles='User,Admin'), userid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(roles='User,Admin'))
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
# working example, mary may change her pw
cl = self._make_client({'password':'ob', '@confirm@password':'ob'},
nodeid='4', userid='4')
self.assertRaises(exceptions.Redirect,
actions.EditItemAction(cl).handle)
cl = self._make_client({'password':'bob', '@confirm@password':'bob'})
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
def testCreatePermission(self):
# this checks if we properly differentiate between create and
# edit permissions
self.db.security.permissions = {}
self.db.security.addRole(name='UserAdd')
# Don't allow roles
p = self.db.security.addPermission(name='Create', klass='user',
properties=("username", "password", "address",
"alternate_address", "realname", "phone", "organisation",
"timezone"))
self.db.security.addPermissionToRole('UserAdd', p)
# Don't allow roles *and* don't allow username
p = self.db.security.addPermission(name='Edit', klass='user',
properties=("password", "address", "alternate_address",
"realname", "phone", "organisation", "timezone"))
self.db.security.addPermissionToRole('UserAdd', p)
self.db.user.set('4', roles='UserAdd')
# anonymous may not
cl = self._make_client({'username':'new_user', 'password':'secret',
'@confirm@password':'secret', 'address':'new_user@bork.bork',
'roles':'Admin'}, nodeid=None, userid='2')
self.assertRaises(exceptions.Unauthorised,
actions.NewItemAction(cl).handle)
# Don't allow creating new user with roles
cl = self._make_client({'username':'new_user', 'password':'secret',
'@confirm@password':'secret', 'address':'new_user@bork.bork',
'roles':'Admin'}, nodeid=None, userid='4')
self.assertRaises(exceptions.Unauthorised,
actions.NewItemAction(cl).handle)
self.assertEqual(cl._error_message,[])
# this should work
cl = self._make_client({'username':'new_user', 'password':'secret',
'@confirm@password':'secret', 'address':'new_user@bork.bork'},
nodeid=None, userid='4')
self.assertRaises(exceptions.Redirect,
actions.NewItemAction(cl).handle)
self.assertEqual(cl._error_message,[])
# don't allow changing (my own) username (in this example)
cl = self._make_client(dict(username='new_user42'), userid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(username='new_user42'), userid='4',
nodeid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
# don't allow changing (my own) roles
cl = self._make_client(dict(roles='User,Admin'), userid='4',
nodeid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(roles='User,Admin'), userid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(roles='User,Admin'))
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
def testSearchPermission(self):
# this checks if we properly check for search permissions
self.db.security.permissions = {}
self.db.security.addRole(name='User')
self.db.security.addRole(name='Project')
self.db.security.addPermissionToRole('User', 'Web Access')
self.db.security.addPermissionToRole('Project', 'Web Access')
# Allow viewing department
p = self.db.security.addPermission(name='View', klass='department')
self.db.security.addPermissionToRole('User', p)
# Allow viewing interesting things (but not department) on iss
# But users might only view issues where they are on nosy
# (so in the real world the check method would be better)
p = self.db.security.addPermission(name='View', klass='iss',
properties=("title", "status"), check=lambda x,y,z: True)
self.db.security.addPermissionToRole('User', p)
# Allow all relevant roles access to stat
p = self.db.security.addPermission(name='View', klass='stat')
self.db.security.addPermissionToRole('User', p)
self.db.security.addPermissionToRole('Project', p)
# Allow role "Project" access to whole iss
p = self.db.security.addPermission(name='View', klass='iss')
self.db.security.addPermissionToRole('Project', p)
department = self.instance.backend.Class(self.db, "department",
name=hyperdb.String())
status = self.instance.backend.Class(self.db, "stat",
name=hyperdb.String())
issue = self.instance.backend.Class(self.db, "iss",
title=hyperdb.String(), status=hyperdb.Link('stat'),
department=hyperdb.Link('department'))
d1 = department.create(name='d1')
d2 = department.create(name='d2')
open = status.create(name='open')
closed = status.create(name='closed')
issue.create(title='i1', status=open, department=d2)
issue.create(title='i2', status=open, department=d1)
issue.create(title='i2', status=closed, department=d1)
chef = self.db.user.lookup('Chef')
mary = self.db.user.lookup('mary')
self.db.user.set(chef, roles = 'User, Project')
perm = self.db.security.hasPermission
search = self.db.security.hasSearchPermission
self.assertTrue(perm('View', chef, 'iss', 'department', '1'))
self.assertTrue(perm('View', chef, 'iss', 'department', '2'))
self.assertTrue(perm('View', chef, 'iss', 'department', '3'))
self.assertTrue(search(chef, 'iss', 'department'))
self.assertTrue(not perm('View', mary, 'iss', 'department'))
self.assertTrue(perm('View', mary, 'iss', 'status'))
# Conditionally allow view of whole iss (check is False here,
# this might check for department owner in the real world)
p = self.db.security.addPermission(name='View', klass='iss',
check=lambda x,y,z: False)
self.db.security.addPermissionToRole('User', p)
self.assertTrue(perm('View', mary, 'iss', 'department'))
self.assertTrue(not perm('View', mary, 'iss', 'department', '1'))
self.assertTrue(not search(mary, 'iss', 'department'))
self.assertTrue(perm('View', mary, 'iss', 'status'))
self.assertTrue(not search(mary, 'iss', 'status'))
# Allow user to search for iss.status
p = self.db.security.addPermission(name='Search', klass='iss',
properties=("status",))
self.db.security.addPermissionToRole('User', p)
self.assertTrue(search(mary, 'iss', 'status'))
dep = {'@action':'search','columns':'id','@filter':'department',
'department':'1'}
stat = {'@action':'search','columns':'id','@filter':'status',
'status':'1'}
depsort = {'@action':'search','columns':'id','@sort':'department'}
depgrp = {'@action':'search','columns':'id','@group':'department'}
# Filter on department ignored for role 'User':
cl = self._make_client(dep, classname='iss', nodeid=None, userid=mary,
template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
# Filter on department works for role 'Project':
cl = self._make_client(dep, classname='iss', nodeid=None, userid=chef,
template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['2', '3'])
# Filter on status works for all:
cl = self._make_client(stat, classname='iss', nodeid=None, userid=mary,
template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['1', '2'])
cl = self._make_client(stat, classname='iss', nodeid=None, userid=chef,
template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['1', '2'])
# Sorting and grouping for class Project works:
cl = self._make_client(depsort, classname='iss', nodeid=None,
userid=chef, template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['2', '3', '1'])
self.assertEqual(cl._error_message, []) # test for empty _error_message when sort is valid
self.assertEqual(cl._ok_message, []) # test for empty _ok_message when sort is valid
# Test for correct _error_message for invalid sort/group properties
baddepsort = {'@action':'search','columns':'id','@sort':'dep'}
baddepgrp = {'@action':'search','columns':'id','@group':'dep'}
cl = self._make_client(baddepsort, classname='iss', nodeid=None,
userid=chef, template='index')
h = HTMLRequest(cl)
self.assertEqual(cl._error_message, ['Unknown sort property dep'])
cl = self._make_client(baddepgrp, classname='iss', nodeid=None,
userid=chef, template='index')
h = HTMLRequest(cl)
self.assertEqual(cl._error_message, ['Unknown group property dep'])
cl = self._make_client(depgrp, classname='iss', nodeid=None,
userid=chef, template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['2', '3', '1'])
# Sorting and grouping for class User fails:
cl = self._make_client(depsort, classname='iss', nodeid=None,
userid=mary, template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
cl = self._make_client(depgrp, classname='iss', nodeid=None,
userid=mary, template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
def testEditCSV(self):
form = dict(rows='id,name\n1,newkey')
cl = self._make_client(form, userid='1', classname='keyword')
cl._ok_message = []
actions.EditCSVAction(cl).handle()
self.assertEqual(cl._ok_message, ['Items edited OK'])
k = self.db.keyword.getnode('1')
self.assertEqual(k.name, 'newkey')
form = dict(rows=u2s(u'id,name\n1,\xe4\xf6\xfc'))
cl = self._make_client(form, userid='1', classname='keyword')
cl._ok_message = []
actions.EditCSVAction(cl).handle()
self.assertEqual(cl._ok_message, ['Items edited OK'])
k = self.db.keyword.getnode('1')
self.assertEqual(k.name, u2s(u'\xe4\xf6\xfc'))
def testEditCSVRestore(self):
form = dict(rows='id,name\n1,key1\n2,key2')
cl = self._make_client(form, userid='1', classname='keyword')
cl._ok_message = []
actions.EditCSVAction(cl).handle()
self.assertEqual(cl._ok_message, ['Items edited OK'])
k = self.db.keyword.getnode('1')
self.assertEqual(k.name, 'key1')
k = self.db.keyword.getnode('2')
self.assertEqual(k.name, 'key2')
form = dict(rows='id,name\n1,key1')
cl = self._make_client(form, userid='1', classname='keyword')
cl._ok_message = []
actions.EditCSVAction(cl).handle()
self.assertEqual(cl._ok_message, ['Items edited OK'])
k = self.db.keyword.getnode('1')
self.assertEqual(k.name, 'key1')
self.assertEqual(self.db.keyword.is_retired('2'), True)
form = dict(rows='id,name\n1,newkey1\n2,newkey2')
cl = self._make_client(form, userid='1', classname='keyword')
cl._ok_message = []
actions.EditCSVAction(cl).handle()
self.assertEqual(cl._ok_message, ['Items edited OK'])
k = self.db.keyword.getnode('1')
self.assertEqual(k.name, 'newkey1')
k = self.db.keyword.getnode('2')
self.assertEqual(k.name, 'newkey2')
def testserve_static_files(self):
# make a client instance
cl = self._make_client({})
# hijack _serve_file so I can see what is found
output = []
def my_serve_file(a, b, c, d):
output.append((a,b,c,d))
cl._serve_file = my_serve_file
# check case where file is not found.
self.assertRaises(NotFound,
cl.serve_static_file,"missing.css")
# TEMPLATES dir is searched by default. So this file exists.
# Check the returned values.
cl.serve_static_file("issue.index.html")
self.assertEqual(output[0][1], "text/html")
self.assertEqual(output[0][3], "_test_cgi_form/html/issue.index.html")
del output[0] # reset output buffer
# stop searching TEMPLATES for the files.
cl.instance.config['STATIC_FILES'] = '-'
# previously found file should not be found
self.assertRaises(NotFound,
cl.serve_static_file,"issue.index.html")
# explicitly allow html directory
cl.instance.config['STATIC_FILES'] = 'html -'
cl.serve_static_file("issue.index.html")
self.assertEqual(output[0][1], "text/html")
self.assertEqual(output[0][3], "_test_cgi_form/html/issue.index.html")
del output[0] # reset output buffer
# set the list of files and do not look at the templates directory
cl.instance.config['STATIC_FILES'] = 'detectors extensions - '
# find file in first directory
cl.serve_static_file("messagesummary.py")
self.assertEqual(output[0][1], "text/x-python")
self.assertEqual(output[0][3], "_test_cgi_form/detectors/messagesummary.py")
del output[0] # reset output buffer
# find file in second directory
cl.serve_static_file("README.txt")
self.assertEqual(output[0][1], "text/plain")
self.assertEqual(output[0][3], "_test_cgi_form/extensions/README.txt")
del output[0] # reset output buffer
# make sure an embedded - ends the searching.
cl.instance.config['STATIC_FILES'] = ' detectors - extensions '
self.assertRaises(NotFound, cl.serve_static_file, "README.txt")
cl.instance.config['STATIC_FILES'] = ' detectors - extensions '
self.assertRaises(NotFound, cl.serve_static_file, "issue.index.html")
# create an empty README.txt in the first directory
f = open('_test_cgi_form/detectors/README.txt', 'a').close()
# find file now in first directory
cl.serve_static_file("README.txt")
self.assertEqual(output[0][1], "text/plain")
self.assertEqual(output[0][3], "_test_cgi_form/detectors/README.txt")
del output[0] # reset output buffer
cl.instance.config['STATIC_FILES'] = ' detectors extensions '
# make sure lack of trailing - allows searching TEMPLATES
cl.serve_static_file("issue.index.html")
self.assertEqual(output[0][1], "text/html")
self.assertEqual(output[0][3], "_test_cgi_form/html/issue.index.html")
del output[0] # reset output buffer
# Make STATIC_FILES a single element.
cl.instance.config['STATIC_FILES'] = 'detectors'
# find file now in first directory
cl.serve_static_file("messagesummary.py")
self.assertEqual(output[0][1], "text/x-python")
self.assertEqual(output[0][3], "_test_cgi_form/detectors/messagesummary.py")
del output[0] # reset output buffer
# make sure files found in subdirectory
os.mkdir('_test_cgi_form/detectors/css')
f = open('_test_cgi_form/detectors/css/README.css', 'a').close()
# use subdir in filename
cl.serve_static_file("css/README.css")
self.assertEqual(output[0][1], "text/css")
self.assertEqual(output[0][3], "_test_cgi_form/detectors/css/README.css")
del output[0] # reset output buffer
# use subdir in static files path
cl.instance.config['STATIC_FILES'] = 'detectors html/css'
os.mkdir('_test_cgi_form/html/css')
f = open('_test_cgi_form/html/css/README1.css', 'a').close()
cl.serve_static_file("README1.css")
self.assertEqual(output[0][1], "text/css")
self.assertEqual(output[0][3], "_test_cgi_form/html/css/README1.css")
del output[0] # reset output buffer
def testRoles(self):
cl = self._make_client({})
self.db.user.set('1', roles='aDmin, uSer')
item = HTMLItem(cl, 'user', '1')
self.assertTrue(item.hasRole('Admin'))
self.assertTrue(item.hasRole('User'))
self.assertTrue(item.hasRole('AdmiN'))
self.assertTrue(item.hasRole('UseR'))
self.assertTrue(item.hasRole('UseR','Admin'))
self.assertTrue(item.hasRole('UseR','somethingelse'))
self.assertTrue(item.hasRole('somethingelse','Admin'))
self.assertTrue(not item.hasRole('userr'))
self.assertTrue(not item.hasRole('adminn'))
self.assertTrue(not item.hasRole(''))
self.assertTrue(not item.hasRole(' '))
self.db.user.set('1', roles='')
self.assertTrue(not item.hasRole(''))
def testCSVExport(self):
cl = self._make_client(
{'@columns': 'id,title,status,keyword,assignedto,nosy'},
nodeid=None, userid='1')
cl.classname = 'issue'
demo_id=self.db.user.create(username='demo', address='demo@test.test',
roles='User', realname='demo')
key_id1=self.db.keyword.create(name='keyword1')
key_id2=self.db.keyword.create(name='keyword2')
self.db.issue.create(title='foo1', status='2', assignedto='4', nosy=['3',demo_id])
self.db.issue.create(title='bar2', status='1', assignedto='3', keyword=[key_id1,key_id2])
self.db.issue.create(title='baz32', status='4')
output = StringIO()
cl.request = MockNull()
cl.request.wfile = output
# call export version that outputs names
actions.ExportCSVAction(cl).handle()
#print(output.getvalue())
should_be=('id,title,status,keyword,assignedto,nosy\r\n'
'1,foo1,deferred,,"Contrary, Mary","Bork, Chef;Contrary, Mary;demo"\r\n'
'2,bar2,unread,keyword1;keyword2,"Bork, Chef","Bork, Chef"\r\n'
'3,baz32,need-eg,,,\r\n')
#print(should_be)
#print(output.getvalue())
self.assertEqual(output.getvalue(), should_be)
output = StringIO()
cl.request = MockNull()
cl.request.wfile = output
# call export version that outputs id numbers
actions.ExportCSVWithIdAction(cl).handle()
print(output.getvalue())
self.assertEqual('id,title,status,keyword,assignedto,nosy\r\n'
"1,foo1,2,[],4,\"['3', '4', '5']\"\r\n"
"2,bar2,1,\"['1', '2']\",3,['3']\r\n"
'3,baz32,4,[],None,[]\r\n',
output.getvalue())
def testCSVExportBadColumnName(self):
cl = self._make_client({'@columns': 'falseid,name'}, nodeid=None,
userid='1')
cl.classname = 'status'
output = StringIO()
cl.request = MockNull()
cl.request.wfile = output
self.assertRaises(exceptions.NotFound,
actions.ExportCSVAction(cl).handle)
def testCSVExportFailPermissionBadColumn(self):
cl = self._make_client({'@columns': 'id,email,password'}, nodeid=None,
userid='2')
cl.classname = 'user'
output = StringIO()
cl.request = MockNull()
cl.request.wfile = output
# used to be self.assertRaises(exceptions.Unauthorised,
# but not acting like the column name is not found
# see issue2550755 - should this return Unauthorised?
# The unauthorised user should never get to the point where
# they can determine if the column name is valid or not.
self.assertRaises(exceptions.NotFound,
actions.ExportCSVAction(cl).handle)
def testCSVExportFailPermissionValidColumn(self):
passwd=password.Password('foo')
demo_id=self.db.user.create(username='demo', address='demo@test.test',
roles='User', realname='demo',
password=passwd)
cl = self._make_client({'@columns': 'id,username,address,password'},
nodeid=None, userid=demo_id)
cl.classname = 'user'
output = StringIO()
cl.request = MockNull()
cl.request.wfile = output
# used to be self.assertRaises(exceptions.Unauthorised,
# but not acting like the column name is not found
actions.ExportCSVAction(cl).handle()
#print(output.getvalue())
self.assertEqual('id,username,address,password\r\n'
'1,admin,[hidden],[hidden]\r\n'
'2,anonymous,[hidden],[hidden]\r\n'
'3,Chef,[hidden],[hidden]\r\n'
'4,mary,[hidden],[hidden]\r\n'
'5,demo,demo@test.test,%s\r\n'%(passwd),
output.getvalue())
def testCSVExportWithId(self):
cl = self._make_client({'@columns': 'id,name'}, nodeid=None,
userid='1')
cl.classname = 'status'
output = StringIO()
cl.request = MockNull()
cl.request.wfile = output
actions.ExportCSVWithIdAction(cl).handle()
self.assertEqual('id,name\r\n1,unread\r\n2,deferred\r\n3,chatting\r\n'
'4,need-eg\r\n5,in-progress\r\n6,testing\r\n7,done-cbb\r\n'
'8,resolved\r\n',
output.getvalue())
def testCSVExportWithIdBadColumnName(self):
cl = self._make_client({'@columns': 'falseid,name'}, nodeid=None,
userid='1')
cl.classname = 'status'
output = StringIO()
cl.request = MockNull()
cl.request.wfile = output
self.assertRaises(exceptions.NotFound,
actions.ExportCSVWithIdAction(cl).handle)
def testCSVExportWithIdFailPermissionBadColumn(self):
cl = self._make_client({'@columns': 'id,email,password'}, nodeid=None,
userid='2')
cl.classname = 'user'
output = StringIO()
cl.request = MockNull()
cl.request.wfile = output
# used to be self.assertRaises(exceptions.Unauthorised,
# but not acting like the column name is not found
# see issue2550755 - should this return Unauthorised?
# The unauthorised user should never get to the point where
# they can determine if the column name is valid or not.
self.assertRaises(exceptions.NotFound,
actions.ExportCSVWithIdAction(cl).handle)
def testCSVExportWithIdFailPermissionValidColumn(self):
cl = self._make_client({'@columns': 'id,address,password'}, nodeid=None,
userid='2')
cl.classname = 'user'
output = StringIO()
cl.request = MockNull()
cl.request.wfile = output
# used to be self.assertRaises(exceptions.Unauthorised,
# but not acting like the column name is not found
self.assertRaises(exceptions.Unauthorised,
actions.ExportCSVWithIdAction(cl).handle)
class TemplateHtmlRendering(unittest.TestCase):
''' try to test the rendering code for tal '''
def setUp(self):
self.dirname = '_test_template'
# set up and open a tracker
self.instance = setupTracker(self.dirname)
# open the database
self.db = self.instance.open('admin')
self.db.tx_Source = "web"
self.db.user.create(username='Chef', address='chef@bork.bork.bork',
realname='Bork, Chef', roles='User')
self.db.user.create(username='mary', address='mary@test.test',
roles='User', realname='Contrary, Mary')
self.db.post_init()
# create a client instance and hijack write_html
self.client = client.Client(self.instance, "user",
{'PATH_INFO':'/user', 'REQUEST_METHOD':'POST'},
form=db_test_base.makeForm({"@template": "item"}))
self.client._error_message = []
self.client._ok_message = []
self.client.db = self.db
self.client.userid = '1'
self.client.language = ('en',)
self.client.session_api = MockNull(_sid="1234567890")
self.output = []
# ugly hack to get html_write to return data here.
def html_write(s):
self.output.append(s)
# hijack html_write
self.client.write_html = html_write
self.db.issue.create(title='foo')
def tearDown(self):
self.db.close()
try:
shutil.rmtree(self.dirname)
except OSError as error:
if error.errno not in (errno.ENOENT, errno.ESRCH): raise
def testrenderFrontPage(self):
self.client.renderFrontPage("hello world RaNdOmJunk")
# make sure we can find the "hello world RaNdOmJunk"
# message in the output.
self.assertNotEqual(-1,
self.output[0].index('
hello world RaNdOmJunk
'))
# make sure we can find issue 1 title foo in the output
self.assertNotEqual(-1,
self.output[0].index('foo'))
# make sure we can find the last SHA1 sum line at the end of the
# page
self.assertNotEqual(-1,
self.output[0].index(''))
def testrenderContext(self):
# set up the client;
# run determine_context to set the required client attributes
# run renderContext(); check result for proper page
# this will generate the default home page like
# testrenderFrontPage
self.client.form=db_test_base.makeForm({})
self.client.path = ''
self.client.determine_context()
self.assertEqual((self.client.classname, self.client.template, self.client.nodeid), (None, '', None))
self.assertEqual(self.client._ok_message, [])
result = self.client.renderContext()
self.assertNotEqual(-1,
result.index(''))
# now look at the user index page
self.client.form=db_test_base.makeForm(
{ "@ok_message": "ok message", "@template": "index"})
self.client.path = 'user'
self.client.determine_context()
self.assertEqual((self.client.classname, self.client.template, self.client.nodeid), ('user', 'index', None))
self.assertEqual(self.client._ok_message, ['ok message'])
result = self.client.renderContext()
self.assertNotEqual(-1, result.index('User listing - Roundup issue tracker'))
self.assertNotEqual(-1, result.index('ok message'))
# print result
def testRenderAltTemplates(self):
# check that right page is returned when rendering
# @template=oktempl|errortmpl
# set up the client;
# run determine_context to set the required client attributes
# run renderContext(); check result for proper page
# Test ok state template that uses user.forgotten.html
self.client.form=db_test_base.makeForm({"@template": "forgotten|item"})
self.client.path = 'user'
self.client.determine_context()
self.client.session_api = MockNull(_sid="1234567890")
self.assertEqual(
(self.client.classname, self.client.template, self.client.nodeid),
('user', 'forgotten|item', None))
self.assertEqual(self.client._ok_message, [])
result = self.client.renderContext()
print(result)
# sha1sum of classic tracker user.forgotten.template must be found
sha1sum = ''
self.assertNotEqual(-1, result.index(sha1sum))
# now set an error in the form to get error template user.item.html
self.client.form=db_test_base.makeForm({"@template": "forgotten|item",
"@error_message": "this is an error"})
self.client.path = 'user'
self.client.determine_context()
self.assertEqual(
(self.client.classname, self.client.template, self.client.nodeid),
('user', 'forgotten|item', None))
self.assertEqual(self.client._ok_message, [])
self.assertEqual(self.client._error_message, ["this is an error"])
result = self.client.renderContext()
print(result)
# sha1sum of classic tracker user.item.template must be found
sha1sum = ''
self.assertNotEqual(-1, result.index(sha1sum))
def testexamine_url(self):
''' test the examine_url function '''
def te(url, exception, raises=ValueError):
with self.assertRaises(raises) as cm:
examine_url(url)
self.assertEqual(cm.exception.args, (exception,))
action = actions.Action(self.client)
examine_url = action.examine_url
# Christmas tree url: test of every component that passes
self.assertEqual(
examine_url("http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue"),
'http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue')
# allow replacing http with https if base is http
self.assertEqual(
examine_url("https://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue"),
'https://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue')
# change base to use https and make sure we don't redirect to http
saved_base = action.base
action.base = "https://tracker.example/cgi-bin/roundup.cgi/bugs/"
te("http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue",
'Base url https://tracker.example/cgi-bin/roundup.cgi/bugs/ requires https. Redirect url http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue uses http.')
action.base = saved_base
# url doesn't have to be valid to roundup, just has to be contained
# inside of roundup. No zoik class is defined
self.assertEqual(examine_url("http://tracker.example/cgi-bin/roundup.cgi/bugs/zoik7;parm=bar?@template=foo&parm=(zot)#issue"), "http://tracker.example/cgi-bin/roundup.cgi/bugs/zoik7;parm=bar?@template=foo&parm=(zot)#issue")
# test with wonky schemes
te("email://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue",
'Unrecognized scheme in email://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue')
te("http%3a//tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue", 'Unrecognized scheme in http%3a//tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue')
# test different netloc/path prefix
# assert port
te("http://tracker.example:1025/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue",'Net location in http://tracker.example:1025/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue does not match base: tracker.example')
#assert user
te("http://user@tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue", 'Net location in http://user@tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue does not match base: tracker.example')
#assert user:password
te("http://user:pass@tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue", 'Net location in http://user:pass@tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue does not match base: tracker.example')
# try localhost http scheme
te("http://localhost/cgi-bin/roundup.cgi/bugs/user3", 'Net location in http://localhost/cgi-bin/roundup.cgi/bugs/user3 does not match base: tracker.example')
# try localhost https scheme
te("https://localhost/cgi-bin/roundup.cgi/bugs/user3", 'Net location in https://localhost/cgi-bin/roundup.cgi/bugs/user3 does not match base: tracker.example')
# try different host
te("http://bad.guys.are.us/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue", 'Net location in http://bad.guys.are.us/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue does not match base: tracker.example')
# change the base path to .../bug from .../bugs
te("http://tracker.example/cgi-bin/roundup.cgi/bug/user3;parm=bar?@template=foo&parm=(zot)#issue", 'Base path /cgi-bin/roundup.cgi/bugs/ is not a prefix for url http://tracker.example/cgi-bin/roundup.cgi/bug/user3;parm=bar?@template=foo&parm=(zot)#issue')
# change the base path eliminate - in cgi-bin
te("http://tracker.example/cgibin/roundup.cgi/bug/user3;parm=bar?@template=foo&parm=(zot)#issue",'Base path /cgi-bin/roundup.cgi/bugs/ is not a prefix for url http://tracker.example/cgibin/roundup.cgi/bug/user3;parm=bar?@template=foo&parm=(zot)#issue')
# scan for unencoded characters
# we skip schema and net location since unencoded character
# are allowed only by an explicit match to a reference.
#
# break components with unescaped character '<'
# path component
te("http://tracker.example/cgi-bin/roundup.cgi/bugs/&parm=(zot)#issue", 'Query component (@template=&parm=(zot)) in http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=&parm=(zot)#issue is not properly escaped')
# fragment component
te("http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#iss