Mercurial > p > roundup > code
comparison test/db_test_base.py @ 5310:efb34cbdba7c
Add (currently failing) test for atomic actions
Actions via the web-interface can have several database-modifying
instructions. These should be atomic, i.e., either all should go in or
none. This is currently not the case due to commit calls from OTK
handling (CSRF-protection).
| author | Ralf Schlatterbeck <rsc@runtux.com> |
|---|---|
| date | Mon, 06 Nov 2017 09:26:59 +0100 |
| parents | f015df5f8edf |
| children | 1303ba0b5358 |
comparison
equal
deleted
inserted
replaced
| 5309:20084e2b48c3 | 5310:efb34cbdba7c |
|---|---|
| 14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" | 14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" |
| 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, | 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, |
| 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. | 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. |
| 17 | 17 |
| 18 import unittest, os, shutil, errno, imp, sys, time, pprint, base64, os.path | 18 import unittest, os, shutil, errno, imp, sys, time, pprint, base64, os.path |
| 19 import logging | 19 import logging, cgi |
| 20 import gpgmelib | 20 import gpgmelib |
| 21 from email.parser import FeedParser | 21 from email.parser import FeedParser |
| 22 | 22 |
| 23 import pytest | 23 import pytest |
| 24 from roundup.hyperdb import String, Password, Link, Multilink, Date, \ | 24 from roundup.hyperdb import String, Password, Link, Multilink, Date, \ |
| 25 Interval, DatabaseError, Boolean, Number, Node, Integer | 25 Interval, DatabaseError, Boolean, Number, Node, Integer |
| 26 from roundup.mailer import Mailer | 26 from roundup.mailer import Mailer |
| 27 from roundup import date, password, init, instance, configuration, \ | 27 from roundup import date, password, init, instance, configuration, \ |
| 28 roundupdb, i18n, hyperdb | 28 roundupdb, i18n, hyperdb |
| 29 from roundup.cgi.templating import HTMLItem | 29 from roundup.cgi.templating import HTMLItem |
| 30 from roundup.cgi.templating import HTMLProperty, _HTMLItem, anti_csrf_nonce | |
| 31 from roundup.cgi import client, actions | |
| 32 from roundup.cgi.engine_zopetal import RoundupPageTemplate | |
| 33 from roundup.cgi.templating import HTMLItem | |
| 34 from roundup.exceptions import UsageError, Reject | |
| 30 | 35 |
| 31 from mocknull import MockNull | 36 from mocknull import MockNull |
| 32 | 37 |
| 33 config = configuration.CoreConfig() | 38 config = configuration.CoreConfig() |
| 34 config.DATABASE = "db" | 39 config.DATABASE = "db" |
| 2293 shutil.rmtree('_test_export') | 2298 shutil.rmtree('_test_export') |
| 2294 | 2299 |
| 2295 # test props from args parsing | 2300 # test props from args parsing |
| 2296 def testAdminOtherCommands(self): | 2301 def testAdminOtherCommands(self): |
| 2297 import roundup.admin | 2302 import roundup.admin |
| 2298 from roundup.exceptions import UsageError | |
| 2299 | 2303 |
| 2300 # use the filtering setup to create a bunch of items | 2304 # use the filtering setup to create a bunch of items |
| 2301 ae, dummy1, dummy2 = self.filteringSetup() | 2305 ae, dummy1, dummy2 = self.filteringSetup() |
| 2302 # create large field | 2306 # create large field |
| 2303 self.db.priority.create(name = 'X' * 500) | 2307 self.db.priority.create(name = 'X' * 500) |
| 3123 ae(str(issue.assignedto.username),m%'username') | 3127 ae(str(issue.assignedto.username),m%'username') |
| 3124 ae(str(issue ['assignedto'].username),m%'username') | 3128 ae(str(issue ['assignedto'].username),m%'username') |
| 3125 ae(bool(issue ['assignedto']['username']),False) | 3129 ae(bool(issue ['assignedto']['username']),False) |
| 3126 ae(bool(issue ['priority']['name']),False) | 3130 ae(bool(issue ['priority']['name']),False) |
| 3127 | 3131 |
| 3132 def makeForm(args): | |
| 3133 form = cgi.FieldStorage() | |
| 3134 for k,v in args.items(): | |
| 3135 if type(v) is type([]): | |
| 3136 [form.list.append(cgi.MiniFieldStorage(k, x)) for x in v] | |
| 3137 elif isinstance(v, FileUpload): | |
| 3138 x = cgi.MiniFieldStorage(k, v.content) | |
| 3139 x.filename = v.filename | |
| 3140 form.list.append(x) | |
| 3141 else: | |
| 3142 form.list.append(cgi.MiniFieldStorage(k, v)) | |
| 3143 return form | |
| 3144 | |
| 3145 class FileUpload: | |
| 3146 def __init__(self, content, filename): | |
| 3147 self.content = content | |
| 3148 self.filename = filename | |
| 3149 | |
| 3150 class FormTestParent(object): | |
| 3151 | |
| 3152 backend = "anydbm" | |
| 3153 def setupDetectors(self): | |
| 3154 pass | |
| 3155 | |
| 3156 def setUp(self): | |
| 3157 self.dirname = '_test_cgi_form' | |
| 3158 # set up and open a tracker | |
| 3159 self.instance = setupTracker(self.dirname, backend = self.backend) | |
| 3160 | |
| 3161 # We may want to register separate detectors | |
| 3162 self.setupDetectors() | |
| 3163 | |
| 3164 # open the database | |
| 3165 self.db = self.instance.open('admin') | |
| 3166 self.db.tx_Source = "web" | |
| 3167 self.db.user.create(username='Chef', address='chef@bork.bork.bork', | |
| 3168 realname='Bork, Chef', roles='User') | |
| 3169 self.db.user.create(username='mary', address='mary@test.test', | |
| 3170 roles='User', realname='Contrary, Mary') | |
| 3171 | |
| 3172 self.db.issue.addprop(tx_Source=hyperdb.String()) | |
| 3173 self.db.msg.addprop(tx_Source=hyperdb.String()) | |
| 3174 self.db.post_init() | |
| 3175 | |
| 3176 def setupClient(self, form, classname, nodeid=None, template='item', env_addon=None): | |
| 3177 cl = client.Client(self.instance, None, {'PATH_INFO':'/', | |
| 3178 'REQUEST_METHOD':'POST'}, makeForm(form)) | |
| 3179 cl.classname = classname | |
| 3180 cl.base = 'http://whoami.com/path/' | |
| 3181 cl.nodeid = nodeid | |
| 3182 cl.language = ('en',) | |
| 3183 cl.userid = '1' | |
| 3184 cl.db = self.db | |
| 3185 cl.user = 'admin' | |
| 3186 cl.template = template | |
| 3187 if env_addon is not None: | |
| 3188 cl.env.update(env_addon) | |
| 3189 return cl | |
| 3190 | |
| 3191 def parseForm(self, form, classname='test', nodeid=None): | |
| 3192 cl = self.setupClient(form, classname, nodeid) | |
| 3193 return cl.parsePropsFromForm(create=1) | |
| 3194 | |
| 3195 def tearDown(self): | |
| 3196 self.db.close() | |
| 3197 try: | |
| 3198 shutil.rmtree(self.dirname) | |
| 3199 except OSError as error: | |
| 3200 if error.errno not in (errno.ENOENT, errno.ESRCH): raise | |
| 3201 | |
| 3202 class SpecialAction(actions.EditItemAction): | |
| 3203 x = False | |
| 3204 def handle(self): | |
| 3205 self.__class__.x = True | |
| 3206 cl = self.db.getclass(self.classname) | |
| 3207 cl.set(self.nodeid, status='2') | |
| 3208 cl.set(self.nodeid, title="Just a test") | |
| 3209 assert(0, "not reached") | |
| 3210 self.db.commit() | |
| 3211 | |
| 3212 def reject_title(db, cl, nodeid, newvalues): | |
| 3213 if 'title' in newvalues: | |
| 3214 raise Reject ("REJECT TITLE CHANGE") | |
| 3215 | |
| 3216 def init_reject(db): | |
| 3217 db.issue.audit("set", reject_title) | |
| 3218 | |
| 3219 def get_extensions(self, what): | |
| 3220 """ For monkey-patch of instance.get_extensions: The old method is | |
| 3221 kept as _get_extensions, we use the new method to return our own | |
| 3222 auditors/reactors. | |
| 3223 """ | |
| 3224 if what == 'detectors': | |
| 3225 return [init_reject] | |
| 3226 return self._get_extensions(what) | |
| 3227 | |
| 3228 class SpecialActionTest(FormTestParent): | |
| 3229 | |
| 3230 def setupDetectors(self): | |
| 3231 self.instance._get_extensions = self.instance.get_extensions | |
| 3232 def ge(what): | |
| 3233 return get_extensions(self.instance, what) | |
| 3234 self.instance.get_extensions = ge | |
| 3235 | |
| 3236 def setUp(self): | |
| 3237 FormTestParent.setUp(self) | |
| 3238 | |
| 3239 self.instance.registerAction('special', SpecialAction) | |
| 3240 self.issue = self.db.issue.create (title = "hello", status='1') | |
| 3241 self.db.commit () | |
| 3242 if not os.environ.has_key('SENDMAILDEBUG'): | |
| 3243 os.environ['SENDMAILDEBUG'] = 'mail-test2.log' | |
| 3244 self.SENDMAILDEBUG = os.environ['SENDMAILDEBUG'] | |
| 3245 page_template = """ | |
| 3246 <html> | |
| 3247 <body> | |
| 3248 <p tal:condition="options/error_message|nothing" | |
| 3249 tal:repeat="m options/error_message" | |
| 3250 tal:content="structure m"/> | |
| 3251 <p tal:content="context/title/plain"/> | |
| 3252 <p tal:content="context/status/plain"/> | |
| 3253 <p tal:content="structure context/submit"/> | |
| 3254 </body> | |
| 3255 </html> | |
| 3256 """.strip () | |
| 3257 self.form = {':action': 'special'} | |
| 3258 cl = self.setupClient(self.form, 'issue', self.issue) | |
| 3259 pt = RoundupPageTemplate() | |
| 3260 pt.pt_edit(page_template, 'text/html') | |
| 3261 self.out = [] | |
| 3262 def wh(s): | |
| 3263 self.out.append(s) | |
| 3264 cl.write_html = wh | |
| 3265 def load_template(x): | |
| 3266 return pt | |
| 3267 cl.instance.templates.load = load_template | |
| 3268 cl.selectTemplate = MockNull() | |
| 3269 cl.determine_context = MockNull () | |
| 3270 def hasPermission(s, p, classname=None, d=None, e=None, **kw): | |
| 3271 return True | |
| 3272 self.hasPermission = actions.Action.hasPermission | |
| 3273 actions.Action.hasPermission = hasPermission | |
| 3274 self.e1 = _HTMLItem.is_edit_ok | |
| 3275 _HTMLItem.is_edit_ok = lambda x : True | |
| 3276 self.e2 = HTMLProperty.is_edit_ok | |
| 3277 HTMLProperty.is_edit_ok = lambda x : True | |
| 3278 # Make sure header check passes | |
| 3279 cl.env['HTTP_REFERER'] = 'http://whoami.com/path/' | |
| 3280 self.client = cl | |
| 3281 | |
| 3282 def tearDown(self): | |
| 3283 FormTestParent.tearDown(self) | |
| 3284 # Remove monkey-patches | |
| 3285 self.instance.get_extensions = self.instance._get_extensions | |
| 3286 del self.instance._get_extensions | |
| 3287 actions.Action.hasPermission = self.hasPermission | |
| 3288 _HTMLItem.is_edit_ok = self.e1 | |
| 3289 HTMLProperty.is_edit_ok = self.e2 | |
| 3290 if os.path.exists(self.SENDMAILDEBUG): | |
| 3291 #os.remove(self.SENDMAILDEBUG) | |
| 3292 pass | |
| 3293 | |
| 3294 def testInnerMain(self): | |
| 3295 cl = self.client | |
| 3296 cl.session_api = MockNull(_sid="1234567890") | |
| 3297 self.form ['@nonce'] = anti_csrf_nonce(cl, cl) | |
| 3298 cl.form = makeForm(self.form) | |
| 3299 # inner_main will re-open the database! | |
| 3300 # Note that in the template above, the rendering of the | |
| 3301 # context/submit button will also call anti_csrf_nonce which | |
| 3302 # does a commit of the otk to the database. | |
| 3303 cl.inner_main() | |
| 3304 cl.db.close() | |
| 3305 print self.out | |
| 3306 # Make sure the action was called | |
| 3307 self.assertEqual(SpecialAction.x, True) | |
| 3308 # Check that the Reject worked: | |
| 3309 self.assertNotEqual(-1, self.out[0].index('REJECT TITLE CHANGE')) | |
| 3310 # Re-open db | |
| 3311 self.db.close() | |
| 3312 self.db = self.instance.open ('admin') | |
| 3313 # We shouldn't see any changes | |
| 3314 self.assertEqual(self.db.issue.get(self.issue, 'title'), 'hello') | |
| 3315 self.assertEqual(self.db.issue.get(self.issue, 'status'), '1') | |
| 3316 | |
| 3128 # vim: set et sts=4 sw=4 : | 3317 # vim: set et sts=4 sw=4 : |
