[views] Respond with 400 Bad Request in case of validation error in ajax request
Previously, we raised a plain RemoteCallFailed without a status code, in which
case it defaults to "500 Internal Server Error". Now, for validation
errors, we issue a 400 Bad Request since these are clearly client errors.
.. -*- coding: utf-8 -*-Tests=====Unit tests----------The *CubicWeb* framework provides the:class:`cubicweb.devtools.testlib.CubicWebTC` test base class .Tests shall be put into the mycube/test directory. Additional testdata shall go into mycube/test/data.It is much advised to write tests concerning entities methods,actions, hooks and operations, security. The:class:`~cubicweb.devtools.testlib.CubicWebTC` base class hasconvenience methods to help test all of this.In the realm of views, automatic tests check that views are validXHTML. See :ref:`automatic_views_tests` for details.Most unit tests need a live database to work against. This is achievedby CubicWeb using automatically sqlite (bundled with Python, seehttp://docs.python.org/library/sqlite3.html) as a backend.The database is stored in the mycube/test/tmpdb,mycube/test/tmpdb-template files. If it does not (yet) exist, it willbe built automatically when the test suite starts...warning:: Whenever the schema changes (new entities, attributes, relations) one must delete these two files. Changes concerned only with entity or relation type properties (constraints, cardinalities, permissions) and generally dealt with using the`sync_schema_props_perms()` function of the migration environment do not need a database regeneration step..._hook_test:Unit test by example````````````````````We start with an example extracted from the keyword cube (availablefrom http://www.cubicweb.org/project/cubicweb-keyword)...sourcecode::pythonfromcubicweb.devtools.testlibimportCubicWebTCfromcubicwebimportValidationErrorclassClassificationHooksTC(CubicWebTC):defsetup_database(self):withself.admin_access.repo_cnx()ascnx:group_etype=cnx.find('CWEType',name='CWGroup').one()c1=cnx.create_entity('Classification',name=u'classif1',classifies=group_etype)user_etype=cnx.find('CWEType',name='CWUser').one()c2=cnx.create_entity('Classification',name=u'classif2',classifies=user_etype)self.kw1eid=cnx.create_entity('Keyword',name=u'kwgroup',included_in=c1).eidcnx.commit()deftest_cannot_create_cycles(self):withself.admin_access.repo_cnx()ascnx:kw1=cnx.entity_from_eid(self.kw1eid)# direct obvious cyclewithself.assertRaises(ValidationError):kw1.cw_set(subkeyword_of=kw1)cnx.rollback()# testing indirect cycleskw3=cnx.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, ''SK subkeyword_of K WHERE C name "classif1", K eid %(k)s'{'k':kw1}).get_entity(0,0)kw3.cw_set(reverse_subkeyword_of=kw1)self.assertRaises(ValidationError,cnx.commit)The test class defines a :meth:`setup_database` method which populates thedatabase with initial data. Each test of the class runs with thispre-populated database.The test case itself checks that an Operation does its job ofpreventing cycles amongst Keyword entities.The `create_entity` method of connection (or request) objects allowsto create an entity. You can link this entity to other entities, byspecifying as argument, the relation name, and the entity to link, asvalue. In the above example, the `Classification` entity is linked toa `CWEtype` via the relation `classifies`. Conversely, if you arecreating a `CWEtype` entity, you can link it to a `Classification`entity, by adding `reverse_classifies` as argument...note:: the :meth:`commit` method is not called automatically. You have to call it explicitly if needed (notably to test operations). It is a good practice to regenerate entities with :meth:`entity_from_eid` after a commit to avoid request cache effects.You can see an example of security tests in the:ref:`adv_tuto_security`.It is possible to have these tests run continuously using `apycot`_..._apycot: http://www.cubicweb.org/project/apycot.._securitytest:Managing connections or users+++++++++++++++++++++++++++++Since unit tests are done with the SQLITE backend and this does notsupport multiple connections at a time, you must be careful whensimulating security, changing users.By default, tests run with a user with admin privileges. Connectionsusing these credentials are accessible through the `admin_access` objectof the test classes.The `repo_cnx()` method returns a connection object that can be used as acontext manager:..sourcecode::python# admin_access is a pre-cooked session wrapping object# it is built with:# self.admin_access = self.new_access('admin')withself.admin_access.repo_cnx()ascnx:cnx.execute(...)self.create_user(cnx,login='user1')cnx.commit()user1access=self.new_access('user1')withuser1access.web_request()asreq:req.execute(...)req.cnx.commit()On exit of the context manager, a rollback is issued, which releasesthe connection. Don't forget to issue the `cnx.commit()` calls!..warning:: Do not use references kept to the entities created with a connection from another one!Email notifications tests`````````````````````````When running tests, potentially generated e-mails are not really sentbut are found in the list `MAILBOX` of module:mod:`cubicweb.devtools.testlib`.You can test your notifications by analyzing the contents of this list, whichcontains objects with two attributes:*`recipients`, the list of recipients*`msg`, email.Message objectLet us look at a simple example from the ``blog`` cube...sourcecode::pythonfromcubicweb.devtools.testlibimportCubicWebTC,MAILBOXclassBlogTestsCubicWebTC(CubicWebTC):"""test blog specific behaviours"""deftest_notifications(self):withself.admin_access.web_request()asreq:cubicweb_blog=req.create_entity('Blog',title=u'cubicweb',description=u'cubicweb is beautiful')blog_entry_1=req.create_entity('BlogEntry',title=u'hop',content=u'cubicweb hop')blog_entry_1.cw_set(entry_of=cubicweb_blog)blog_entry_2=req.create_entity('BlogEntry',title=u'yes',content=u'cubicweb yes')blog_entry_2.cw_set(entry_of=cubicweb_blog)self.assertEqual(len(MAILBOX),0)req.cnx.commit()self.assertEqual(len(MAILBOX),2)mail=MAILBOX[0]self.assertEqual(mail.subject,'[data] hop')mail=MAILBOX[1]self.assertEqual(mail.subject,'[data] yes')Visible actions tests`````````````````````It is easy to write unit tests to test actions which are visible toa user or to a category of users. Let's take an example in the`conference cube`_..._`conference cube`: http://www.cubicweb.org/project/cubicweb-conference..sourcecode::pythonclassConferenceActionsTC(CubicWebTC):defsetup_database(self):withself.admin_access.repo_cnx()ascnx:self.confeid=cnx.create_entity('Conference',title=u'my conf',url_id=u'conf',start_on=date(2010,1,27),end_on=date(2010,1,29),call_open=True,reverse_is_chair_at=chair,reverse_is_reviewer_at=reviewer).eiddeftest_admin(self):withself.admin_access.web_request()asreq:rset=req.find('Conference').one()self.assertListEqual(self.pactions(req,rset),[('workflow',workflow.WorkflowActions),('edit',confactions.ModifyAction),('managepermission',actions.ManagePermissionsAction),('addrelated',actions.AddRelatedActions),('delete',actions.DeleteAction),('generate_badge_action',badges.GenerateBadgeAction),('addtalkinconf',confactions.AddTalkInConferenceAction)])self.assertListEqual(self.action_submenu(req,rset,'addrelated'),[(u'add Track in_conf Conference object',u'http://testing.fr/cubicweb/add/Track'u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&'u'__redirectpath=conference%%2Fconf&'u'__redirectvid='%{'conf':self.confeid}),])You just have to execute a rql query corresponding to the view you want to test,and to compare the result of:meth:`~cubicweb.devtools.testlib.CubicWebTC.pactions` with the list of actionsthat must be visible in the interface. This is a list of tuples. The firstelement is the action's `__regid__`, the second the action's class.To test actions in a submenu, you just have to test the result of:meth:`~cubicweb.devtools.testlib.CubicWebTC.action_submenu` method. The lastparameter of the method is the action's category. The result is a list oftuples. The first element is the action's title, and the second element theaction's url..._automatic_views_tests:Automatic views testing-----------------------This is done automatically with the :class:`cubicweb.devtools.testlib.AutomaticWebTest`class. At cube creation time, the mycube/test/test_mycube.py filecontains such a test. The code here has to be uncommented to beusable, without further modification.The ``auto_populate`` method uses a smart algorithm to createpseudo-random data in the database, thus enabling the views to beinvoked and tested.Depending on the schema, hooks and operations constraints, it is notalways possible for the automatic auto_populate to proceed.It is possible of course to completely redefine auto_populate. Alighter solution is to give hints (fill some class attributes) aboutwhat entities and relations have to be skipped by the auto_populatemechanism. These are:*`no_auto_populate`, may contain a list of entity types to skip*`ignored_relations`, may contain a list of relation types to skip*`application_rql`, may contain a list of rql expressions that auto_populate cannot guess by itself; these must yield resultsets against which views may be selected...warning:: Take care to not let the imported `AutomaticWebTest` in your test module namespace, else both your subclass *and* this parent class will be run.Cache heavy database setup-------------------------------Some test suites require a complex setup of the database that takesseconds (or even minutes) to complete. Doing the whole setup for eachindividual test makes the whole run very slow. The ``CubicWebTC``class offer a simple way to prepare a specific database once formultiple tests. The `test_db_id` class attribute of your``CubicWebTC`` subclass must be set to a unique identifier and the:meth:`pre_setup_database` class method must build the cached content. Asthe :meth:`pre_setup_database` method is not garanteed to be calledevery time a test method is run, you must not set any class attributeto be used during test *there*. Databases for each `test_db_id` areautomatically created if not already in cache. Clearing the cache isup to the user. Cache files are found in the :file:`data/database`subdirectory of your test directory...warning:: Take care to always have the same :meth:`pre_setup_database` function for all classes with a given `test_db_id` otherwise your tests will have unpredictable results depending on the first encountered one.Testing on a real-life database-------------------------------The ``CubicWebTC`` class uses the `cubicweb.devtools.ApptestConfiguration`configuration class to setup its testing environment (database driver,user password, application home, and so on). The `cubicweb.devtools`module also provides a `RealDatabaseConfiguration`class that will read a regular cubicweb sources file to fetch allthis information but will also prevent the database to be initalizedand reset between tests.For a test class to use a specific configuration, you have to setthe `_config` class attribute on the class as in:..sourcecode::pythonfromcubicweb.devtoolsimportRealDatabaseConfigurationfromcubicweb.devtools.testlibimportCubicWebTCclassBlogRealDatabaseTC(CubicWebTC):_config=RealDatabaseConfiguration('blog',sourcefile='/path/to/realdb_sources')deftest_blog_rss(self):withself.admin_access.web_request()asreq:rset=req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, ''B created_by U, U login "logilab", B creation_date D')self.view('rss',rset,req=req)Testing with other cubes------------------------Sometimes a small component cannot be tested all by itself, so oneneeds to specify other cubes to be used as part of the the unit testsuite. This is handled by the ``bootstrap_cubes`` file located under``mycube/test/data``. One example from the `preview` cube:: card, file, previewThe format is:* possibly several empy lines or lines starting with ``#`` (comment lines)* one line containing a comma-separated list of cube names.It is also possible to add a ``schema.py`` file in``mycube/test/data``, which will be used by the testing framework,therefore making new entity types and relations available to thetests. Literate programming--------------------CubicWeb provides some literate programming capabilities. The :ref:`cubicweb-ctl``shell` command accepts different file formats. If your file ends with `.txt`or `.rst`, the file will be parsed by :mod:`doctest.testfile` with CubicWeb's:ref:`migration` API enabled in it.Create a `scenario.txt` file in the `test/` directory and fill with some content.Refer to the :mod:`doctest.testfile``documentation`_..._documentation: http://docs.python.org/library/doctest.htmlThen, you can run it directly by:: $ cubicweb-ctl shell <cube_instance> test/scenario.txtWhen your scenario file is ready, put it in a new test case to be able to runit automatically...sourcecode::pythonfromos.pathimportdirname,joinfromlogilab.common.testlibimportunittest_mainfromcubicweb.devtools.testlibimportCubicWebTCclassAcceptanceTC(CubicWebTC):deftest_scenario(self):self.assertDocTestFile(join(dirname(__file__),'scenario.txt'))if__name__=='__main__':unittest_main()Skipping a scenario```````````````````If you want to set up initial conditions that you can't put in your unit testcase, you have to use a :exc:`KeyboardInterrupt` exception only because of theway :mod:`doctest` module will catch all the exceptions internally. >>> if condition_not_met: ... raise KeyboardInterrupt('please, check your fixture.')Passing paramaters``````````````````Using extra arguments to parametrize your scenario is possible by prepending themby double dashes.Please refer to the `cubicweb-ctl shell --help` usage...important:: Your scenario file must be utf-8 encoded.Test APIS---------Using Pytest````````````The `pytest` utility (shipping with `logilab-common`_, which is amandatory dependency of CubicWeb) extends the Python unittestfunctionality and is the preferred way to run the CubicWeb testsuites. Bare unittests also work the usual way..._logilab-common: http://www.logilab.org/project/logilab-commonTo use it, you may:* just launch `pytest` in your cube to execute all tests (it will discover them automatically)* launch `pytest unittest_foo.py` to execute one test file* launch `pytest unittest_foo.py bar` to execute all test methods and all test cases whose name contains `bar`Additionally, the `-x` option tells pytest to exit at the first erroror failure. The `-i` option tells pytest to drop into pdb whenever anexception occurs in a test.When the `-x` option has been used and the run stopped on a test, itis possible, after having fixed the test, to relaunch pytest with the`-R` option to tell it to start testing again from where it previouslyfailed.Using the `TestCase` base class```````````````````````````````The base class of CubicWebTC is logilab.common.testlib.TestCase, whichprovides a lot of convenient assertion methods...autoclass:: logilab.common.testlib.TestCase:members:CubicWebTC API``````````````..autoclass:: cubicweb.devtools.testlib.CubicWebTC:members:What you need to know about request and session-----------------------------------------------..image:: ../../images/request_session.pngFirst, remember to think that some code run on a client side, someother on the repository side. More precisely:* client side: web interface, raw repoapi connection (cubicweb-ctl shell for instance);* repository side: RQL query execution, that may trigger hooks and operation.The client interacts with the repository through a repoapi connection...note:: These distinctions are going to disappear in cubicweb 3.21 (if not before).A repoapi connection is tied to a session in the repository. The connection andrequest objects are inaccessible from repository code / the session object isinaccessible from client code (theoretically at least).The web interface provides a request class. That `request` object providesaccess to all cubicweb resources, eg:* the registry (which itself provides access to the schema and the configuration);* an underlying repoapi connection (when using req.execute, you actually call the repoapi);* other specific resources depending on the client type (url generation according to base url, form parameters, etc.).A `session` provides an api similar to a request regarding RQL execution andaccess to global resources (registry and all), but also has the followingresponsibilities:* handle transaction data, that will live during the time of a single transaction. This includes the database connections that will be used to execute RQL queries.* handle persistent data that may be used across different (web) requests* security and hooks control (not possible through a request)The `_cw` attribute```````````````````The `_cw` attribute available on every application object provides access to allcubicweb resources, i.e.:- For code running on the client side (eg web interface view), `_cw` is a request instance.- For code running on the repository side (hooks and operation), `_cw` is a Connection or Session instance.Beware some views may be called with a session (e.g. notifications) or with arequest.Request, session and transaction````````````````````````````````In the web interface, an HTTP request is handled by a single request, which willbe thrown away once the response is sent.The web publisher handles the transaction:* commit / rollback is done automatically* you should not commit / rollback explicitly, except if you really need itLet's detail the process:1. an incoming RQL query comes from a client to the web stack2. the web stack opens an authenticated database connection for the request, which is associated to a user session3. the query is executed (through the repository connection)4. this query may trigger hooks. Hooks and operations may execute some rql queries through `cnx.execute`.5. the repository gets the result of the query in 1. If it was a RQL read query, the database connection is released. If it was a write query, the connection is then tied to the session until the transaction is commited or rolled back.6. results are sent back to the clientThis implies several things:* when using a request, or code executed in hooks, this database connection handling is totally transparent* however, take care when writing tests: you are usually faking / testing both the server and the client side, so you have to decide when to use RepoAccess.client_cnx or RepoAccess.repo_cnx. Ask yourself "where will the code I want to test be running, client or repository side?". The response is usually: use a repo (since the "client connection" concept is going away in a couple of releases).