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