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