from __future__ import print_function
import unittest
from cgi import FieldStorage, MiniFieldStorage
from roundup.cgi.templating import *
from .test_actions import MockNull, true
import pytest
from .pytest_patcher import mark_class
if ReStructuredText:
skip_rst = lambda func, *args, **kwargs: func
else:
skip_rst = mark_class(pytest.mark.skip(
reason='ReStructuredText not available'))
if StructuredText:
skip_stext = lambda func, *args, **kwargs: func
else:
skip_stext = mark_class(pytest.mark.skip(
reason='StructuredText not available'))
import roundup.cgi.templating
if roundup.cgi.templating._import_mistune():
skip_mistune = lambda func, *args, **kwargs: func
else:
skip_mistune = mark_class(pytest.mark.skip(
reason='mistune not available'))
if roundup.cgi.templating._import_markdown2():
skip_markdown2 = lambda func, *args, **kwargs: func
else:
skip_markdown2 = mark_class(pytest.mark.skip(
reason='markdown2 not available'))
if roundup.cgi.templating._import_markdown():
skip_markdown = lambda func, *args, **kwargs: func
else:
skip_markdown = mark_class(pytest.mark.skip(
reason='markdown not available'))
from roundup.anypy.strings import u2s, s2u
class MockDatabase(MockNull):
def getclass(self, name):
return self.classes[name]
# setup for csrf testing of otks database api
storage = {}
def set(self, key, **props):
MockDatabase.storage[key] = {}
MockDatabase.storage[key].update(props)
def get(self, key, field, default=None):
if key not in MockDatabase.storage:
return default
return MockDatabase.storage[key][field]
def exists(self,key):
return key in MockDatabase.storage
def getOTKManager(self):
return MockDatabase()
class TemplatingTestCase(unittest.TestCase):
def setUp(self):
self.form = FieldStorage()
self.client = MockNull()
self.client.db = db = MockDatabase()
db.security.hasPermission = lambda *args, **kw: True
self.client.form = self.form
# add client props for testing anti_csrf_nonce
self.client.session_api = MockNull(_sid="1234567890")
self.client.db.getuid = lambda : 10
self.client.db.config = {'WEB_CSRF_TOKEN_LIFETIME': 10, 'MARKDOWN_BREAK_ON_NEWLINE': False }
class HTMLDatabaseTestCase(TemplatingTestCase):
def test_HTMLDatabase___getitem__(self):
db = HTMLDatabase(self.client)
self.assertTrue(isinstance(db['issue'], HTMLClass))
# following assertions are invalid
# since roundup/cgi/templating.py r1.173.
# HTMLItem is function, not class,
# but HTMLUserClass and HTMLUser are passed on.
# these classes are no more. they have ceased to be.
#self.assertTrue(isinstance(db['user'], HTMLUserClass))
#self.assertTrue(isinstance(db['issue1'], HTMLItem))
#self.assertTrue(isinstance(db['user1'], HTMLUser))
def test_HTMLDatabase___getattr__(self):
db = HTMLDatabase(self.client)
self.assertTrue(isinstance(db.issue, HTMLClass))
# see comment in test_HTMLDatabase___getitem__
#self.assertTrue(isinstance(db.user, HTMLUserClass))
#self.assertTrue(isinstance(db.issue1, HTMLItem))
#self.assertTrue(isinstance(db.user1, HTMLUser))
def test_HTMLDatabase_classes(self):
db = HTMLDatabase(self.client)
db._db.classes = {'issue':MockNull(), 'user': MockNull()}
db.classes()
class FunctionsTestCase(TemplatingTestCase):
def test_lookupIds(self):
db = HTMLDatabase(self.client)
def lookup(key):
if key == 'ok':
return '1'
if key == 'fail':
raise KeyError('fail')
return key
db._db.classes = {'issue': MockNull(lookup=lookup)}
prop = MockNull(classname='issue')
self.assertEqual(lookupIds(db._db, prop, ['1','2']), ['1','2'])
self.assertEqual(lookupIds(db._db, prop, ['ok','2']), ['1','2'])
self.assertEqual(lookupIds(db._db, prop, ['ok', 'fail'], 1),
['1', 'fail'])
self.assertEqual(lookupIds(db._db, prop, ['ok', 'fail']), ['1'])
def test_lookupKeys(self):
db = HTMLDatabase(self.client)
def get(entry, key):
return {'1': 'green', '2': 'eggs'}.get(entry, entry)
shrubbery = MockNull(get=get)
db._db.classes = {'shrubbery': shrubbery}
self.assertEqual(lookupKeys(shrubbery, 'spam', ['1','2']),
['green', 'eggs'])
self.assertEqual(lookupKeys(shrubbery, 'spam', ['ok','2']), ['ok',
'eggs'])
class HTMLClassTestCase(TemplatingTestCase) :
def test_link(self):
"""Make sure lookup of a Link property works even in the
presence of multiple values in the form."""
def lookup(key) :
self.assertEqual(key, key.strip())
return "Status%s"%key
self.form.list.append(MiniFieldStorage("status", "1"))
self.form.list.append(MiniFieldStorage("status", "2"))
status = hyperdb.Link("status")
self.client.db.classes = dict \
( issue = MockNull(getprops = lambda : dict(status = status))
, status = MockNull(get = lambda id, name : id, lookup = lookup)
)
cls = HTMLClass(self.client, "issue")
cls["status"]
def test_multilink(self):
"""`lookup` of an item will fail if leading or trailing whitespace
has not been stripped.
"""
def lookup(key) :
self.assertEqual(key, key.strip())
return "User%s"%key
self.form.list.append(MiniFieldStorage("nosy", "1, 2"))
nosy = hyperdb.Multilink("user")
self.client.db.classes = dict \
( issue = MockNull(getprops = lambda : dict(nosy = nosy))
, user = MockNull(get = lambda id, name : id, lookup = lookup)
)
cls = HTMLClass(self.client, "issue")
cls["nosy"]
def test_anti_csrf_nonce(self):
'''call the csrf creation function and do basic length test
Store the data in a mock db with the same api as the otk
db. Make sure nonce is 64 chars long. Lookup the nonce in
db and retrieve data. Verify that the nonce lifetime is
correct (within 1 second of 1 week - lifetime), the uid is
correct (1), the dummy sid is correct.
Consider three cases:
* create nonce via module function setting lifetime
* create nonce via TemplatingUtils method setting lifetime
* create nonce via module function with default lifetime
'''
# the value below is number of seconds in a week.
week_seconds = 604800
otks=self.client.db.getOTKManager()
for test in [ 'module', 'template', 'default_time' ]:
print("Testing:", test)
if test == 'module':
# test the module function
nonce1 = anti_csrf_nonce(self.client, lifetime=1)
# lifetime * 60 is the offset
greater_than = week_seconds - 1 * 60
elif test == 'template':
# call the function through the TemplatingUtils class
cls = TemplatingUtils(self.client)
nonce1 = cls.anti_csrf_nonce(lifetime=5)
greater_than = week_seconds - 5 * 60
elif test == 'default_time':
# use the module function but with no lifetime
nonce1 = anti_csrf_nonce(self.client)
# see above for web nonce lifetime.
greater_than = week_seconds - 10 * 60
self.assertEqual(len(nonce1), 64)
uid = otks.get(nonce1, 'uid', default=None)
sid = otks.get(nonce1, 'sid', default=None)
timestamp = otks.get(nonce1, '__timestamp', default=None)
self.assertEqual(uid, 10)
self.assertEqual(sid, self.client.session_api._sid)
now = time.time()
print("now, timestamp, greater, difference",
now, timestamp, greater_than, now - timestamp)
# lower bound of the difference is above. Upper bound
# of difference is run time between time.time() in
# the call to anti_csrf_nonce and the time.time() call
# that assigns ts above. I declare that difference
# to be less than 1 second for this to pass.
self.assertEqual(True,
greater_than <= now - timestamp < (greater_than + 1) )
def test_string_url_quote(self):
''' test that urlquote quotes the string '''
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', 'test string< foo@bar')
self.assertEqual(p.url_quote(), 'test%20string%3C%20foo%40bar')
def test_string_email(self):
''' test that email obscures the email '''
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', 'rouilj@foo.example.com')
self.assertEqual(p.email(), 'rouilj at foo example ...')
def test_string_plain_or_hyperlinked(self):
''' test that email obscures the email '''
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', 'A string with rouilj@example.com embedded < html')
self.assertEqual(p.plain(), 'A string with rouilj@example.com embedded < html')
self.assertEqual(p.plain(escape=1), 'A string <b> with rouilj@example.com embedded < html</b>')
self.assertEqual(p.plain(hyperlink=1), 'A string <b> with rouilj@example.com embedded < html</b>')
self.assertEqual(p.plain(escape=1, hyperlink=1), 'A string <b> with rouilj@example.com embedded < html</b>')
self.assertEqual(p.hyperlinked(), 'A string <b> with rouilj@example.com embedded < html</b>')
@skip_rst
def test_string_rst(self):
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'A string with cmeerw@example.com *embedded* \u00df'))
# test case to make sure include directive is disabled
q = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'\n\n.. include:: XyZrMt.html\n\n\n\n'))
q_result=u'''
System Message: WARNING/2 (<string>, line 3)
"include" directive disabled.
.. include:: XyZrMt.html
<badtag>
'''
# test case to make sure raw directive is disabled
r = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'\n\n.. raw:: html\n\n \n\n'))
r_result='''
System Message: WARNING/2 (<string>, line 3)
"raw" directive disabled.
.. raw:: html
<badtag>
'''
# test case to make sure javascript and data url's aren't turned
# into links
s = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'\njavascript:badcode data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=='))
s_result = '
\n'))
def test_string_field(self):
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', 'A string with rouilj@example.com embedded < html')
self.assertEqual(p.field(), '')
def test_string_multiline(self):
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', 'A string with rouilj@example.com embedded < html')
self.assertEqual(p.multiline(), '')
self.assertEqual(p.multiline(rows=300, cols=100, **{'class':'css_class'}), '')
def test_url_match(self):
'''Test the URL regular expression in StringHTMLProperty.
'''
def t(s, nothing=False, **groups):
m = StringHTMLProperty.hyper_re.search(s)
if nothing:
if m:
self.assertEqual(m, None, '%r matched (%r)'%(s, m.groupdict()))
return
else:
self.assertNotEqual(m, None, '%r did not match'%s)
d = m.groupdict()
for g in groups:
self.assertEqual(d[g], groups[g], '%s %r != %r in %r'%(g, d[g],
groups[g], s))
#t('123.321.123.321', 'url')
t('http://localhost/', url='http://localhost/')
t('http://roundup.net/', url='http://roundup.net/')
t('http://richard@localhost/', url='http://richard@localhost/')
t('http://richard:sekrit@localhost/',
url='http://richard:sekrit@localhost/')
t('', url='HTTP://roundup.net/')
t('www.a.ex', url='www.a.ex')
t('foo.a.ex', nothing=True)
t('StDevValidTimeSeries.GetObservation', nothing=True)
t('http://a.ex', url='http://a.ex')
t('http://a.ex/?foo&bar=baz\\.@!$%()qwerty',
url='http://a.ex/?foo&bar=baz\\.@!$%()qwerty')
t('www.foo.net', url='www.foo.net')
t('richard@com.example', email='richard@com.example')
t('r@a.com', email='r@a.com')
t('i1', **{'class':'i', 'id':'1'})
t('item123', **{'class':'item', 'id':'123'})
t('www.user:pass@host.net', email='pass@host.net')
t('user:pass@www.host.net', url='user:pass@www.host.net')
t('123.35', nothing=True)
t('-.3535', nothing=True)
def test_url_replace(self):
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', '')
def t(s): return p.hyper_re.sub(p._hyper_repl, s)
ae = self.assertEqual
ae(t('item123123123123'), 'item123123123123')
ae(t('http://roundup.net/'),
'http://roundup.net/')
ae(t('<HTTP://roundup.net/>'),
'<HTTP://roundup.net/>')
ae(t('<http://roundup.net/>.'),
'<http://roundup.net/>.')
ae(t('<www.roundup.net>'),
'<www.roundup.net>')
ae(t('(www.roundup.net)'),
'(www.roundup.net)')
ae(t('foo http://msdn.microsoft.com/en-us/library/ms741540(VS.85).aspx bar'),
'foo '
'http://msdn.microsoft.com/en-us/library/ms741540(VS.85).aspx bar')
ae(t('(e.g. http://en.wikipedia.org/wiki/Python_(programming_language))'),
'(e.g. '
'http://en.wikipedia.org/wiki/Python_(programming_language))')
ae(t('(e.g. http://en.wikipedia.org/wiki/Python_(programming_language)).'),
'(e.g. '
'http://en.wikipedia.org/wiki/Python_(programming_language)).')
ae(t('(e.g. http://en.wikipedia.org/wiki/Python_(programming_language))>.'),
'(e.g. '
'http://en.wikipedia.org/wiki/Python_(programming_language))>.')
ae(t('(e.g. http://en.wikipedia.org/wiki/Python_(programming_language>)).'),
'(e.g. '
'http://en.wikipedia.org/wiki/Python_(programming_language>)).')
for c in '.,;:!':
# trailing punctuation is not included
ae(t('http://roundup.net/%c ' % c),
'http://roundup.net/%c ' % c)
# but it's included if it's part of the URL
ae(t('http://roundup.net/%c/' % c),
'http://roundup.net/%c/' % (c, c))
def test_input_html4(self):
# boolean attributes are just the attribute name
# indicate with attr=None or attr="attr"
# e.g. disabled
input=input_html4(required=None, size=30)
self.assertEqual(input, '')
input=input_html4(required="required", size=30)
self.assertEqual(input, '')
attrs={"required": None, "class": "required", "size": 30}
input=input_html4(**attrs)
self.assertEqual(input, '')
attrs={"disabled": "disabled", "class": "required", "size": 30}
input=input_html4(**attrs)
self.assertEqual(input, '')
def test_input_xhtml(self):
# boolean attributes are attribute name="attribute name"
# indicate with attr=None or attr="attr"
# e.g. disabled="disabled"
input=input_xhtml(required=None, size=30)
self.assertEqual(input, '')
input=input_xhtml(required="required", size=30)
self.assertEqual(input, '')
attrs={"required": None, "class": "required", "size": 30}
input=input_xhtml(**attrs)
self.assertEqual(input, '')
attrs={"disabled": "disabled", "class": "required", "size": 30}
input=input_xhtml(**attrs)
self.assertEqual(input, '')
# common markdown test cases
class MarkdownTests:
def mangleMarkdown2(self, s):
''' markdown2's rel=nofollow support on 'a' tags isn't programmable.
So we are using it's builtin nofollow support. Mangle the string
so that it matches the test case.
turn: into
Also if it is a mailto url, we don't expect rel="nofollow",
so delete it.
turn: into
Also when a title is present it is put in a different place
from markdown, so fix it to normalize.
turn:
into
'''
if type(self) == Markdown2TestCase and s.find('a rel="nofollow"') != -1:
if s.find('href="mailto:') == -1:
# not a mailto url
if 'rel="nofollow"' in s:
if 'title="' in s:
s = s.replace(' rel="nofollow" ', ' ').replace(' title=', ' rel="nofollow noopener" title=')
else:
s = s.replace(' rel="nofollow" ', ' ').replace('">', '" rel="nofollow noopener">')
return s
else:
# a mailto url
return s.replace(' rel="nofollow" ', ' ')
return s
def test_string_markdown(self):
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'A string with *embedded* \u00df'))
self.assertEqual(p.markdown().strip(), u2s(u'
'))
def test_string_markdown_link(self):
# markdown2 and markdown escape the email address
try:
from html import unescape as html_unescape
except ImportError:
from HTMLParser import HTMLParser
html_unescape = HTMLParser().unescape
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'A link '))
m = html_unescape(p.markdown().strip())
m = self.mangleMarkdown2(m)
self.assertEqual(m, u2s(u'
'))
def test_string_markdown_javascript_link(self):
# make sure we don't get a "javascript:" link
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u''))
self.assertTrue(p.markdown().find('href="javascript:') == -1)
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'[link](javascript:alert(1))'))
self.assertTrue(p.markdown().find('href="javascript:') == -1)
def test_string_markdown_data_link(self):
# make sure we don't get a "data:" link
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u''))
print(p.markdown())
self.assertTrue(p.markdown().find('href="data:') == -1)
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'[data link](data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==)'))
print(p.markdown())
self.assertTrue(p.markdown().find('href="data:') == -1)
def test_string_markdown_forced_line_break(self):
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'This is a set of text \n:that should have a break \n:at newlines. Each \n:colon should be the start of an html line'))
# sigh different backends render this differently:
# of text
# of text
# etc.
# Rather than using a different result for each
# renderer, look for ' \n\n```\nline 1\nline 2\n```\n\nnew paragraph'))
self.assertEqual(p.markdown().strip().replace('\n\n', '\n'), u2s(u'
embedded code block <pre>
\n
line 1\nline 2\n
\n
new </pre> paragraph
'))
def test_string_markdown_code_block_attribute(self):
''' also verify that embedded html is escaped '''
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'embedded code block
\n\n``` python\nline 1\nline 2\n```\n\nnew
paragraph'))
m = p.markdown().strip()
print(m)
if type(self) == MistuneTestCase:
self.assertEqual(m.replace('\n\n','\n'), '
')
p = StringHTMLProperty(self.client, 'test', '1', None, 'test', u2s(u'[label](http://example.com/ "a title")'))
m = p.markdown(hyperlink=1)
m = self.mangleMarkdown2(m)
self.assertEqual(m.rstrip('\n'), '