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