Code

Add "lookup" method to xmlrpc interface (Ralf Schlatterbeck)
[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 all relevant roles access to stat
745         p = self.db.security.addPermission(name='View', klass='stat')
746         self.db.security.addPermissionToRole('User', p)
747         self.db.security.addPermissionToRole('Project', p)
748         # Allow role "Project" access to whole iss
749         p = self.db.security.addPermission(name='View', klass='iss')
750         self.db.security.addPermissionToRole('Project', p)
752         department = self.instance.backend.Class(self.db, "department",
753             name=hyperdb.String())
754         status = self.instance.backend.Class(self.db, "stat",
755             name=hyperdb.String())
756         issue = self.instance.backend.Class(self.db, "iss",
757             title=hyperdb.String(), status=hyperdb.Link('stat'),
758             department=hyperdb.Link('department'))
760         d1 = department.create(name='d1')
761         d2 = department.create(name='d2')
762         open = status.create(name='open')
763         closed = status.create(name='closed')
764         issue.create(title='i1', status=open, department=d2)
765         issue.create(title='i2', status=open, department=d1)
766         issue.create(title='i2', status=closed, department=d1)
768         chef = self.db.user.lookup('Chef')
769         mary = self.db.user.lookup('mary')
770         self.db.user.set(chef, roles = 'User, Project')
772         perm = self.db.security.hasPermission
773         search = self.db.security.hasSearchPermission
774         self.assert_(perm('View', chef, 'iss', 'department', '1'))
775         self.assert_(perm('View', chef, 'iss', 'department', '2'))
776         self.assert_(perm('View', chef, 'iss', 'department', '3'))
777         self.assert_(search(chef, 'iss', 'department'))
779         self.assert_(not perm('View', mary, 'iss', 'department'))
780         self.assert_(perm('View', mary, 'iss', 'status'))
781         # Conditionally allow view of whole iss (check is False here,
782         # this might check for department owner in the real world)
783         p = self.db.security.addPermission(name='View', klass='iss',
784             check=lambda x,y,z: False)
785         self.db.security.addPermissionToRole('User', p)
786         self.assert_(perm('View', mary, 'iss', 'department'))
787         self.assert_(not perm('View', mary, 'iss', 'department', '1'))
788         self.assert_(not search(mary, 'iss', 'department'))
790         self.assert_(perm('View', mary, 'iss', 'status'))
791         self.assert_(not search(mary, 'iss', 'status'))
792         # Allow user to search for iss.status
793         p = self.db.security.addPermission(name='Search', klass='iss',
794             properties=("status",))
795         self.db.security.addPermissionToRole('User', p)
796         self.assert_(search(mary, 'iss', 'status'))
798         dep = {'@action':'search','columns':'id','@filter':'department',
799             'department':'1'}
800         stat = {'@action':'search','columns':'id','@filter':'status',
801             'status':'1'}
802         depsort = {'@action':'search','columns':'id','@sort':'department'}
803         depgrp = {'@action':'search','columns':'id','@group':'department'}
805         # Filter on department ignored for role 'User':
806         cl = self._make_client(dep, classname='iss', nodeid=None, userid=mary,
807             template='index')
808         h = HTMLRequest(cl)
809         self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
810         # Filter on department works for role 'Project':
811         cl = self._make_client(dep, classname='iss', nodeid=None, userid=chef,
812             template='index')
813         h = HTMLRequest(cl)
814         self.assertEqual([x.id for x in h.batch()],['2', '3'])
815         # Filter on status works for all:
816         cl = self._make_client(stat, classname='iss', nodeid=None, userid=mary,
817             template='index')
818         h = HTMLRequest(cl)
819         self.assertEqual([x.id for x in h.batch()],['1', '2'])
820         cl = self._make_client(stat, classname='iss', nodeid=None, userid=chef,
821             template='index')
822         h = HTMLRequest(cl)
823         self.assertEqual([x.id for x in h.batch()],['1', '2'])
824         # Sorting and grouping for class Project works:
825         cl = self._make_client(depsort, 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         cl = self._make_client(depgrp, classname='iss', nodeid=None,
830             userid=chef, template='index')
831         h = HTMLRequest(cl)
832         self.assertEqual([x.id for x in h.batch()],['2', '3', '1'])
833         # Sorting and grouping for class User fails:
834         cl = self._make_client(depsort, 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'])
838         cl = self._make_client(depgrp, classname='iss', nodeid=None,
839             userid=mary, template='index')
840         h = HTMLRequest(cl)
841         self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
843     def testRoles(self):
844         cl = self._make_client({})
845         self.db.user.set('1', roles='aDmin,    uSer')
846         item = HTMLItem(cl, 'user', '1')
847         self.assert_(item.hasRole('Admin'))
848         self.assert_(item.hasRole('User'))
849         self.assert_(item.hasRole('AdmiN'))
850         self.assert_(item.hasRole('UseR'))
851         self.assert_(item.hasRole('UseR','Admin'))
852         self.assert_(item.hasRole('UseR','somethingelse'))
853         self.assert_(item.hasRole('somethingelse','Admin'))
854         self.assert_(not item.hasRole('userr'))
855         self.assert_(not item.hasRole('adminn'))
856         self.assert_(not item.hasRole(''))
857         self.assert_(not item.hasRole(' '))
858         self.db.user.set('1', roles='')
859         self.assert_(not item.hasRole(''))
861     def testCSVExport(self):
862         cl = self._make_client({'@columns': 'id,name'}, nodeid=None,
863             userid='1')
864         cl.classname = 'status'
865         output = StringIO.StringIO()
866         cl.request = MockNull()
867         cl.request.wfile = output
868         actions.ExportCSVAction(cl).handle()
869         self.assertEquals('id,name\r\n1,unread\r\n2,deferred\r\n3,chatting\r\n'
870             '4,need-eg\r\n5,in-progress\r\n6,testing\r\n7,done-cbb\r\n'
871             '8,resolved\r\n',
872             output.getvalue())
874     def testCSVExportFailPermission(self):
875         cl = self._make_client({'@columns': 'id,email,password'}, nodeid=None,
876             userid='2')
877         cl.classname = 'user'
878         output = StringIO.StringIO()
879         cl.request = MockNull()
880         cl.request.wfile = output
881         self.assertRaises(exceptions.Unauthorised,
882             actions.ExportCSVAction(cl).handle)
885 def test_suite():
886     suite = unittest.TestSuite()
888 def test_suite():
889     suite = unittest.TestSuite()
890     suite.addTest(unittest.makeSuite(FormTestCase))
891     suite.addTest(unittest.makeSuite(MessageTestCase))
892     return suite
894 if __name__ == '__main__':
895     runner = unittest.TextTestRunner()
896     unittest.main(testRunner=runner)
898 # vim: set filetype=python sts=4 sw=4 et si :