Code

- Add explicit "Search" permissions, see Security Fix below.
[roundup.git] / test / test_cgi.py
1 #
2 # Copyright (c) 2003 Richard Jones, rjones@ekit-inc.com
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # This module is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10 #
11 # $Id: test_cgi.py,v 1.36 2008-08-07 06:12:57 richard Exp $
13 import unittest, os, shutil, errno, sys, difflib, cgi, re, StringIO
15 from roundup.cgi import client, actions, exceptions
16 from roundup.cgi.exceptions import FormError
17 from roundup.cgi.templating import HTMLItem, HTMLRequest
18 from roundup.cgi.form_parser import FormParser
19 from roundup import init, instance, password, hyperdb, date
21 from mocknull import MockNull
23 import db_test_base
25 NEEDS_INSTANCE = 1
27 class FileUpload:
28     def __init__(self, content, filename):
29         self.content = content
30         self.filename = filename
32 def makeForm(args):
33     form = cgi.FieldStorage()
34     for k,v in args.items():
35         if type(v) is type([]):
36             [form.list.append(cgi.MiniFieldStorage(k, x)) for x in v]
37         elif isinstance(v, FileUpload):
38             x = cgi.MiniFieldStorage(k, v.content)
39             x.filename = v.filename
40             form.list.append(x)
41         else:
42             form.list.append(cgi.MiniFieldStorage(k, v))
43     return form
45 cm = client.clean_message
46 class MessageTestCase(unittest.TestCase):
47     def testCleanMessageOK(self):
48         self.assertEqual(cm('<br>x<br />'), '<br>x<br />')
49         self.assertEqual(cm('<i>x</i>'), '<i>x</i>')
50         self.assertEqual(cm('<b>x</b>'), '<b>x</b>')
51         self.assertEqual(cm('<a href="y">x</a>'),
52             '<a href="y">x</a>')
53         self.assertEqual(cm('<BR>x<BR />'), '<BR>x<BR />')
54         self.assertEqual(cm('<I>x</I>'), '<I>x</I>')
55         self.assertEqual(cm('<B>x</B>'), '<B>x</B>')
56         self.assertEqual(cm('<A HREF="y">x</A>'),
57             '<A HREF="y">x</A>')
59     def testCleanMessageBAD(self):
60         self.assertEqual(cm('<script>x</script>'),
61             '&lt;script&gt;x&lt;/script&gt;')
62         self.assertEqual(cm('<iframe>x</iframe>'),
63             '&lt;iframe&gt;x&lt;/iframe&gt;')
65 class FormTestCase(unittest.TestCase):
66     def setUp(self):
67         self.dirname = '_test_cgi_form'
68         # set up and open a tracker
69         self.instance = db_test_base.setupTracker(self.dirname)
71         # open the database
72         self.db = self.instance.open('admin')
73         self.db.user.create(username='Chef', address='chef@bork.bork.bork',
74             realname='Bork, Chef', roles='User')
75         self.db.user.create(username='mary', address='mary@test.test',
76             roles='User', realname='Contrary, Mary')
78         test = self.instance.backend.Class(self.db, "test",
79             string=hyperdb.String(), number=hyperdb.Number(),
80             boolean=hyperdb.Boolean(), link=hyperdb.Link('test'),
81             multilink=hyperdb.Multilink('test'), date=hyperdb.Date(),
82             messages=hyperdb.Multilink('msg'), interval=hyperdb.Interval())
84         # compile the labels re
85         classes = '|'.join(self.db.classes.keys())
86         self.FV_SPECIAL = re.compile(FormParser.FV_LABELS%classes,
87             re.VERBOSE)
89     def parseForm(self, form, classname='test', nodeid=None):
90         cl = client.Client(self.instance, None, {'PATH_INFO':'/',
91             'REQUEST_METHOD':'POST'}, makeForm(form))
92         cl.classname = classname
93         cl.nodeid = nodeid
94         cl.language = ('en',)
95         cl.db = self.db
96         return cl.parsePropsFromForm(create=1)
98     def tearDown(self):
99         self.db.close()
100         try:
101             shutil.rmtree(self.dirname)
102         except OSError, error:
103             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
105     #
106     # form label extraction
107     #
108     def tl(self, s, c, i, a, p):
109         m = self.FV_SPECIAL.match(s)
110         self.assertNotEqual(m, None)
111         d = m.groupdict()
112         self.assertEqual(d['classname'], c)
113         self.assertEqual(d['id'], i)
114         for action in 'required add remove link note file'.split():
115             if a == action:
116                 self.assertNotEqual(d[action], None)
117             else:
118                 self.assertEqual(d[action], None)
119         self.assertEqual(d['propname'], p)
121     def testLabelMatching(self):
122         self.tl('<propname>', None, None, None, '<propname>')
123         self.tl(':required', None, None, 'required', None)
124         self.tl(':confirm:<propname>', None, None, 'confirm', '<propname>')
125         self.tl(':add:<propname>', None, None, 'add', '<propname>')
126         self.tl(':remove:<propname>', None, None, 'remove', '<propname>')
127         self.tl(':link:<propname>', None, None, 'link', '<propname>')
128         self.tl('test1:<prop>', 'test', '1', None, '<prop>')
129         self.tl('test1:required', 'test', '1', 'required', None)
130         self.tl('test1:add:<prop>', 'test', '1', 'add', '<prop>')
131         self.tl('test1:remove:<prop>', 'test', '1', 'remove', '<prop>')
132         self.tl('test1:link:<prop>', 'test', '1', 'link', '<prop>')
133         self.tl('test1:confirm:<prop>', 'test', '1', 'confirm', '<prop>')
134         self.tl('test-1:<prop>', 'test', '-1', None, '<prop>')
135         self.tl('test-1:required', 'test', '-1', 'required', None)
136         self.tl('test-1:add:<prop>', 'test', '-1', 'add', '<prop>')
137         self.tl('test-1:remove:<prop>', 'test', '-1', 'remove', '<prop>')
138         self.tl('test-1:link:<prop>', 'test', '-1', 'link', '<prop>')
139         self.tl('test-1:confirm:<prop>', 'test', '-1', 'confirm', '<prop>')
140         self.tl(':note', None, None, 'note', None)
141         self.tl(':file', None, None, 'file', None)
143     #
144     # Empty form
145     #
146     def testNothing(self):
147         self.assertEqual(self.parseForm({}), ({('test', None): {}}, []))
149     def testNothingWithRequired(self):
150         self.assertRaises(FormError, self.parseForm, {':required': 'string'})
151         self.assertRaises(FormError, self.parseForm,
152             {':required': 'title,status', 'status':'1'}, 'issue')
153         self.assertRaises(FormError, self.parseForm,
154             {':required': ['title','status'], 'status':'1'}, 'issue')
155         self.assertRaises(FormError, self.parseForm,
156             {':required': 'status', 'status':''}, 'issue')
157         self.assertRaises(FormError, self.parseForm,
158             {':required': 'nosy', 'nosy':''}, 'issue')
159         self.assertRaises(FormError, self.parseForm,
160             {':required': 'msg-1@content', 'msg-1@content':''}, 'issue')
161         self.assertRaises(FormError, self.parseForm,
162             {':required': 'msg-1@content'}, 'issue')
164     #
165     # Nonexistant edit
166     #
167     def testEditNonexistant(self):
168         self.assertRaises(FormError, self.parseForm, {'boolean': ''},
169             'test', '1')
171     #
172     # String
173     #
174     def testEmptyString(self):
175         self.assertEqual(self.parseForm({'string': ''}),
176             ({('test', None): {}}, []))
177         self.assertEqual(self.parseForm({'string': ' '}),
178             ({('test', None): {}}, []))
179         self.assertRaises(FormError, self.parseForm, {'string': ['', '']})
181     def testSetString(self):
182         self.assertEqual(self.parseForm({'string': 'foo'}),
183             ({('test', None): {'string': 'foo'}}, []))
184         self.assertEqual(self.parseForm({'string': 'a\r\nb\r\n'}),
185             ({('test', None): {'string': 'a\nb'}}, []))
186         nodeid = self.db.issue.create(title='foo')
187         self.assertEqual(self.parseForm({'title': 'foo'}, 'issue', nodeid),
188             ({('issue', nodeid): {}}, []))
190     def testEmptyStringSet(self):
191         nodeid = self.db.issue.create(title='foo')
192         self.assertEqual(self.parseForm({'title': ''}, 'issue', nodeid),
193             ({('issue', nodeid): {'title': None}}, []))
194         nodeid = self.db.issue.create(title='foo')
195         self.assertEqual(self.parseForm({'title': ' '}, 'issue', nodeid),
196             ({('issue', nodeid): {'title': None}}, []))
198     def testStringLinkId(self):
199         self.db.status.set('1', name='2')
200         self.db.status.set('2', name='1')
201         issue = self.db.issue.create(title='i1-status1', status='1')
202         self.assertEqual(self.db.issue.get(issue,'status'),'1')
203         self.assertEqual(self.db.status.lookup('1'),'2')
204         self.assertEqual(self.db.status.lookup('2'),'1')
205         form = cgi.FieldStorage()
206         cl = client.Client(self.instance, None, {'PATH_INFO':'/'}, form)
207         cl.classname = 'issue'
208         cl.nodeid = issue
209         cl.db = self.db
210         cl.language = ('en',)
211         item = HTMLItem(cl, 'issue', issue)
212         self.assertEqual(item.status.id, '1')
213         self.assertEqual(item.status.name, '2')
215     def testStringMultilinkId(self):
216         id = self.db.keyword.create(name='2')
217         self.assertEqual(id,'1')
218         id = self.db.keyword.create(name='1')
219         self.assertEqual(id,'2')
220         issue = self.db.issue.create(title='i1-status1', keyword=['1'])
221         self.assertEqual(self.db.issue.get(issue,'keyword'),['1'])
222         self.assertEqual(self.db.keyword.lookup('1'),'2')
223         self.assertEqual(self.db.keyword.lookup('2'),'1')
224         form = cgi.FieldStorage()
225         cl = client.Client(self.instance, None, {'PATH_INFO':'/'}, form)
226         cl.classname = 'issue'
227         cl.nodeid = issue
228         cl.db = self.db
229         cl.language = ('en',)
230         cl.userid = '1'
231         item = HTMLItem(cl, 'issue', issue)
232         for keyword in item.keyword:
233             self.assertEqual(keyword.id, '1')
234             self.assertEqual(keyword.name, '2')
236     def testFileUpload(self):
237         file = FileUpload('foo', 'foo.txt')
238         self.assertEqual(self.parseForm({'content': file}, 'file'),
239             ({('file', None): {'content': 'foo', 'name': 'foo.txt',
240             'type': 'text/plain'}}, []))
242     def testEditFileClassAttributes(self):
243         self.assertEqual(self.parseForm({'name': 'foo.txt',
244                                          'type': 'application/octet-stream'},
245                                         'file'),
246                          ({('file', None): {'name': 'foo.txt',
247                                             'type': 'application/octet-stream'}},[]))
249     #
250     # Link
251     #
252     def testEmptyLink(self):
253         self.assertEqual(self.parseForm({'link': ''}),
254             ({('test', None): {}}, []))
255         self.assertEqual(self.parseForm({'link': ' '}),
256             ({('test', None): {}}, []))
257         self.assertRaises(FormError, self.parseForm, {'link': ['', '']})
258         self.assertEqual(self.parseForm({'link': '-1'}),
259             ({('test', None): {}}, []))
261     def testSetLink(self):
262         self.assertEqual(self.parseForm({'status': 'unread'}, 'issue'),
263             ({('issue', None): {'status': '1'}}, []))
264         self.assertEqual(self.parseForm({'status': '1'}, 'issue'),
265             ({('issue', None): {'status': '1'}}, []))
266         nodeid = self.db.issue.create(status='unread')
267         self.assertEqual(self.parseForm({'status': 'unread'}, 'issue', nodeid),
268             ({('issue', nodeid): {}}, []))
270     def testUnsetLink(self):
271         nodeid = self.db.issue.create(status='unread')
272         self.assertEqual(self.parseForm({'status': '-1'}, 'issue', nodeid),
273             ({('issue', nodeid): {'status': None}}, []))
275     def testInvalidLinkValue(self):
276 # XXX This is not the current behaviour - should we enforce this?
277 #        self.assertRaises(IndexError, self.parseForm,
278 #            {'status': '4'}))
279         self.assertRaises(FormError, self.parseForm, {'link': 'frozzle'})
280         self.assertRaises(FormError, self.parseForm, {'status': 'frozzle'},
281             'issue')
283     #
284     # Multilink
285     #
286     def testEmptyMultilink(self):
287         self.assertEqual(self.parseForm({'nosy': ''}),
288             ({('test', None): {}}, []))
289         self.assertEqual(self.parseForm({'nosy': ' '}),
290             ({('test', None): {}}, []))
292     def testSetMultilink(self):
293         self.assertEqual(self.parseForm({'nosy': '1'}, 'issue'),
294             ({('issue', None): {'nosy': ['1']}}, []))
295         self.assertEqual(self.parseForm({'nosy': 'admin'}, 'issue'),
296             ({('issue', None): {'nosy': ['1']}}, []))
297         self.assertEqual(self.parseForm({'nosy': ['1','2']}, 'issue'),
298             ({('issue', None): {'nosy': ['1','2']}}, []))
299         self.assertEqual(self.parseForm({'nosy': '1,2'}, 'issue'),
300             ({('issue', None): {'nosy': ['1','2']}}, []))
301         self.assertEqual(self.parseForm({'nosy': 'admin,2'}, 'issue'),
302             ({('issue', None): {'nosy': ['1','2']}}, []))
304     def testMixedMultilink(self):
305         form = cgi.FieldStorage()
306         form.list.append(cgi.MiniFieldStorage('nosy', '1,2'))
307         form.list.append(cgi.MiniFieldStorage('nosy', '3'))
308         cl = client.Client(self.instance, None, {'PATH_INFO':'/'}, form)
309         cl.classname = 'issue'
310         cl.nodeid = None
311         cl.db = self.db
312         cl.language = ('en',)
313         self.assertEqual(cl.parsePropsFromForm(create=1),
314             ({('issue', None): {'nosy': ['1','2', '3']}}, []))
316     def testEmptyMultilinkSet(self):
317         nodeid = self.db.issue.create(nosy=['1','2'])
318         self.assertEqual(self.parseForm({'nosy': ''}, 'issue', nodeid),
319             ({('issue', nodeid): {'nosy': []}}, []))
320         nodeid = self.db.issue.create(nosy=['1','2'])
321         self.assertEqual(self.parseForm({'nosy': ' '}, 'issue', nodeid),
322             ({('issue', nodeid): {'nosy': []}}, []))
323         self.assertEqual(self.parseForm({'nosy': '1,2'}, 'issue', nodeid),
324             ({('issue', nodeid): {}}, []))
326     def testInvalidMultilinkValue(self):
327 # XXX This is not the current behaviour - should we enforce this?
328 #        self.assertRaises(IndexError, self.parseForm,
329 #            {'nosy': '4'}))
330         self.assertRaises(FormError, self.parseForm, {'nosy': 'frozzle'},
331             'issue')
332         self.assertRaises(FormError, self.parseForm, {'nosy': '1,frozzle'},
333             'issue')
334         self.assertRaises(FormError, self.parseForm, {'multilink': 'frozzle'})
336     def testMultilinkAdd(self):
337         nodeid = self.db.issue.create(nosy=['1'])
338         # do nothing
339         self.assertEqual(self.parseForm({':add:nosy': ''}, 'issue', nodeid),
340             ({('issue', nodeid): {}}, []))
342         # do something ;)
343         self.assertEqual(self.parseForm({':add:nosy': '2'}, 'issue', nodeid),
344             ({('issue', nodeid): {'nosy': ['1','2']}}, []))
345         self.assertEqual(self.parseForm({':add:nosy': '2,mary'}, 'issue',
346             nodeid), ({('issue', nodeid): {'nosy': ['1','2','4']}}, []))
347         self.assertEqual(self.parseForm({':add:nosy': ['2','3']}, 'issue',
348             nodeid), ({('issue', nodeid): {'nosy': ['1','2','3']}}, []))
350     def testMultilinkAddNew(self):
351         self.assertEqual(self.parseForm({':add:nosy': ['2','3']}, 'issue'),
352             ({('issue', None): {'nosy': ['2','3']}}, []))
354     def testMultilinkRemove(self):
355         nodeid = self.db.issue.create(nosy=['1','2'])
356         # do nothing
357         self.assertEqual(self.parseForm({':remove:nosy': ''}, 'issue', nodeid),
358             ({('issue', nodeid): {}}, []))
360         # do something ;)
361         self.assertEqual(self.parseForm({':remove:nosy': '1'}, 'issue',
362             nodeid), ({('issue', nodeid): {'nosy': ['2']}}, []))
363         self.assertEqual(self.parseForm({':remove:nosy': 'admin,2'},
364             'issue', nodeid), ({('issue', nodeid): {'nosy': []}}, []))
365         self.assertEqual(self.parseForm({':remove:nosy': ['1','2']},
366             'issue', nodeid), ({('issue', nodeid): {'nosy': []}}, []))
368         # add and remove
369         self.assertEqual(self.parseForm({':add:nosy': ['3'],
370             ':remove:nosy': ['1','2']},
371             'issue', nodeid), ({('issue', nodeid): {'nosy': ['3']}}, []))
373         # remove one that doesn't exist?
374         self.assertRaises(FormError, self.parseForm, {':remove:nosy': '4'},
375             'issue', nodeid)
377     def testMultilinkRetired(self):
378         self.db.user.retire('2')
379         self.assertEqual(self.parseForm({'nosy': ['2','3']}, 'issue'),
380             ({('issue', None): {'nosy': ['2','3']}}, []))
381         nodeid = self.db.issue.create(nosy=['1','2'])
382         self.assertEqual(self.parseForm({':remove:nosy': '2'}, 'issue',
383             nodeid), ({('issue', nodeid): {'nosy': ['1']}}, []))
384         self.assertEqual(self.parseForm({':add:nosy': '3'}, 'issue', nodeid),
385             ({('issue', nodeid): {'nosy': ['1','2','3']}}, []))
387     def testAddRemoveNonexistant(self):
388         self.assertRaises(FormError, self.parseForm, {':remove:foo': '2'},
389             'issue')
390         self.assertRaises(FormError, self.parseForm, {':add:foo': '2'},
391             'issue')
393     #
394     # Password
395     #
396     def testEmptyPassword(self):
397         self.assertEqual(self.parseForm({'password': ''}, 'user'),
398             ({('user', None): {}}, []))
399         self.assertEqual(self.parseForm({'password': ''}, 'user'),
400             ({('user', None): {}}, []))
401         self.assertRaises(FormError, self.parseForm, {'password': ['', '']},
402             'user')
403         self.assertRaises(FormError, self.parseForm, {'password': 'foo',
404             ':confirm:password': ['', '']}, 'user')
406     def testSetPassword(self):
407         self.assertEqual(self.parseForm({'password': 'foo',
408             ':confirm:password': 'foo'}, 'user'),
409             ({('user', None): {'password': 'foo'}}, []))
411     def testSetPasswordConfirmBad(self):
412         self.assertRaises(FormError, self.parseForm, {'password': 'foo'},
413             'user')
414         self.assertRaises(FormError, self.parseForm, {'password': 'foo',
415             ':confirm:password': 'bar'}, 'user')
417     def testEmptyPasswordNotSet(self):
418         nodeid = self.db.user.create(username='1',
419             password=password.Password('foo'))
420         self.assertEqual(self.parseForm({'password': ''}, 'user', nodeid),
421             ({('user', nodeid): {}}, []))
422         nodeid = self.db.user.create(username='2',
423             password=password.Password('foo'))
424         self.assertEqual(self.parseForm({'password': '',
425             ':confirm:password': ''}, 'user', nodeid),
426             ({('user', nodeid): {}}, []))
428     #
429     # Boolean
430     #
431     def testEmptyBoolean(self):
432         self.assertEqual(self.parseForm({'boolean': ''}),
433             ({('test', None): {}}, []))
434         self.assertEqual(self.parseForm({'boolean': ' '}),
435             ({('test', None): {}}, []))
436         self.assertRaises(FormError, self.parseForm, {'boolean': ['', '']})
438     def testSetBoolean(self):
439         self.assertEqual(self.parseForm({'boolean': 'yes'}),
440             ({('test', None): {'boolean': 1}}, []))
441         self.assertEqual(self.parseForm({'boolean': 'a\r\nb\r\n'}),
442             ({('test', None): {'boolean': 0}}, []))
443         nodeid = self.db.test.create(boolean=1)
444         self.assertEqual(self.parseForm({'boolean': 'yes'}, 'test', nodeid),
445             ({('test', nodeid): {}}, []))
446         nodeid = self.db.test.create(boolean=0)
447         self.assertEqual(self.parseForm({'boolean': 'no'}, 'test', nodeid),
448             ({('test', nodeid): {}}, []))
450     def testEmptyBooleanSet(self):
451         nodeid = self.db.test.create(boolean=0)
452         self.assertEqual(self.parseForm({'boolean': ''}, 'test', nodeid),
453             ({('test', nodeid): {'boolean': None}}, []))
454         nodeid = self.db.test.create(boolean=1)
455         self.assertEqual(self.parseForm({'boolean': ' '}, 'test', nodeid),
456             ({('test', nodeid): {'boolean': None}}, []))
458     def testRequiredBoolean(self):
459         self.assertRaises(FormError, self.parseForm, {'boolean': '',
460             ':required': 'boolean'})
461         try:
462             self.parseForm({'boolean': 'no', ':required': 'boolean'})
463         except FormError:
464             self.fail('boolean "no" raised "required missing"')
466     #
467     # Number
468     #
469     def testEmptyNumber(self):
470         self.assertEqual(self.parseForm({'number': ''}),
471             ({('test', None): {}}, []))
472         self.assertEqual(self.parseForm({'number': ' '}),
473             ({('test', None): {}}, []))
474         self.assertRaises(FormError, self.parseForm, {'number': ['', '']})
476     def testInvalidNumber(self):
477         self.assertRaises(FormError, self.parseForm, {'number': 'hi, mum!'})
479     def testSetNumber(self):
480         self.assertEqual(self.parseForm({'number': '1'}),
481             ({('test', None): {'number': 1}}, []))
482         self.assertEqual(self.parseForm({'number': '0'}),
483             ({('test', None): {'number': 0}}, []))
484         self.assertEqual(self.parseForm({'number': '\n0\n'}),
485             ({('test', None): {'number': 0}}, []))
487     def testSetNumberReplaceOne(self):
488         nodeid = self.db.test.create(number=1)
489         self.assertEqual(self.parseForm({'number': '1'}, 'test', nodeid),
490             ({('test', nodeid): {}}, []))
491         self.assertEqual(self.parseForm({'number': '0'}, 'test', nodeid),
492             ({('test', nodeid): {'number': 0}}, []))
494     def testSetNumberReplaceZero(self):
495         nodeid = self.db.test.create(number=0)
496         self.assertEqual(self.parseForm({'number': '0'}, 'test', nodeid),
497             ({('test', nodeid): {}}, []))
499     def testSetNumberReplaceNone(self):
500         nodeid = self.db.test.create()
501         self.assertEqual(self.parseForm({'number': '0'}, 'test', nodeid),
502             ({('test', nodeid): {'number': 0}}, []))
503         self.assertEqual(self.parseForm({'number': '1'}, 'test', nodeid),
504             ({('test', nodeid): {'number': 1}}, []))
506     def testEmptyNumberSet(self):
507         nodeid = self.db.test.create(number=0)
508         self.assertEqual(self.parseForm({'number': ''}, 'test', nodeid),
509             ({('test', nodeid): {'number': None}}, []))
510         nodeid = self.db.test.create(number=1)
511         self.assertEqual(self.parseForm({'number': ' '}, 'test', nodeid),
512             ({('test', nodeid): {'number': None}}, []))
514     def testRequiredNumber(self):
515         self.assertRaises(FormError, self.parseForm, {'number': '',
516             ':required': 'number'})
517         try:
518             self.parseForm({'number': '0', ':required': 'number'})
519         except FormError:
520             self.fail('number "no" raised "required missing"')
522     #
523     # Date
524     #
525     def testEmptyDate(self):
526         self.assertEqual(self.parseForm({'date': ''}),
527             ({('test', None): {}}, []))
528         self.assertEqual(self.parseForm({'date': ' '}),
529             ({('test', None): {}}, []))
530         self.assertRaises(FormError, self.parseForm, {'date': ['', '']})
532     def testInvalidDate(self):
533         self.assertRaises(FormError, self.parseForm, {'date': '12'})
535     def testSetDate(self):
536         self.assertEqual(self.parseForm({'date': '2003-01-01'}),
537             ({('test', None): {'date': date.Date('2003-01-01')}}, []))
538         nodeid = self.db.test.create(date=date.Date('2003-01-01'))
539         self.assertEqual(self.parseForm({'date': '2003-01-01'}, 'test',
540             nodeid), ({('test', nodeid): {}}, []))
542     def testEmptyDateSet(self):
543         nodeid = self.db.test.create(date=date.Date('.'))
544         self.assertEqual(self.parseForm({'date': ''}, 'test', nodeid),
545             ({('test', nodeid): {'date': None}}, []))
546         nodeid = self.db.test.create(date=date.Date('1970-01-01.00:00:00'))
547         self.assertEqual(self.parseForm({'date': ' '}, 'test', nodeid),
548             ({('test', nodeid): {'date': None}}, []))
550     #
551     # Test multiple items in form
552     #
553     def testMultiple(self):
554         self.assertEqual(self.parseForm({'string': 'a', 'issue-1@title': 'b'}),
555             ({('test', None): {'string': 'a'},
556               ('issue', '-1'): {'title': 'b'}
557              }, []))
559     def testMultipleExistingContext(self):
560         nodeid = self.db.test.create()
561         self.assertEqual(self.parseForm({'string': 'a', 'issue-1@title': 'b'},
562             'test', nodeid),({('test', nodeid): {'string': 'a'},
563             ('issue', '-1'): {'title': 'b'}}, []))
565     def testLinking(self):
566         self.assertEqual(self.parseForm({
567             'string': 'a',
568             'issue-1@add@nosy': '1',
569             'issue-2@link@superseder': 'issue-1',
570             }),
571             ({('test', None): {'string': 'a'},
572               ('issue', '-1'): {'nosy': ['1']},
573              },
574              [('issue', '-2', 'superseder', [('issue', '-1')])
575              ]
576             )
577         )
579     def testMessages(self):
580         self.assertEqual(self.parseForm({
581             'msg-1@content': 'asdf',
582             'msg-2@content': 'qwer',
583             '@link@messages': 'msg-1, msg-2'}),
584             ({('test', None): {},
585               ('msg', '-2'): {'content': 'qwer'},
586               ('msg', '-1'): {'content': 'asdf'}},
587              [('test', None, 'messages', [('msg', '-1'), ('msg', '-2')])]
588             )
589         )
591     def testLinkBadDesignator(self):
592         self.assertRaises(FormError, self.parseForm,
593             {'test-1@link@link': 'blah'})
594         self.assertRaises(FormError, self.parseForm,
595             {'test-1@link@link': 'issue'})
597     def testLinkNotLink(self):
598         self.assertRaises(FormError, self.parseForm,
599             {'test-1@link@boolean': 'issue-1'})
600         self.assertRaises(FormError, self.parseForm,
601             {'test-1@link@string': 'issue-1'})
603     def testBackwardsCompat(self):
604         res = self.parseForm({':note': 'spam'}, 'issue')
605         date = res[0][('msg', '-1')]['date']
606         self.assertEqual(res, ({('issue', None): {}, ('msg', '-1'):
607             {'content': 'spam', 'author': '1', 'date': date}},
608             [('issue', None, 'messages', [('msg', '-1')])]))
609         file = FileUpload('foo', 'foo.txt')
610         self.assertEqual(self.parseForm({':file': file}, 'issue'),
611             ({('issue', None): {}, ('file', '-1'): {'content': 'foo',
612             'name': 'foo.txt', 'type': 'text/plain'}},
613             [('issue', None, 'files', [('file', '-1')])]))
615     #
616     # SECURITY
617     #
618     # XXX test all default permissions
619     def _make_client(self, form, classname='user', nodeid='1',
620            userid='2', template='item'):
621         cl = client.Client(self.instance, None, {'PATH_INFO':'/',
622             'REQUEST_METHOD':'POST'}, makeForm(form))
623         cl.classname = classname
624         if nodeid is not None:
625             cl.nodeid = nodeid
626         cl.db = self.db
627         cl.userid = userid
628         cl.language = ('en',)
629         cl.error_message = []
630         cl.template = template
631         return cl
633     def testClassPermission(self):
634         cl = self._make_client(dict(username='bob'))
635         self.failUnlessRaises(exceptions.Unauthorised,
636             actions.EditItemAction(cl).handle)
637         cl.nodeid = '1'
638         self.assertRaises(exceptions.Unauthorised,
639             actions.EditItemAction(cl).handle)
641     def testCheckAndPropertyPermission(self):
642         self.db.security.permissions = {}
643         def own_record(db, userid, itemid):
644             return userid == itemid
645         p = self.db.security.addPermission(name='Edit', klass='user',
646             check=own_record, properties=("password", ))
647         self.db.security.addPermissionToRole('User', p)
649         cl = self._make_client(dict(username='bob'))
650         self.assertRaises(exceptions.Unauthorised,
651             actions.EditItemAction(cl).handle)
652         cl = self._make_client(dict(roles='User,Admin'), userid='4', nodeid='4')
653         self.assertRaises(exceptions.Unauthorised,
654             actions.EditItemAction(cl).handle)
655         cl = self._make_client(dict(roles='User,Admin'), userid='4')
656         self.assertRaises(exceptions.Unauthorised,
657             actions.EditItemAction(cl).handle)
658         cl = self._make_client(dict(roles='User,Admin'))
659         self.assertRaises(exceptions.Unauthorised,
660             actions.EditItemAction(cl).handle)
661         # working example, mary may change her pw
662         cl = self._make_client({'password':'ob', '@confirm@password':'ob'},
663             nodeid='4', userid='4')
664         self.assertRaises(exceptions.Redirect,
665             actions.EditItemAction(cl).handle)
666         cl = self._make_client({'password':'bob', '@confirm@password':'bob'})
667         self.failUnlessRaises(exceptions.Unauthorised,
668             actions.EditItemAction(cl).handle)
670     def testCreatePermission(self):
671         # this checks if we properly differentiate between create and
672         # edit permissions
673         self.db.security.permissions = {}
674         self.db.security.addRole(name='UserAdd')
675         # Don't allow roles
676         p = self.db.security.addPermission(name='Create', klass='user',
677             properties=("username", "password", "address",
678             "alternate_address", "realname", "phone", "organisation",
679             "timezone"))
680         self.db.security.addPermissionToRole('UserAdd', p)
681         # Don't allow roles *and* don't allow username
682         p = self.db.security.addPermission(name='Edit', klass='user',
683             properties=("password", "address", "alternate_address",
684             "realname", "phone", "organisation", "timezone"))
685         self.db.security.addPermissionToRole('UserAdd', p)
686         self.db.user.set('4', roles='UserAdd')
688         # anonymous may not
689         cl = self._make_client({'username':'new_user', 'password':'secret',
690             '@confirm@password':'secret', 'address':'new_user@bork.bork',
691             'roles':'Admin'}, nodeid=None, userid='2')
692         self.assertRaises(exceptions.Unauthorised,
693             actions.NewItemAction(cl).handle)
694         # Don't allow creating new user with roles
695         cl = self._make_client({'username':'new_user', 'password':'secret',
696             '@confirm@password':'secret', 'address':'new_user@bork.bork',
697             'roles':'Admin'}, nodeid=None, userid='4')
698         self.assertRaises(exceptions.Unauthorised,
699             actions.NewItemAction(cl).handle)
700         self.assertEqual(cl.error_message,[])
701         # this should work
702         cl = self._make_client({'username':'new_user', 'password':'secret',
703             '@confirm@password':'secret', 'address':'new_user@bork.bork'},
704             nodeid=None, userid='4')
705         self.assertRaises(exceptions.Redirect,
706             actions.NewItemAction(cl).handle)
707         self.assertEqual(cl.error_message,[])
708         # don't allow changing (my own) username (in this example)
709         cl = self._make_client(dict(username='new_user42'), userid='4')
710         self.assertRaises(exceptions.Unauthorised,
711             actions.EditItemAction(cl).handle)
712         cl = self._make_client(dict(username='new_user42'), userid='4',
713             nodeid='4')
714         self.assertRaises(exceptions.Unauthorised,
715             actions.EditItemAction(cl).handle)
716         # don't allow changing (my own) roles
717         cl = self._make_client(dict(roles='User,Admin'), userid='4',
718             nodeid='4')
719         self.assertRaises(exceptions.Unauthorised,
720             actions.EditItemAction(cl).handle)
721         cl = self._make_client(dict(roles='User,Admin'), userid='4')
722         self.assertRaises(exceptions.Unauthorised,
723             actions.EditItemAction(cl).handle)
724         cl = self._make_client(dict(roles='User,Admin'))
725         self.assertRaises(exceptions.Unauthorised,
726             actions.EditItemAction(cl).handle)
728     def testSearchPermission(self):
729         # this checks if we properly check for search permissions
730         self.db.security.permissions = {}
731         self.db.security.addRole(name='User')
732         self.db.security.addRole(name='Project')
733         self.db.security.addPermissionToRole('User', 'Web Access')
734         self.db.security.addPermissionToRole('Project', 'Web Access')
735         # Allow viewing department
736         p = self.db.security.addPermission(name='View', klass='department')
737         self.db.security.addPermissionToRole('User', p)
738         # Allow viewing interesting things (but not department) on iss
739         # But users might only view issues where they are on nosy
740         # (so in the real world the check method would be better)
741         p = self.db.security.addPermission(name='View', klass='iss',
742             properties=("title", "status"), check=lambda x,y,z: True)
743         self.db.security.addPermissionToRole('User', p)
744         # Allow role "Project" access to whole iss
745         p = self.db.security.addPermission(name='View', klass='iss')
746         self.db.security.addPermissionToRole('Project', p)
748         department = self.instance.backend.Class(self.db, "department",
749             name=hyperdb.String())
750         status = self.instance.backend.Class(self.db, "stat",
751             name=hyperdb.String())
752         issue = self.instance.backend.Class(self.db, "iss",
753             title=hyperdb.String(), status=hyperdb.Link('stat'),
754             department=hyperdb.Link('department'))
756         d1 = department.create(name='d1')
757         d2 = department.create(name='d2')
758         open = status.create(name='open')
759         closed = status.create(name='closed')
760         issue.create(title='i1', status=open, department=d2)
761         issue.create(title='i2', status=open, department=d1)
762         issue.create(title='i2', status=closed, department=d1)
764         chef = self.db.user.lookup('Chef')
765         mary = self.db.user.lookup('mary')
766         self.db.user.set(chef, roles = 'User, Project')
768         perm = self.db.security.hasPermission
769         search = self.db.security.hasSearchPermission
770         self.assert_(perm('View', chef, 'iss', 'department', '1'))
771         self.assert_(perm('View', chef, 'iss', 'department', '2'))
772         self.assert_(perm('View', chef, 'iss', 'department', '3'))
773         self.assert_(search(chef, 'iss', 'department'))
775         self.assert_(not perm('View', mary, 'iss', 'department'))
776         self.assert_(perm('View', mary, 'iss', 'status'))
777         # Conditionally allow view of whole iss (check is False here,
778         # this might check for department owner in the real world)
779         p = self.db.security.addPermission(name='View', klass='iss',
780             check=lambda x,y,z: False)
781         self.db.security.addPermissionToRole('User', p)
782         self.assert_(perm('View', mary, 'iss', 'department'))
783         self.assert_(not perm('View', mary, 'iss', 'department', '1'))
784         self.assert_(not search(mary, 'iss', 'department'))
786         self.assert_(perm('View', mary, 'iss', 'status'))
787         self.assert_(not search(mary, 'iss', 'status'))
788         # Allow user to search for iss.status
789         p = self.db.security.addPermission(name='Search', klass='iss',
790             properties=("status",))
791         self.db.security.addPermissionToRole('User', p)
792         self.assert_(search(mary, 'iss', 'status'))
794         dep = {'@action':'search','columns':'id','@filter':'department',
795             'department':'1'}
796         stat = {'@action':'search','columns':'id','@filter':'status',
797             'status':'1'}
798         depsort = {'@action':'search','columns':'id','@sort':'department'}
799         depgrp = {'@action':'search','columns':'id','@group':'department'}
801         # Filter on department ignored for role 'User':
802         cl = self._make_client(dep, classname='iss', nodeid=None, userid=mary,
803             template='index')
804         h = HTMLRequest(cl)
805         self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
806         # Filter on department works for role 'Project':
807         cl = self._make_client(dep, classname='iss', nodeid=None, userid=chef,
808             template='index')
809         h = HTMLRequest(cl)
810         self.assertEqual([x.id for x in h.batch()],['2', '3'])
811         # Filter on status works for all:
812         cl = self._make_client(stat, classname='iss', nodeid=None, userid=mary,
813             template='index')
814         h = HTMLRequest(cl)
815         self.assertEqual([x.id for x in h.batch()],['1', '2'])
816         cl = self._make_client(stat, classname='iss', nodeid=None, userid=chef,
817             template='index')
818         h = HTMLRequest(cl)
819         self.assertEqual([x.id for x in h.batch()],['1', '2'])
820         # Sorting and grouping for class Project works:
821         cl = self._make_client(depsort, classname='iss', nodeid=None,
822             userid=chef, template='index')
823         h = HTMLRequest(cl)
824         self.assertEqual([x.id for x in h.batch()],['2', '3', '1'])
825         cl = self._make_client(depgrp, classname='iss', nodeid=None,
826             userid=chef, template='index')
827         h = HTMLRequest(cl)
828         self.assertEqual([x.id for x in h.batch()],['2', '3', '1'])
829         # Sorting and grouping for class User fails:
830         cl = self._make_client(depsort, classname='iss', nodeid=None,
831             userid=mary, template='index')
832         h = HTMLRequest(cl)
833         self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
834         cl = self._make_client(depgrp, classname='iss', nodeid=None,
835             userid=mary, template='index')
836         h = HTMLRequest(cl)
837         self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
839     def testRoles(self):
840         cl = self._make_client({})
841         self.db.user.set('1', roles='aDmin,    uSer')
842         item = HTMLItem(cl, 'user', '1')
843         self.assert_(item.hasRole('Admin'))
844         self.assert_(item.hasRole('User'))
845         self.assert_(item.hasRole('AdmiN'))
846         self.assert_(item.hasRole('UseR'))
847         self.assert_(item.hasRole('UseR','Admin'))
848         self.assert_(item.hasRole('UseR','somethingelse'))
849         self.assert_(item.hasRole('somethingelse','Admin'))
850         self.assert_(not item.hasRole('userr'))
851         self.assert_(not item.hasRole('adminn'))
852         self.assert_(not item.hasRole(''))
853         self.assert_(not item.hasRole(' '))
854         self.db.user.set('1', roles='')
855         self.assert_(not item.hasRole(''))
857     def testCSVExport(self):
858         cl = self._make_client({'@columns': 'id,name'}, nodeid=None,
859             userid='1')
860         cl.classname = 'status'
861         output = StringIO.StringIO()
862         cl.request = MockNull()
863         cl.request.wfile = output
864         actions.ExportCSVAction(cl).handle()
865         self.assertEquals('id,name\r\n1,unread\r\n2,deferred\r\n3,chatting\r\n'
866             '4,need-eg\r\n5,in-progress\r\n6,testing\r\n7,done-cbb\r\n'
867             '8,resolved\r\n',
868             output.getvalue())
870     def testCSVExportFailPermission(self):
871         cl = self._make_client({'@columns': 'id,email,password'}, nodeid=None,
872             userid='2')
873         cl.classname = 'user'
874         output = StringIO.StringIO()
875         cl.request = MockNull()
876         cl.request.wfile = output
877         self.assertRaises(exceptions.Unauthorised,
878             actions.ExportCSVAction(cl).handle)
881 def test_suite():
882     suite = unittest.TestSuite()
884 def test_suite():
885     suite = unittest.TestSuite()
886     suite.addTest(unittest.makeSuite(FormTestCase))
887     suite.addTest(unittest.makeSuite(MessageTestCase))
888     return suite
890 if __name__ == '__main__':
891     runner = unittest.TextTestRunner()
892     unittest.main(testRunner=runner)
894 # vim: set filetype=python sts=4 sw=4 et si :