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 '<script>x</script>')
62 self.assertEqual(cm('<iframe>x</iframe>'),
63 '<iframe>x</iframe>')
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 :