16 # You should have received a copy of the GNU Lesser General Public License along |
16 # You should have received a copy of the GNU Lesser General Public License along |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """this module contains base classes and utilities for cubicweb tests""" |
18 """this module contains base classes and utilities for cubicweb tests""" |
19 from __future__ import print_function |
19 from __future__ import print_function |
20 |
20 |
21 __docformat__ = "restructuredtext en" |
|
22 |
|
23 import sys |
21 import sys |
24 import re |
22 import re |
25 from os.path import dirname, join, abspath |
23 from os.path import dirname, join, abspath |
26 from math import log |
24 from math import log |
27 from contextlib import contextmanager |
25 from contextlib import contextmanager |
28 from warnings import warn |
|
29 from itertools import chain |
26 from itertools import chain |
30 |
27 |
31 from six import text_type, string_types |
28 from six import text_type, string_types |
32 from six.moves import range |
29 from six.moves import range |
33 from six.moves.urllib.parse import urlparse, parse_qs, unquote as urlunquote |
30 from six.moves.urllib.parse import urlparse, parse_qs, unquote as urlunquote |
41 from logilab.common.decorators import cached, classproperty, clear_cache, iclassmethod |
38 from logilab.common.decorators import cached, classproperty, clear_cache, iclassmethod |
42 from logilab.common.deprecation import deprecated, class_deprecated |
39 from logilab.common.deprecation import deprecated, class_deprecated |
43 from logilab.common.shellutils import getlogin |
40 from logilab.common.shellutils import getlogin |
44 |
41 |
45 from cubicweb import (ValidationError, NoSelectableObject, AuthenticationError, |
42 from cubicweb import (ValidationError, NoSelectableObject, AuthenticationError, |
46 ProgrammingError, BadConnectionId) |
43 BadConnectionId) |
47 from cubicweb import cwconfig, devtools, web, server, repoapi |
44 from cubicweb import cwconfig, devtools, web, server, repoapi |
48 from cubicweb.utils import json |
45 from cubicweb.utils import json |
49 from cubicweb.sobjects import notification |
46 from cubicweb.sobjects import notification |
50 from cubicweb.web import Redirect, application, eid_param |
47 from cubicweb.web import Redirect, application, eid_param |
51 from cubicweb.server.hook import SendMailOp |
48 from cubicweb.server.hook import SendMailOp |
52 from cubicweb.server.session import Session |
49 from cubicweb.server.session import Session |
53 from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS |
50 from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS |
54 from cubicweb.devtools import fake, htmlparser, DEFAULT_EMPTY_DB_ID |
51 from cubicweb.devtools import fake, htmlparser, DEFAULT_EMPTY_DB_ID |
55 from cubicweb.utils import json |
52 |
56 |
53 |
57 # low-level utilities ########################################################## |
54 # low-level utilities ########################################################## |
58 |
55 |
59 class CubicWebDebugger(Debugger): |
56 class CubicWebDebugger(Debugger): |
60 """special debugger class providing a 'view' function which saves some |
57 """special debugger class providing a 'view' function which saves some |
65 data = self._getval(arg) |
62 data = self._getval(arg) |
66 with open('/tmp/toto.html', 'w') as toto: |
63 with open('/tmp/toto.html', 'w') as toto: |
67 toto.write(data) |
64 toto.write(data) |
68 webbrowser.open('file:///tmp/toto.html') |
65 webbrowser.open('file:///tmp/toto.html') |
69 |
66 |
|
67 |
70 def line_context_filter(line_no, center, before=3, after=None): |
68 def line_context_filter(line_no, center, before=3, after=None): |
71 """return true if line are in context |
69 """return true if line are in context |
72 |
70 |
73 if after is None: after = before |
71 if after is None: after = before |
74 """ |
72 """ |
75 if after is None: |
73 if after is None: |
76 after = before |
74 after = before |
77 return center - before <= line_no <= center + after |
75 return center - before <= line_no <= center + after |
|
76 |
78 |
77 |
79 def unprotected_entities(schema, strict=False): |
78 def unprotected_entities(schema, strict=False): |
80 """returned a set of each non final entity type, excluding "system" entities |
79 """returned a set of each non final entity type, excluding "system" entities |
81 (eg CWGroup, CWUser...) |
80 (eg CWGroup, CWUser...) |
82 """ |
81 """ |
84 protected_entities = yams.schema.BASE_TYPES |
83 protected_entities = yams.schema.BASE_TYPES |
85 else: |
84 else: |
86 protected_entities = yams.schema.BASE_TYPES.union(SYSTEM_ENTITIES) |
85 protected_entities = yams.schema.BASE_TYPES.union(SYSTEM_ENTITIES) |
87 return set(schema.entities()) - protected_entities |
86 return set(schema.entities()) - protected_entities |
88 |
87 |
|
88 |
89 class JsonValidator(object): |
89 class JsonValidator(object): |
90 def parse_string(self, data): |
90 def parse_string(self, data): |
91 return json.loads(data.decode('ascii')) |
91 return json.loads(data.decode('ascii')) |
|
92 |
92 |
93 |
93 @contextmanager |
94 @contextmanager |
94 def real_error_handling(app): |
95 def real_error_handling(app): |
95 """By default, CubicWebTC `app` attribute (ie the publisher) is monkey |
96 """By default, CubicWebTC `app` attribute (ie the publisher) is monkey |
96 patched so that unexpected error are raised rather than going through the |
97 patched so that unexpected error are raised rather than going through the |
144 |
147 |
145 def __repr__(self): |
148 def __repr__(self): |
146 return '<Email to %s with subject %s>' % (','.join(self.recipients), |
149 return '<Email to %s with subject %s>' % (','.join(self.recipients), |
147 self.message.get('Subject')) |
150 self.message.get('Subject')) |
148 |
151 |
|
152 |
149 # the trick to get email into MAILBOX instead of actually sent: monkey patch |
153 # the trick to get email into MAILBOX instead of actually sent: monkey patch |
150 # cwconfig.SMTP object |
154 # cwconfig.SMTP object |
151 class MockSMTP: |
155 class MockSMTP: |
|
156 |
152 def __init__(self, server, port): |
157 def __init__(self, server, port): |
153 pass |
158 pass |
|
159 |
154 def close(self): |
160 def close(self): |
155 pass |
161 pass |
|
162 |
156 def sendmail(self, fromaddr, recipients, msg): |
163 def sendmail(self, fromaddr, recipients, msg): |
157 MAILBOX.append(Email(fromaddr, recipients, msg)) |
164 MAILBOX.append(Email(fromaddr, recipients, msg)) |
158 |
165 |
159 cwconfig.SMTP = MockSMTP |
166 cwconfig.SMTP = MockSMTP |
160 |
167 |
294 def _close_access(self): |
300 def _close_access(self): |
295 while self._open_access: |
301 while self._open_access: |
296 try: |
302 try: |
297 self._open_access.pop().close() |
303 self._open_access.pop().close() |
298 except BadConnectionId: |
304 except BadConnectionId: |
299 continue # already closed |
305 continue # already closed |
300 |
306 |
301 @property |
307 @property |
302 def session(self): |
308 def session(self): |
303 """return admin session""" |
309 """return admin session""" |
304 return self._admin_session |
310 return self._admin_session |
305 |
311 |
306 #XXX this doesn't need to a be classmethod anymore |
312 # XXX this doesn't need to a be classmethod anymore |
307 def _init_repo(self): |
313 def _init_repo(self): |
308 """init the repository and connection to it. |
314 """init the repository and connection to it. |
309 """ |
315 """ |
310 # get or restore and working db. |
316 # get or restore and working db. |
311 db_handler = devtools.get_test_db_handler(self.config, self.init_config) |
317 db_handler = devtools.get_test_db_handler(self.config, self.init_config) |
314 self.repo = db_handler.get_repo(startup=True) |
320 self.repo = db_handler.get_repo(startup=True) |
315 # get an admin session (without actual login) |
321 # get an admin session (without actual login) |
316 login = text_type(db_handler.config.default_admin_config['login']) |
322 login = text_type(db_handler.config.default_admin_config['login']) |
317 self.admin_access = self.new_access(login) |
323 self.admin_access = self.new_access(login) |
318 self._admin_session = self.admin_access._session |
324 self._admin_session = self.admin_access._session |
319 |
|
320 |
325 |
321 # config management ######################################################## |
326 # config management ######################################################## |
322 |
327 |
323 @classproperty |
328 @classproperty |
324 def config(cls): |
329 def config(cls): |
336 home = abspath(join(dirname(sys.modules[cls.__module__].__file__), cls.appid)) |
341 home = abspath(join(dirname(sys.modules[cls.__module__].__file__), cls.appid)) |
337 config = cls._config = cls.configcls(cls.appid, apphome=home) |
342 config = cls._config = cls.configcls(cls.appid, apphome=home) |
338 config.mode = 'test' |
343 config.mode = 'test' |
339 return config |
344 return config |
340 |
345 |
341 @classmethod # XXX could be turned into a regular method |
346 @classmethod # XXX could be turned into a regular method |
342 def init_config(cls, config): |
347 def init_config(cls, config): |
343 """configuration initialization hooks. |
348 """configuration initialization hooks. |
344 |
349 |
345 You may only want to override here the configuraton logic. |
350 You may only want to override here the configuraton logic. |
346 |
351 |
352 """ |
357 """ |
353 admincfg = config.default_admin_config |
358 admincfg = config.default_admin_config |
354 cls.admlogin = text_type(admincfg['login']) |
359 cls.admlogin = text_type(admincfg['login']) |
355 cls.admpassword = admincfg['password'] |
360 cls.admpassword = admincfg['password'] |
356 # uncomment the line below if you want rql queries to be logged |
361 # uncomment the line below if you want rql queries to be logged |
357 #config.global_set_option('query-log-file', |
362 # config.global_set_option('query-log-file', |
358 # '/tmp/test_rql_log.' + `os.getpid()`) |
363 # '/tmp/test_rql_log.' + `os.getpid()`) |
359 config.global_set_option('log-file', None) |
364 config.global_set_option('log-file', None) |
360 # set default-dest-addrs to a dumb email address to avoid mailbox or |
365 # set default-dest-addrs to a dumb email address to avoid mailbox or |
361 # mail queue pollution |
366 # mail queue pollution |
362 config.global_set_option('default-dest-addrs', ['whatever']) |
367 config.global_set_option('default-dest-addrs', ['whatever']) |
363 send_to = '%s@logilab.fr' % getlogin() |
368 send_to = '%s@logilab.fr' % getlogin() |
364 config.global_set_option('sender-addr', send_to) |
369 config.global_set_option('sender-addr', send_to) |
365 config.global_set_option('default-dest-addrs', send_to) |
370 config.global_set_option('default-dest-addrs', send_to) |
366 config.global_set_option('sender-name', 'cubicweb-test') |
371 config.global_set_option('sender-name', 'cubicweb-test') |
367 config.global_set_option('sender-addr', 'cubicweb-test@logilab.fr') |
372 config.global_set_option('sender-addr', 'cubicweb-test@logilab.fr') |
368 # default_base_url on config class isn't enough for TestServerConfiguration |
373 # default_base_url on config class isn't enough for TestServerConfiguration |
369 config.global_set_option('base-url', config.default_base_url()) |
374 config.global_set_option('base-url', config.default_base_url()) |
370 # web resources |
375 # web resources |
371 try: |
376 try: |
372 config.global_set_option('embed-allowed', re.compile('.*')) |
377 config.global_set_option('embed-allowed', re.compile('.*')) |
373 except Exception: # not in server only configuration |
378 except Exception: # not in server only configuration |
374 pass |
379 pass |
375 |
380 |
376 @property |
381 @property |
377 def vreg(self): |
382 def vreg(self): |
378 return self.repo.vreg |
383 return self.repo.vreg |
379 |
|
380 |
384 |
381 # global resources accessors ############################################### |
385 # global resources accessors ############################################### |
382 |
386 |
383 @property |
387 @property |
384 def schema(self): |
388 def schema(self): |
409 self.__class__._repo_init_failed = ex |
413 self.__class__._repo_init_failed = ex |
410 raise |
414 raise |
411 self.addCleanup(self._close_access) |
415 self.addCleanup(self._close_access) |
412 self.config.set_anonymous_allowed(self.anonymous_allowed) |
416 self.config.set_anonymous_allowed(self.anonymous_allowed) |
413 self.setup_database() |
417 self.setup_database() |
414 MAILBOX[:] = [] # reset mailbox |
418 MAILBOX[:] = [] # reset mailbox |
415 |
419 |
416 def tearDown(self): |
420 def tearDown(self): |
417 # XXX hack until logilab.common.testlib is fixed |
421 # XXX hack until logilab.common.testlib is fixed |
418 if self._admin_session is not None: |
422 if self._admin_session is not None: |
419 self.repo.close(self._admin_session.sessionid) |
423 self.repo.close(self._admin_session.sessionid) |
425 |
429 |
426 def _patch_SendMailOp(self): |
430 def _patch_SendMailOp(self): |
427 # monkey patch send mail operation so emails are sent synchronously |
431 # monkey patch send mail operation so emails are sent synchronously |
428 _old_mail_postcommit_event = SendMailOp.postcommit_event |
432 _old_mail_postcommit_event = SendMailOp.postcommit_event |
429 SendMailOp.postcommit_event = SendMailOp.sendmails |
433 SendMailOp.postcommit_event = SendMailOp.sendmails |
|
434 |
430 def reverse_SendMailOp_monkey_patch(): |
435 def reverse_SendMailOp_monkey_patch(): |
431 SendMailOp.postcommit_event = _old_mail_postcommit_event |
436 SendMailOp.postcommit_event = _old_mail_postcommit_event |
|
437 |
432 self.addCleanup(reverse_SendMailOp_monkey_patch) |
438 self.addCleanup(reverse_SendMailOp_monkey_patch) |
433 |
439 |
434 def setup_database(self): |
440 def setup_database(self): |
435 """add your database setup code by overriding this method""" |
441 """add your database setup code by overriding this method""" |
436 |
442 |
450 if req is None: |
456 if req is None: |
451 return self.request().user |
457 return self.request().user |
452 else: |
458 else: |
453 return req.user |
459 return req.user |
454 |
460 |
455 @iclassmethod # XXX turn into a class method |
461 @iclassmethod # XXX turn into a class method |
456 def create_user(self, req, login=None, groups=('users',), password=None, |
462 def create_user(self, req, login=None, groups=('users',), password=None, |
457 email=None, commit=True, **kwargs): |
463 email=None, commit=True, **kwargs): |
458 """create and return a new user entity""" |
464 """create and return a new user entity""" |
459 if password is None: |
465 if password is None: |
460 password = login |
466 password = login |
469 req.create_entity('EmailAddress', address=text_type(email), |
475 req.create_entity('EmailAddress', address=text_type(email), |
470 reverse_primary_email=user) |
476 reverse_primary_email=user) |
471 user.cw_clear_relation_cache('in_group', 'subject') |
477 user.cw_clear_relation_cache('in_group', 'subject') |
472 if commit: |
478 if commit: |
473 try: |
479 try: |
474 req.commit() # req is a session |
480 req.commit() # req is a session |
475 except AttributeError: |
481 except AttributeError: |
476 req.cnx.commit() |
482 req.cnx.commit() |
477 return user |
483 return user |
478 |
|
479 |
484 |
480 # other utilities ######################################################### |
485 # other utilities ######################################################### |
481 |
486 |
482 @contextmanager |
487 @contextmanager |
483 def temporary_appobjects(self, *appobjects): |
488 def temporary_appobjects(self, *appobjects): |
553 def assertPossibleTransitions(self, entity, expected): |
558 def assertPossibleTransitions(self, entity, expected): |
554 transitions = entity.cw_adapt_to('IWorkflowable').possible_transitions() |
559 transitions = entity.cw_adapt_to('IWorkflowable').possible_transitions() |
555 self.assertListEqual(sorted(tr.name for tr in transitions), |
560 self.assertListEqual(sorted(tr.name for tr in transitions), |
556 sorted(expected)) |
561 sorted(expected)) |
557 |
562 |
558 |
|
559 # views and actions registries inspection ################################## |
563 # views and actions registries inspection ################################## |
560 |
564 |
561 def pviews(self, req, rset): |
565 def pviews(self, req, rset): |
562 return sorted((a.__regid__, a.__class__) |
566 return sorted((a.__regid__, a.__class__) |
563 for a in self.vreg['views'].possible_views(req, rset=rset)) |
567 for a in self.vreg['views'].possible_views(req, rset=rset)) |
589 def _test_action(self, action): |
593 def _test_action(self, action): |
590 class fake_menu(list): |
594 class fake_menu(list): |
591 @property |
595 @property |
592 def items(self): |
596 def items(self): |
593 return self |
597 return self |
|
598 |
594 class fake_box(object): |
599 class fake_box(object): |
595 def action_link(self, action, **kwargs): |
600 def action_link(self, action, **kwargs): |
596 return (action.title, action.url()) |
601 return (action.title, action.url()) |
597 submenu = fake_menu() |
602 submenu = fake_menu() |
598 action.fill_menu(fake_box(), submenu) |
603 action.fill_menu(fake_box(), submenu) |
615 and not isinstance(view, class_deprecated)] |
620 and not isinstance(view, class_deprecated)] |
616 if views: |
621 if views: |
617 try: |
622 try: |
618 view = viewsvreg._select_best(views, req, rset=rset) |
623 view = viewsvreg._select_best(views, req, rset=rset) |
619 if view is None: |
624 if view is None: |
620 raise NoSelectableObject((req,), {'rset':rset}, views) |
625 raise NoSelectableObject((req,), {'rset': rset}, views) |
621 if view.linkable(): |
626 if view.linkable(): |
622 yield view |
627 yield view |
623 else: |
628 else: |
624 not_selected(self.vreg, view) |
629 not_selected(self.vreg, view) |
625 # else the view is expected to be used as subview and should |
630 # else the view is expected to be used as subview and should |
646 if view.category == 'startupview': |
651 if view.category == 'startupview': |
647 yield view.__regid__ |
652 yield view.__regid__ |
648 else: |
653 else: |
649 not_selected(self.vreg, view) |
654 not_selected(self.vreg, view) |
650 |
655 |
651 |
|
652 # web ui testing utilities ################################################# |
656 # web ui testing utilities ################################################# |
653 |
657 |
654 @property |
658 @property |
655 @cached |
659 @cached |
656 def app(self): |
660 def app(self): |
657 """return a cubicweb publisher""" |
661 """return a cubicweb publisher""" |
658 publisher = application.CubicWebPublisher(self.repo, self.config) |
662 publisher = application.CubicWebPublisher(self.repo, self.config) |
|
663 |
659 def raise_error_handler(*args, **kwargs): |
664 def raise_error_handler(*args, **kwargs): |
660 raise |
665 raise |
|
666 |
661 publisher.error_handler = raise_error_handler |
667 publisher.error_handler = raise_error_handler |
662 return publisher |
668 return publisher |
663 |
669 |
664 @deprecated('[3.19] use the .remote_calling method') |
670 @deprecated('[3.19] use the .remote_calling method') |
665 def remote_call(self, fname, *args): |
671 def remote_call(self, fname, *args): |
707 |
713 |
708 * `entity_field_dicts`, list of (entity, dictionary) where dictionary contains name:value |
714 * `entity_field_dicts`, list of (entity, dictionary) where dictionary contains name:value |
709 for fields that are not tied to the given entity |
715 for fields that are not tied to the given entity |
710 """ |
716 """ |
711 assert field_dict or entity_field_dicts, \ |
717 assert field_dict or entity_field_dicts, \ |
712 'field_dict and entity_field_dicts arguments must not be both unspecified' |
718 'field_dict and entity_field_dicts arguments must not be both unspecified' |
713 if field_dict is None: |
719 if field_dict is None: |
714 field_dict = {} |
720 field_dict = {} |
715 form = {'__form_id': formid} |
721 form = {'__form_id': formid} |
716 fields = [] |
722 fields = [] |
717 for field, value in field_dict.items(): |
723 for field, value in field_dict.items(): |
718 fields.append(field) |
724 fields.append(field) |
719 form[field] = value |
725 form[field] = value |
|
726 |
720 def _add_entity_field(entity, field, value): |
727 def _add_entity_field(entity, field, value): |
721 entity_fields.append(field) |
728 entity_fields.append(field) |
722 form[eid_param(field, entity.eid)] = value |
729 form[eid_param(field, entity.eid)] = value |
|
730 |
723 for entity, field_dict in entity_field_dicts: |
731 for entity, field_dict in entity_field_dicts: |
724 if '__maineid' not in form: |
732 if '__maineid' not in form: |
725 form['__maineid'] = entity.eid |
733 form['__maineid'] = entity.eid |
726 entity_fields = [] |
734 entity_fields = [] |
727 form.setdefault('eid', []).append(entity.eid) |
735 form.setdefault('eid', []).append(entity.eid) |
740 |
748 |
741 req.form will be setup using the url's query string |
749 req.form will be setup using the url's query string |
742 """ |
750 """ |
743 req = self.request(url=url) |
751 req = self.request(url=url) |
744 if isinstance(url, unicode): |
752 if isinstance(url, unicode): |
745 url = url.encode(req.encoding) # req.setup_params() expects encoded strings |
753 url = url.encode(req.encoding) # req.setup_params() expects encoded strings |
746 querystring = urlparse(url)[-2] |
754 querystring = urlparse(url)[-2] |
747 params = parse_qs(querystring) |
755 params = parse_qs(querystring) |
748 req.setup_params(params) |
756 req.setup_params(params) |
749 return req |
757 return req |
750 |
758 |
754 |
762 |
755 req.form will be setup using the url's query string |
763 req.form will be setup using the url's query string |
756 """ |
764 """ |
757 with self.admin_access.web_request(url=url) as req: |
765 with self.admin_access.web_request(url=url) as req: |
758 if isinstance(url, unicode): |
766 if isinstance(url, unicode): |
759 url = url.encode(req.encoding) # req.setup_params() expects encoded strings |
767 url = url.encode(req.encoding) # req.setup_params() expects encoded strings |
760 querystring = urlparse(url)[-2] |
768 querystring = urlparse(url)[-2] |
761 params = parse_qs(querystring) |
769 params = parse_qs(querystring) |
762 req.setup_params(params) |
770 req.setup_params(params) |
763 yield req |
771 yield req |
764 |
772 |
797 path = location |
805 path = location |
798 params = {} |
806 params = {} |
799 else: |
807 else: |
800 cleanup = lambda p: (p[0], urlunquote(p[1])) |
808 cleanup = lambda p: (p[0], urlunquote(p[1])) |
801 params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p) |
809 params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p) |
802 if path.startswith(req.base_url()): # may be relative |
810 if path.startswith(req.base_url()): # may be relative |
803 path = path[len(req.base_url()):] |
811 path = path[len(req.base_url()):] |
804 return path, params |
812 return path, params |
805 |
813 |
806 def expect_redirect(self, callback, req): |
814 def expect_redirect(self, callback, req): |
807 """call the given callback with req as argument, expecting to get a |
815 """call the given callback with req as argument, expecting to get a |
816 |
824 |
817 def expect_redirect_handle_request(self, req, path='edit'): |
825 def expect_redirect_handle_request(self, req, path='edit'): |
818 """call the publish method of the application publisher, expecting to |
826 """call the publish method of the application publisher, expecting to |
819 get a Redirect exception |
827 get a Redirect exception |
820 """ |
828 """ |
821 result = self.app_handle_request(req, path) |
829 self.app_handle_request(req, path) |
822 self.assertTrue(300 <= req.status_out <400, req.status_out) |
830 self.assertTrue(300 <= req.status_out < 400, req.status_out) |
823 location = req.get_response_header('location') |
831 location = req.get_response_header('location') |
824 return self._parse_location(req, location) |
832 return self._parse_location(req, location) |
825 |
833 |
826 @deprecated("[3.15] expect_redirect_handle_request is the new and better way" |
834 @deprecated("[3.15] expect_redirect_handle_request is the new and better way" |
827 " (beware of small semantic changes)") |
835 " (beware of small semantic changes)") |
828 def expect_redirect_publish(self, *args, **kwargs): |
836 def expect_redirect_publish(self, *args, **kwargs): |
829 return self.expect_redirect_handle_request(*args, **kwargs) |
837 return self.expect_redirect_handle_request(*args, **kwargs) |
830 |
|
831 |
838 |
832 def set_auth_mode(self, authmode, anonuser=None): |
839 def set_auth_mode(self, authmode, anonuser=None): |
833 self.set_option('auth-mode', authmode) |
840 self.set_option('auth-mode', authmode) |
834 self.set_option('anonymous-user', anonuser) |
841 self.set_option('anonymous-user', anonuser) |
835 if anonuser is None: |
842 if anonuser is None: |
875 content_type_validators = { |
882 content_type_validators = { |
876 # maps MIME type : validator name |
883 # maps MIME type : validator name |
877 # |
884 # |
878 # do not set html validators here, we need HTMLValidator for html |
885 # do not set html validators here, we need HTMLValidator for html |
879 # snippets |
886 # snippets |
880 #'text/html': DTDValidator, |
887 # 'text/html': DTDValidator, |
881 #'application/xhtml+xml': DTDValidator, |
888 # 'application/xhtml+xml': DTDValidator, |
882 'application/xml': htmlparser.XMLValidator, |
889 'application/xml': htmlparser.XMLValidator, |
883 'text/xml': htmlparser.XMLValidator, |
890 'text/xml': htmlparser.XMLValidator, |
884 'application/json': JsonValidator, |
891 'application/json': JsonValidator, |
885 'text/plain': None, |
892 'text/plain': None, |
886 'text/comma-separated-values': None, |
893 'text/comma-separated-values': None, |
889 'image/png': None, |
896 'image/png': None, |
890 } |
897 } |
891 # maps vid : validator name (override content_type_validators) |
898 # maps vid : validator name (override content_type_validators) |
892 vid_validators = dict((vid, htmlparser.VALMAP[valkey]) |
899 vid_validators = dict((vid, htmlparser.VALMAP[valkey]) |
893 for vid, valkey in VIEW_VALIDATORS.items()) |
900 for vid, valkey in VIEW_VALIDATORS.items()) |
894 |
|
895 |
901 |
896 def view(self, vid, rset=None, req=None, template='main-template', |
902 def view(self, vid, rset=None, req=None, template='main-template', |
897 **kwargs): |
903 **kwargs): |
898 """This method tests the view `vid` on `rset` using `template` |
904 """This method tests the view `vid` on `rset` using `template` |
899 |
905 |
919 self.set_description("testing vid=%s defined in %s with (%s)" % ( |
925 self.set_description("testing vid=%s defined in %s with (%s)" % ( |
920 vid, view.__module__, rql)) |
926 vid, view.__module__, rql)) |
921 else: |
927 else: |
922 self.set_description("testing vid=%s defined in %s without rset" % ( |
928 self.set_description("testing vid=%s defined in %s without rset" % ( |
923 vid, view.__module__)) |
929 vid, view.__module__)) |
924 if template is None: # raw view testing, no template |
930 if template is None: # raw view testing, no template |
925 viewfunc = view.render |
931 viewfunc = view.render |
926 else: |
932 else: |
927 kwargs['view'] = view |
933 kwargs['view'] = view |
928 viewfunc = lambda **k: viewsreg.main_template(req, template, |
934 viewfunc = lambda **k: viewsreg.main_template(req, template, |
929 rset=rset, **kwargs) |
935 rset=rset, **kwargs) |
930 return self._test_view(viewfunc, view, template, kwargs) |
936 return self._test_view(viewfunc, view, template, kwargs) |
931 |
|
932 |
937 |
933 def _test_view(self, viewfunc, view, template='main-template', kwargs={}): |
938 def _test_view(self, viewfunc, view, template='main-template', kwargs={}): |
934 """this method does the actual call to the view |
939 """this method does the actual call to the view |
935 |
940 |
936 If no error occurred while rendering the view, the HTML is analyzed |
941 If no error occurred while rendering the view, the HTML is analyzed |
987 if isinstance(output, text_type): |
992 if isinstance(output, text_type): |
988 # XXX |
993 # XXX |
989 output = output.encode('utf-8') |
994 output = output.encode('utf-8') |
990 validator = self.get_validator(view, output=output) |
995 validator = self.get_validator(view, output=output) |
991 if validator is None: |
996 if validator is None: |
992 return output # return raw output if no validator is defined |
997 return output # return raw output if no validator is defined |
993 if isinstance(validator, htmlparser.DTDValidator): |
998 if isinstance(validator, htmlparser.DTDValidator): |
994 # XXX remove <canvas> used in progress widget, unknown in html dtd |
999 # XXX remove <canvas> used in progress widget, unknown in html dtd |
995 output = re.sub('<canvas.*?></canvas>', '', output) |
1000 output = re.sub('<canvas.*?></canvas>', '', output) |
996 return self.assertWellFormed(validator, output.strip(), context= view.__regid__) |
1001 return self.assertWellFormed(validator, output.strip(), context=view.__regid__) |
997 |
1002 |
998 def assertWellFormed(self, validator, content, context=None): |
1003 def assertWellFormed(self, validator, content, context=None): |
999 try: |
1004 try: |
1000 return validator.parse_string(content) |
1005 return validator.parse_string(content) |
1001 except Exception: |
1006 except Exception: |
1067 |
1072 |
1068 from cubicweb.devtools.fill import insert_entity_queries, make_relations_queries |
1073 from cubicweb.devtools.fill import insert_entity_queries, make_relations_queries |
1069 |
1074 |
1070 # XXX cleanup unprotected_entities & all mess |
1075 # XXX cleanup unprotected_entities & all mess |
1071 |
1076 |
|
1077 |
1072 def how_many_dict(schema, cnx, how_many, skip): |
1078 def how_many_dict(schema, cnx, how_many, skip): |
1073 """given a schema, compute how many entities by type we need to be able to |
1079 """given a schema, compute how many entities by type we need to be able to |
1074 satisfy relations cardinality. |
1080 satisfy relations cardinality. |
1075 |
1081 |
1076 The `how_many` argument tells how many entities of which type we want at |
1082 The `how_many` argument tells how many entities of which type we want at |
1095 relmap.setdefault((rschema, subj), []).append(str(obj)) |
1101 relmap.setdefault((rschema, subj), []).append(str(obj)) |
1096 if card[1] in '1+' and card[0] in '1?': |
1102 if card[1] in '1+' and card[0] in '1?': |
1097 # reverse subj and obj in the above explanation |
1103 # reverse subj and obj in the above explanation |
1098 relmap.setdefault((rschema, obj), []).append(str(subj)) |
1104 relmap.setdefault((rschema, obj), []).append(str(subj)) |
1099 unprotected = unprotected_entities(schema) |
1105 unprotected = unprotected_entities(schema) |
1100 for etype in skip: # XXX (syt) duh? explain or kill |
1106 for etype in skip: # XXX (syt) duh? explain or kill |
1101 unprotected.add(etype) |
1107 unprotected.add(etype) |
1102 howmanydict = {} |
1108 howmanydict = {} |
1103 # step 1, compute a base number of each entity types: number of already |
1109 # step 1, compute a base number of each entity types: number of already |
1104 # existing entities of this type + `how_many` |
1110 # existing entities of this type + `how_many` |
1105 for etype in unprotected_entities(schema, strict=True): |
1111 for etype in unprotected_entities(schema, strict=True): |
1140 def custom_populate(self, how_many, cnx): |
1146 def custom_populate(self, how_many, cnx): |
1141 pass |
1147 pass |
1142 |
1148 |
1143 def post_populate(self, cnx): |
1149 def post_populate(self, cnx): |
1144 pass |
1150 pass |
1145 |
|
1146 |
1151 |
1147 @nocoverage |
1152 @nocoverage |
1148 def auto_populate(self, how_many): |
1153 def auto_populate(self, how_many): |
1149 """this method populates the database with `how_many` entities |
1154 """this method populates the database with `how_many` entities |
1150 of each possible type. It also inserts random relations between them |
1155 of each possible type. It also inserts random relations between them |
1181 try: |
1186 try: |
1182 cnx.execute(rql, args) |
1187 cnx.execute(rql, args) |
1183 except ValidationError as ex: |
1188 except ValidationError as ex: |
1184 # failed to satisfy some constraint |
1189 # failed to satisfy some constraint |
1185 print('error in automatic db population', ex) |
1190 print('error in automatic db population', ex) |
1186 cnx.commit_state = None # reset uncommitable flag |
1191 cnx.commit_state = None # reset uncommitable flag |
1187 self.post_populate(cnx) |
1192 self.post_populate(cnx) |
1188 |
1193 |
1189 def iter_individual_rsets(self, etypes=None, limit=None): |
1194 def iter_individual_rsets(self, etypes=None, limit=None): |
1190 etypes = etypes or self.to_test_etypes() |
1195 etypes = etypes or self.to_test_etypes() |
1191 with self.admin_access.web_request() as req: |
1196 with self.admin_access.web_request() as req: |
1216 except KeyError: |
1221 except KeyError: |
1217 etype2 = etype1 |
1222 etype2 = etype1 |
1218 # test a mixed query (DISTINCT/GROUP to avoid getting duplicate |
1223 # test a mixed query (DISTINCT/GROUP to avoid getting duplicate |
1219 # X which make muledit view failing for instance (html validation fails |
1224 # X which make muledit view failing for instance (html validation fails |
1220 # because of some duplicate "id" attributes) |
1225 # because of some duplicate "id" attributes) |
1221 yield req.execute('DISTINCT Any X, MAX(Y) GROUPBY X WHERE X is %s, Y is %s' % (etype1, etype2)) |
1226 yield req.execute('DISTINCT Any X, MAX(Y) GROUPBY X WHERE X is %s, Y is %s' % |
|
1227 (etype1, etype2)) |
1222 # test some application-specific queries if defined |
1228 # test some application-specific queries if defined |
1223 for rql in self.application_rql: |
1229 for rql in self.application_rql: |
1224 yield req.execute(rql) |
1230 yield req.execute(rql) |
1225 |
1231 |
1226 def _test_everything_for(self, rset): |
1232 def _test_everything_for(self, rset): |
1239 rset.req.reset_headers(), 'main-template') |
1245 rset.req.reset_headers(), 'main-template') |
1240 # We have to do this because some views modify the |
1246 # We have to do this because some views modify the |
1241 # resultset's syntax tree |
1247 # resultset's syntax tree |
1242 rset = backup_rset |
1248 rset = backup_rset |
1243 for action in self.list_actions_for(rset): |
1249 for action in self.list_actions_for(rset): |
1244 yield InnerTest(self._testname(rset, action.__regid__, 'action'), self._test_action, action) |
1250 yield InnerTest(self._testname(rset, action.__regid__, 'action'), |
|
1251 self._test_action, action) |
1245 for box in self.list_boxes_for(rset): |
1252 for box in self.list_boxes_for(rset): |
1246 w = [].append |
1253 w = [].append |
1247 yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render, w) |
1254 yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render, w) |
1248 |
1255 |
1249 @staticmethod |
1256 @staticmethod |
1267 |
1274 |
1268 # access to self.app for proper initialization of the authentication |
1275 # access to self.app for proper initialization of the authentication |
1269 # machinery (else some views may fail) |
1276 # machinery (else some views may fail) |
1270 self.app |
1277 self.app |
1271 |
1278 |
1272 ## one each |
|
1273 def test_one_each_config(self): |
1279 def test_one_each_config(self): |
1274 self.auto_populate(1) |
1280 self.auto_populate(1) |
1275 for rset in self.iter_automatic_rsets(limit=1): |
1281 for rset in self.iter_automatic_rsets(limit=1): |
1276 for testargs in self._test_everything_for(rset): |
1282 for testargs in self._test_everything_for(rset): |
1277 yield testargs |
1283 yield testargs |
1278 |
1284 |
1279 ## ten each |
|
1280 def test_ten_each_config(self): |
1285 def test_ten_each_config(self): |
1281 self.auto_populate(10) |
1286 self.auto_populate(10) |
1282 for rset in self.iter_automatic_rsets(limit=10): |
1287 for rset in self.iter_automatic_rsets(limit=10): |
1283 for testargs in self._test_everything_for(rset): |
1288 for testargs in self._test_everything_for(rset): |
1284 yield testargs |
1289 yield testargs |
1285 |
1290 |
1286 ## startup views |
|
1287 def test_startup_views(self): |
1291 def test_startup_views(self): |
1288 for vid in self.list_startup_views(): |
1292 for vid in self.list_startup_views(): |
1289 with self.admin_access.web_request() as req: |
1293 with self.admin_access.web_request() as req: |
1290 yield self.view, vid, None, req |
1294 yield self.view, vid, None, req |
1291 |
1295 |