Code

2d63ed4c26df63cb442ecfdcc4dbd933ad219981
[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     def testPasswordMigration(self):
429         chef = self.db.user.lookup('Chef')
430         form = dict(__login_name='Chef', __login_password='foo')
431         cl = self._make_client(form)
432         # assume that the "best" algorithm is the first one and doesn't
433         # need migration, all others should be migrated.
434         for scheme in password.Password.deprecated_schemes:
435             pw1 = password.Password('foo', scheme=scheme)
436             self.assertEqual(pw1.needs_migration(), True)
437             self.db.user.set(chef, password=pw1)
438             self.db.commit()
439             actions.LoginAction(cl).handle()
440             pw = self.db.user.get(chef, 'password')
441             self.assertEqual(pw, 'foo')
442             self.assertEqual(pw.needs_migration(), False)
443         pw1 = pw
444         self.assertEqual(pw1.needs_migration(), False)
445         scheme = password.Password.known_schemes[0]
446         self.assertEqual(scheme, pw1.scheme)
447         actions.LoginAction(cl).handle()
448         pw = self.db.user.get(chef, 'password')
449         self.assertEqual(pw, 'foo')
450         self.assertEqual(pw, pw1)
452     #
453     # Boolean
454     #
455     def testEmptyBoolean(self):
456         self.assertEqual(self.parseForm({'boolean': ''}),
457             ({('test', None): {}}, []))
458         self.assertEqual(self.parseForm({'boolean': ' '}),
459             ({('test', None): {}}, []))
460         self.assertRaises(FormError, self.parseForm, {'boolean': ['', '']})
462     def testSetBoolean(self):
463         self.assertEqual(self.parseForm({'boolean': 'yes'}),
464             ({('test', None): {'boolean': 1}}, []))
465         self.assertEqual(self.parseForm({'boolean': 'a\r\nb\r\n'}),
466             ({('test', None): {'boolean': 0}}, []))
467         nodeid = self.db.test.create(boolean=1)
468         self.assertEqual(self.parseForm({'boolean': 'yes'}, 'test', nodeid),
469             ({('test', nodeid): {}}, []))
470         nodeid = self.db.test.create(boolean=0)
471         self.assertEqual(self.parseForm({'boolean': 'no'}, 'test', nodeid),
472             ({('test', nodeid): {}}, []))
474     def testEmptyBooleanSet(self):
475         nodeid = self.db.test.create(boolean=0)
476         self.assertEqual(self.parseForm({'boolean': ''}, 'test', nodeid),
477             ({('test', nodeid): {'boolean': None}}, []))
478         nodeid = self.db.test.create(boolean=1)
479         self.assertEqual(self.parseForm({'boolean': ' '}, 'test', nodeid),
480             ({('test', nodeid): {'boolean': None}}, []))
482     def testRequiredBoolean(self):
483         self.assertRaises(FormError, self.parseForm, {'boolean': '',
484             ':required': 'boolean'})
485         try:
486             self.parseForm({'boolean': 'no', ':required': 'boolean'})
487         except FormError:
488             self.fail('boolean "no" raised "required missing"')
490     #
491     # Number
492     #
493     def testEmptyNumber(self):
494         self.assertEqual(self.parseForm({'number': ''}),
495             ({('test', None): {}}, []))
496         self.assertEqual(self.parseForm({'number': ' '}),
497             ({('test', None): {}}, []))
498         self.assertRaises(FormError, self.parseForm, {'number': ['', '']})
500     def testInvalidNumber(self):
501         self.assertRaises(FormError, self.parseForm, {'number': 'hi, mum!'})
503     def testSetNumber(self):
504         self.assertEqual(self.parseForm({'number': '1'}),
505             ({('test', None): {'number': 1}}, []))
506         self.assertEqual(self.parseForm({'number': '0'}),
507             ({('test', None): {'number': 0}}, []))
508         self.assertEqual(self.parseForm({'number': '\n0\n'}),
509             ({('test', None): {'number': 0}}, []))
511     def testSetNumberReplaceOne(self):
512         nodeid = self.db.test.create(number=1)
513         self.assertEqual(self.parseForm({'number': '1'}, 'test', nodeid),
514             ({('test', nodeid): {}}, []))
515         self.assertEqual(self.parseForm({'number': '0'}, 'test', nodeid),
516             ({('test', nodeid): {'number': 0}}, []))
518     def testSetNumberReplaceZero(self):
519         nodeid = self.db.test.create(number=0)
520         self.assertEqual(self.parseForm({'number': '0'}, 'test', nodeid),
521             ({('test', nodeid): {}}, []))
523     def testSetNumberReplaceNone(self):
524         nodeid = self.db.test.create()
525         self.assertEqual(self.parseForm({'number': '0'}, 'test', nodeid),
526             ({('test', nodeid): {'number': 0}}, []))
527         self.assertEqual(self.parseForm({'number': '1'}, 'test', nodeid),
528             ({('test', nodeid): {'number': 1}}, []))
530     def testEmptyNumberSet(self):
531         nodeid = self.db.test.create(number=0)
532         self.assertEqual(self.parseForm({'number': ''}, 'test', nodeid),
533             ({('test', nodeid): {'number': None}}, []))
534         nodeid = self.db.test.create(number=1)
535         self.assertEqual(self.parseForm({'number': ' '}, 'test', nodeid),
536             ({('test', nodeid): {'number': None}}, []))
538     def testRequiredNumber(self):
539         self.assertRaises(FormError, self.parseForm, {'number': '',
540             ':required': 'number'})
541         try:
542             self.parseForm({'number': '0', ':required': 'number'})
543         except FormError:
544             self.fail('number "no" raised "required missing"')
546     #
547     # Date
548     #
549     def testEmptyDate(self):
550         self.assertEqual(self.parseForm({'date': ''}),
551             ({('test', None): {}}, []))
552         self.assertEqual(self.parseForm({'date': ' '}),
553             ({('test', None): {}}, []))
554         self.assertRaises(FormError, self.parseForm, {'date': ['', '']})
556     def testInvalidDate(self):
557         self.assertRaises(FormError, self.parseForm, {'date': '12'})
559     def testSetDate(self):
560         self.assertEqual(self.parseForm({'date': '2003-01-01'}),
561             ({('test', None): {'date': date.Date('2003-01-01')}}, []))
562         nodeid = self.db.test.create(date=date.Date('2003-01-01'))
563         self.assertEqual(self.parseForm({'date': '2003-01-01'}, 'test',
564             nodeid), ({('test', nodeid): {}}, []))
566     def testEmptyDateSet(self):
567         nodeid = self.db.test.create(date=date.Date('.'))
568         self.assertEqual(self.parseForm({'date': ''}, 'test', nodeid),
569             ({('test', nodeid): {'date': None}}, []))
570         nodeid = self.db.test.create(date=date.Date('1970-01-01.00:00:00'))
571         self.assertEqual(self.parseForm({'date': ' '}, 'test', nodeid),
572             ({('test', nodeid): {'date': None}}, []))
574     #
575     # Test multiple items in form
576     #
577     def testMultiple(self):
578         self.assertEqual(self.parseForm({'string': 'a', 'issue-1@title': 'b'}),
579             ({('test', None): {'string': 'a'},
580               ('issue', '-1'): {'title': 'b'}
581              }, []))
583     def testMultipleExistingContext(self):
584         nodeid = self.db.test.create()
585         self.assertEqual(self.parseForm({'string': 'a', 'issue-1@title': 'b'},
586             'test', nodeid),({('test', nodeid): {'string': 'a'},
587             ('issue', '-1'): {'title': 'b'}}, []))
589     def testLinking(self):
590         self.assertEqual(self.parseForm({
591             'string': 'a',
592             'issue-1@add@nosy': '1',
593             'issue-2@link@superseder': 'issue-1',
594             }),
595             ({('test', None): {'string': 'a'},
596               ('issue', '-1'): {'nosy': ['1']},
597              },
598              [('issue', '-2', 'superseder', [('issue', '-1')])
599              ]
600             )
601         )
603     def testMessages(self):
604         self.assertEqual(self.parseForm({
605             'msg-1@content': 'asdf',
606             'msg-2@content': 'qwer',
607             '@link@messages': 'msg-1, msg-2'}),
608             ({('test', None): {},
609               ('msg', '-2'): {'content': 'qwer'},
610               ('msg', '-1'): {'content': 'asdf'}},
611              [('test', None, 'messages', [('msg', '-1'), ('msg', '-2')])]
612             )
613         )
615     def testLinkBadDesignator(self):
616         self.assertRaises(FormError, self.parseForm,
617             {'test-1@link@link': 'blah'})
618         self.assertRaises(FormError, self.parseForm,
619             {'test-1@link@link': 'issue'})
621     def testLinkNotLink(self):
622         self.assertRaises(FormError, self.parseForm,
623             {'test-1@link@boolean': 'issue-1'})
624         self.assertRaises(FormError, self.parseForm,
625             {'test-1@link@string': 'issue-1'})
627     def testBackwardsCompat(self):
628         res = self.parseForm({':note': 'spam'}, 'issue')
629         date = res[0][('msg', '-1')]['date']
630         self.assertEqual(res, ({('issue', None): {}, ('msg', '-1'):
631             {'content': 'spam', 'author': '1', 'date': date}},
632             [('issue', None, 'messages', [('msg', '-1')])]))
633         file = FileUpload('foo', 'foo.txt')
634         self.assertEqual(self.parseForm({':file': file}, 'issue'),
635             ({('issue', None): {}, ('file', '-1'): {'content': 'foo',
636             'name': 'foo.txt', 'type': 'text/plain'}},
637             [('issue', None, 'files', [('file', '-1')])]))
639     #
640     # SECURITY
641     #
642     # XXX test all default permissions
643     def _make_client(self, form, classname='user', nodeid='1',
644            userid='2', template='item'):
645         cl = client.Client(self.instance, None, {'PATH_INFO':'/',
646             'REQUEST_METHOD':'POST'}, makeForm(form))
647         cl.classname = classname
648         if nodeid is not None:
649             cl.nodeid = nodeid
650         cl.db = self.db
651         cl.userid = userid
652         cl.language = ('en',)
653         cl.error_message = []
654         cl.template = template
655         return cl
657     def testClassPermission(self):
658         cl = self._make_client(dict(username='bob'))
659         self.failUnlessRaises(exceptions.Unauthorised,
660             actions.EditItemAction(cl).handle)
661         cl.nodeid = '1'
662         self.assertRaises(exceptions.Unauthorised,
663             actions.EditItemAction(cl).handle)
665     def testCheckAndPropertyPermission(self):
666         self.db.security.permissions = {}
667         def own_record(db, userid, itemid):
668             return userid == itemid
669         p = self.db.security.addPermission(name='Edit', klass='user',
670             check=own_record, properties=("password", ))
671         self.db.security.addPermissionToRole('User', p)
673         cl = self._make_client(dict(username='bob'))
674         self.assertRaises(exceptions.Unauthorised,
675             actions.EditItemAction(cl).handle)
676         cl = self._make_client(dict(roles='User,Admin'), userid='4', nodeid='4')
677         self.assertRaises(exceptions.Unauthorised,
678             actions.EditItemAction(cl).handle)
679         cl = self._make_client(dict(roles='User,Admin'), userid='4')
680         self.assertRaises(exceptions.Unauthorised,
681             actions.EditItemAction(cl).handle)
682         cl = self._make_client(dict(roles='User,Admin'))
683         self.assertRaises(exceptions.Unauthorised,
684             actions.EditItemAction(cl).handle)
685         # working example, mary may change her pw
686         cl = self._make_client({'password':'ob', '@confirm@password':'ob'},
687             nodeid='4', userid='4')
688         self.assertRaises(exceptions.Redirect,
689             actions.EditItemAction(cl).handle)
690         cl = self._make_client({'password':'bob', '@confirm@password':'bob'})
691         self.failUnlessRaises(exceptions.Unauthorised,
692             actions.EditItemAction(cl).handle)
694     def testCreatePermission(self):
695         # this checks if we properly differentiate between create and
696         # edit permissions
697         self.db.security.permissions = {}
698         self.db.security.addRole(name='UserAdd')
699         # Don't allow roles
700         p = self.db.security.addPermission(name='Create', klass='user',
701             properties=("username", "password", "address",
702             "alternate_address", "realname", "phone", "organisation",
703             "timezone"))
704         self.db.security.addPermissionToRole('UserAdd', p)
705         # Don't allow roles *and* don't allow username
706         p = self.db.security.addPermission(name='Edit', klass='user',
707             properties=("password", "address", "alternate_address",
708             "realname", "phone", "organisation", "timezone"))
709         self.db.security.addPermissionToRole('UserAdd', p)
710         self.db.user.set('4', roles='UserAdd')
712         # anonymous may not
713         cl = self._make_client({'username':'new_user', 'password':'secret',
714             '@confirm@password':'secret', 'address':'new_user@bork.bork',
715             'roles':'Admin'}, nodeid=None, userid='2')
716         self.assertRaises(exceptions.Unauthorised,
717             actions.NewItemAction(cl).handle)
718         # Don't allow creating new user with roles
719         cl = self._make_client({'username':'new_user', 'password':'secret',
720             '@confirm@password':'secret', 'address':'new_user@bork.bork',
721             'roles':'Admin'}, nodeid=None, userid='4')
722         self.assertRaises(exceptions.Unauthorised,
723             actions.NewItemAction(cl).handle)
724         self.assertEqual(cl.error_message,[])
725         # this should work
726         cl = self._make_client({'username':'new_user', 'password':'secret',
727             '@confirm@password':'secret', 'address':'new_user@bork.bork'},
728             nodeid=None, userid='4')
729         self.assertRaises(exceptions.Redirect,
730             actions.NewItemAction(cl).handle)
731         self.assertEqual(cl.error_message,[])
732         # don't allow changing (my own) username (in this example)
733         cl = self._make_client(dict(username='new_user42'), userid='4')
734         self.assertRaises(exceptions.Unauthorised,
735             actions.EditItemAction(cl).handle)
736         cl = self._make_client(dict(username='new_user42'), userid='4',
737             nodeid='4')
738         self.assertRaises(exceptions.Unauthorised,
739             actions.EditItemAction(cl).handle)
740         # don't allow changing (my own) roles
741         cl = self._make_client(dict(roles='User,Admin'), userid='4',
742             nodeid='4')
743         self.assertRaises(exceptions.Unauthorised,
744             actions.EditItemAction(cl).handle)
745         cl = self._make_client(dict(roles='User,Admin'), userid='4')
746         self.assertRaises(exceptions.Unauthorised,
747             actions.EditItemAction(cl).handle)
748         cl = self._make_client(dict(roles='User,Admin'))
749         self.assertRaises(exceptions.Unauthorised,
750             actions.EditItemAction(cl).handle)
752     def testSearchPermission(self):
753         # this checks if we properly check for search permissions
754         self.db.security.permissions = {}
755         self.db.security.addRole(name='User')
756         self.db.security.addRole(name='Project')
757         self.db.security.addPermissionToRole('User', 'Web Access')
758         self.db.security.addPermissionToRole('Project', 'Web Access')
759         # Allow viewing department
760         p = self.db.security.addPermission(name='View', klass='department')
761         self.db.security.addPermissionToRole('User', p)
762         # Allow viewing interesting things (but not department) on iss
763         # But users might only view issues where they are on nosy
764         # (so in the real world the check method would be better)
765         p = self.db.security.addPermission(name='View', klass='iss',
766             properties=("title", "status"), check=lambda x,y,z: True)
767         self.db.security.addPermissionToRole('User', p)
768         # Allow all relevant roles access to stat
769         p = self.db.security.addPermission(name='View', klass='stat')
770         self.db.security.addPermissionToRole('User', p)
771         self.db.security.addPermissionToRole('Project', p)
772         # Allow role "Project" access to whole iss
773         p = self.db.security.addPermission(name='View', klass='iss')
774         self.db.security.addPermissionToRole('Project', p)
776         department = self.instance.backend.Class(self.db, "department",
777             name=hyperdb.String())
778         status = self.instance.backend.Class(self.db, "stat",
779             name=hyperdb.String())
780         issue = self.instance.backend.Class(self.db, "iss",
781             title=hyperdb.String(), status=hyperdb.Link('stat'),
782             department=hyperdb.Link('department'))
784         d1 = department.create(name='d1')
785         d2 = department.create(name='d2')
786         open = status.create(name='open')
787         closed = status.create(name='closed')
788         issue.create(title='i1', status=open, department=d2)
789         issue.create(title='i2', status=open, department=d1)
790         issue.create(title='i2', status=closed, department=d1)
792         chef = self.db.user.lookup('Chef')
793         mary = self.db.user.lookup('mary')
794         self.db.user.set(chef, roles = 'User, Project')
796         perm = self.db.security.hasPermission
797         search = self.db.security.hasSearchPermission
798         self.assert_(perm('View', chef, 'iss', 'department', '1'))
799         self.assert_(perm('View', chef, 'iss', 'department', '2'))
800         self.assert_(perm('View', chef, 'iss', 'department', '3'))
801         self.assert_(search(chef, 'iss', 'department'))
803         self.assert_(not perm('View', mary, 'iss', 'department'))
804         self.assert_(perm('View', mary, 'iss', 'status'))
805         # Conditionally allow view of whole iss (check is False here,
806         # this might check for department owner in the real world)
807         p = self.db.security.addPermission(name='View', klass='iss',
808             check=lambda x,y,z: False)
809         self.db.security.addPermissionToRole('User', p)
810         self.assert_(perm('View', mary, 'iss', 'department'))
811         self.assert_(not perm('View', mary, 'iss', 'department', '1'))
812         self.assert_(not search(mary, 'iss', 'department'))
814         self.assert_(perm('View', mary, 'iss', 'status'))
815         self.assert_(not search(mary, 'iss', 'status'))
816         # Allow user to search for iss.status
817         p = self.db.security.addPermission(name='Search', klass='iss',
818             properties=("status",))
819         self.db.security.addPermissionToRole('User', p)
820         self.assert_(search(mary, 'iss', 'status'))
822         dep = {'@action':'search','columns':'id','@filter':'department',
823             'department':'1'}
824         stat = {'@action':'search','columns':'id','@filter':'status',
825             'status':'1'}
826         depsort = {'@action':'search','columns':'id','@sort':'department'}
827         depgrp = {'@action':'search','columns':'id','@group':'department'}
829         # Filter on department ignored for role 'User':
830         cl = self._make_client(dep, classname='iss', nodeid=None, userid=mary,
831             template='index')
832         h = HTMLRequest(cl)
833         self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
834         # Filter on department works for role 'Project':
835         cl = self._make_client(dep, classname='iss', nodeid=None, userid=chef,
836             template='index')
837         h = HTMLRequest(cl)
838         self.assertEqual([x.id for x in h.batch()],['2', '3'])
839         # Filter on status works for all:
840         cl = self._make_client(stat, classname='iss', nodeid=None, userid=mary,
841             template='index')
842         h = HTMLRequest(cl)
843         self.assertEqual([x.id for x in h.batch()],['1', '2'])
844         cl = self._make_client(stat, classname='iss', nodeid=None, userid=chef,
845             template='index')
846         h = HTMLRequest(cl)
847         self.assertEqual([x.id for x in h.batch()],['1', '2'])
848         # Sorting and grouping for class Project works:
849         cl = self._make_client(depsort, classname='iss', nodeid=None,
850             userid=chef, template='index')
851         h = HTMLRequest(cl)
852         self.assertEqual([x.id for x in h.batch()],['2', '3', '1'])
853         cl = self._make_client(depgrp, classname='iss', nodeid=None,
854             userid=chef, template='index')
855         h = HTMLRequest(cl)
856         self.assertEqual([x.id for x in h.batch()],['2', '3', '1'])
857         # Sorting and grouping for class User fails:
858         cl = self._make_client(depsort, classname='iss', nodeid=None,
859             userid=mary, template='index')
860         h = HTMLRequest(cl)
861         self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
862         cl = self._make_client(depgrp, classname='iss', nodeid=None,
863             userid=mary, template='index')
864         h = HTMLRequest(cl)
865         self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
867     def testRoles(self):
868         cl = self._make_client({})
869         self.db.user.set('1', roles='aDmin,    uSer')
870         item = HTMLItem(cl, 'user', '1')
871         self.assert_(item.hasRole('Admin'))
872         self.assert_(item.hasRole('User'))
873         self.assert_(item.hasRole('AdmiN'))
874         self.assert_(item.hasRole('UseR'))
875         self.assert_(item.hasRole('UseR','Admin'))
876         self.assert_(item.hasRole('UseR','somethingelse'))
877         self.assert_(item.hasRole('somethingelse','Admin'))
878         self.assert_(not item.hasRole('userr'))
879         self.assert_(not item.hasRole('adminn'))
880         self.assert_(not item.hasRole(''))
881         self.assert_(not item.hasRole(' '))
882         self.db.user.set('1', roles='')
883         self.assert_(not item.hasRole(''))
885     def testCSVExport(self):
886         cl = self._make_client({'@columns': 'id,name'}, nodeid=None,
887             userid='1')
888         cl.classname = 'status'
889         output = StringIO.StringIO()
890         cl.request = MockNull()
891         cl.request.wfile = output
892         actions.ExportCSVAction(cl).handle()
893         self.assertEquals('id,name\r\n1,unread\r\n2,deferred\r\n3,chatting\r\n'
894             '4,need-eg\r\n5,in-progress\r\n6,testing\r\n7,done-cbb\r\n'
895             '8,resolved\r\n',
896             output.getvalue())
898     def testCSVExportFailPermission(self):
899         cl = self._make_client({'@columns': 'id,email,password'}, nodeid=None,
900             userid='2')
901         cl.classname = 'user'
902         output = StringIO.StringIO()
903         cl.request = MockNull()
904         cl.request.wfile = output
905         self.assertRaises(exceptions.Unauthorised,
906             actions.ExportCSVAction(cl).handle)
909 def test_suite():
910     suite = unittest.TestSuite()
912 def test_suite():
913     suite = unittest.TestSuite()
914     suite.addTest(unittest.makeSuite(FormTestCase))
915     suite.addTest(unittest.makeSuite(MessageTestCase))
916     return suite
918 if __name__ == '__main__':
919     runner = unittest.TextTestRunner()
920     unittest.main(testRunner=runner)
922 # vim: set filetype=python sts=4 sw=4 et si :