Code

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