51 from cubicweb import ValidationError |
51 from cubicweb import ValidationError |
52 |
52 |
53 class ClassificationHooksTC(CubicWebTC): |
53 class ClassificationHooksTC(CubicWebTC): |
54 |
54 |
55 def setup_database(self): |
55 def setup_database(self): |
56 req = self.request() |
56 with self.admin_access.repo_cnx() as cnx: |
57 group_etype = req.find_one_entity('CWEType', name='CWGroup') |
57 group_etype = cnx.find('CWEType', name='CWGroup').one() |
58 c1 = req.create_entity('Classification', name=u'classif1', |
58 c1 = cnx.create_entity('Classification', name=u'classif1', |
59 classifies=group_etype) |
59 classifies=group_etype) |
60 user_etype = req.find_one_entity('CWEType', name='CWUser') |
60 user_etype = cnx.find('CWEType', name='CWUser').one() |
61 c2 = req.create_entity('Classification', name=u'classif2', |
61 c2 = cnx.create_entity('Classification', name=u'classif2', |
62 classifies=user_etype) |
62 classifies=user_etype) |
63 self.kw1 = req.create_entity('Keyword', name=u'kwgroup', included_in=c1) |
63 self.kw1eid = cnx.create_entity('Keyword', name=u'kwgroup', included_in=c1).eid |
64 self.kw2 = req.create_entity('Keyword', name=u'kwuser', included_in=c2) |
64 cnx.commit() |
65 |
65 |
66 def test_cannot_create_cycles(self): |
66 def test_cannot_create_cycles(self): |
67 # direct obvious cycle |
67 with self.admin_access.repo_cnx() as cnx: |
68 self.assertRaises(ValidationError, self.kw1.cw_set, |
68 kw1 = cnx.entity_from_eid(self.kw1eid) |
69 subkeyword_of=self.kw1) |
69 # direct obvious cycle |
70 # testing indirect cycles |
70 with self.assertRaises(ValidationError): |
71 kw3 = self.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, ' |
71 kw1.cw_set(subkeyword_of=kw1) |
72 'SK subkeyword_of K WHERE C name "classif1", K eid %s' |
72 cnx.rollback() |
73 % self.kw1.eid).get_entity(0,0) |
73 # testing indirect cycles |
74 self.kw1.cw_set(subkeyword_of=kw3) |
74 kw3 = cnx.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, ' |
75 self.assertRaises(ValidationError, self.commit) |
75 'SK subkeyword_of K WHERE C name "classif1", K eid %(k)s' |
|
76 {'k': kw1}).get_entity(0,0) |
|
77 kw3.cw_set(reverse_subkeyword_of=kw1) |
|
78 self.assertRaises(ValidationError, cnx.commit) |
76 |
79 |
77 The test class defines a :meth:`setup_database` method which populates the |
80 The test class defines a :meth:`setup_database` method which populates the |
78 database with initial data. Each test of the class runs with this |
81 database with initial data. Each test of the class runs with this |
79 pre-populated database. A commit is done automatically after the |
82 pre-populated database. |
80 :meth:`setup_database` call. You don't have to call it explicitely. |
83 |
81 |
84 The test case itself checks that an Operation does its job of |
82 The test case itself checks that an Operation does it job of |
|
83 preventing cycles amongst Keyword entities. |
85 preventing cycles amongst Keyword entities. |
84 |
86 |
85 `create_entity` is a useful method, which easily allows to create an |
87 The `create_entity` method of connection (or request) objects allows |
86 entity. You can link this entity to others entities, by specifying as |
88 to create an entity. You can link this entity to others entities, by |
87 argument, the relation name, and the entity to link, as value. In the |
89 specifying as argument, the relation name, and the entity to link, as |
88 above example, the `Classification` entity is linked to a `CWEtype` |
90 value. In the above example, the `Classification` entity is linked to |
89 via the relation `classifies`. Conversely, if you are creating a |
91 a `CWEtype` via the relation `classifies`. Conversely, if you are |
90 `CWEtype` entity, you can link it to a `Classification` entity, by |
92 creating a `CWEtype` entity, you can link it to a `Classification` |
91 adding `reverse_classifies` as argument. |
93 entity, by adding `reverse_classifies` as argument. |
92 |
94 |
93 .. note:: |
95 .. note:: |
94 |
96 |
95 :meth:`commit` method is not called automatically in test_XXX |
97 the :meth:`commit` method is not called automatically. You have to |
96 methods. You have to call it explicitely if needed (notably to test |
98 call it explicitely if needed (notably to test operations). It is a |
97 operations). It is a good practice to call :meth:`clear_all_caches` |
99 good practice to regenerate entities with :meth:`entity_from_eid` |
98 on entities after a commit to avoid request cache effects. |
100 after a commit to avoid request cache effects. |
99 |
101 |
100 You can see an example of security tests in the |
102 You can see an example of security tests in the |
101 :ref:`adv_tuto_security`. |
103 :ref:`adv_tuto_security`. |
102 |
104 |
103 It is possible to have these tests run continuously using `apycot`_. |
105 It is possible to have these tests run continuously using `apycot`_. |
112 Since unit tests are done with the SQLITE backend and this does not |
114 Since unit tests are done with the SQLITE backend and this does not |
113 support multiple connections at a time, you must be careful when |
115 support multiple connections at a time, you must be careful when |
114 simulating security, changing users. |
116 simulating security, changing users. |
115 |
117 |
116 By default, tests run with a user with admin privileges. This |
118 By default, tests run with a user with admin privileges. This |
117 user/connection must never be closed. |
119 user/connection must never be closed. It is accessible through the |
118 |
120 `admin_access` object of the test classes. |
119 Before a self.login, one has to release the connection pool in use |
121 |
120 with a self.commit, self.rollback or self.close. |
122 The `repo_cnx()` method returns a connection object that can be used as a |
121 |
|
122 The `login` method returns a connection object that can be used as a |
|
123 context manager: |
123 context manager: |
124 |
124 |
125 .. sourcecode:: python |
125 .. sourcecode:: python |
126 |
126 |
127 with self.login('user1') as user: |
127 # admin_access is a pre-cooked session wrapping object |
128 req = user.req |
128 # it is built with: |
|
129 # self.admin_access = self.new_access('admin') |
|
130 with self.admin_access.repo_cnx() as cnx: |
|
131 cnx.execute(...) |
|
132 self.create_user(cnx, login='user1') |
|
133 cnx.commit() |
|
134 |
|
135 user1access = self.new_access('user1') |
|
136 with user1access.web_request() as req: |
129 req.execute(...) |
137 req.execute(...) |
130 |
138 req.cnx.commit() |
131 On exit of the context manager, either a commit or rollback is issued, |
139 |
132 which releases the connection. |
140 On exit of the context manager, a rollback is issued, which releases |
133 |
141 the connection. Don't forget to issue the `cnx.commit()` calls ! |
134 When one is logged in as a normal user and wants to switch back to the |
|
135 admin user without committing, one has to use |
|
136 self.restore_connection(). |
|
137 |
|
138 Usage with restore_connection: |
|
139 |
|
140 .. sourcecode:: python |
|
141 |
|
142 # execute using default admin connection |
|
143 self.execute(...) |
|
144 # I want to login with another user, ensure to free admin connection pool |
|
145 # (could have used rollback but not close here |
|
146 # we should never close defaut admin connection) |
|
147 self.commit() |
|
148 cnx = self.login('user') |
|
149 # execute using user connection |
|
150 self.execute(...) |
|
151 # I want to login with another user or with admin user |
|
152 self.commit(); cnx.close() |
|
153 # restore admin connection, never use cnx = self.login('admin'), it will return |
|
154 # the default admin connection and one may be tempted to close it |
|
155 self.restore_connection() |
|
156 |
142 |
157 .. warning:: |
143 .. warning:: |
158 |
144 |
159 Do not use the references kept to the entities created with a |
145 Do not use the references kept to the entities created with a |
160 connection from another ! |
146 connection from another ! |
180 |
166 |
181 class BlogTestsCubicWebTC(CubicWebTC): |
167 class BlogTestsCubicWebTC(CubicWebTC): |
182 """test blog specific behaviours""" |
168 """test blog specific behaviours""" |
183 |
169 |
184 def test_notifications(self): |
170 def test_notifications(self): |
185 req = self.request() |
171 with self.admin_access.web_request() as req: |
186 cubicweb_blog = req.create_entity('Blog', title=u'cubicweb', |
172 cubicweb_blog = req.create_entity('Blog', title=u'cubicweb', |
187 description=u'cubicweb is beautiful') |
173 description=u'cubicweb is beautiful') |
188 blog_entry_1 = req.create_entity('BlogEntry', title=u'hop', |
174 blog_entry_1 = req.create_entity('BlogEntry', title=u'hop', |
189 content=u'cubicweb hop') |
175 content=u'cubicweb hop') |
190 blog_entry_1.cw_set(entry_of=cubicweb_blog) |
176 blog_entry_1.cw_set(entry_of=cubicweb_blog) |
191 blog_entry_2 = req.create_entity('BlogEntry', title=u'yes', |
177 blog_entry_2 = req.create_entity('BlogEntry', title=u'yes', |
192 content=u'cubicweb yes') |
178 content=u'cubicweb yes') |
193 blog_entry_2.cw_set(entry_of=cubicweb_blog) |
179 blog_entry_2.cw_set(entry_of=cubicweb_blog) |
194 self.assertEqual(len(MAILBOX), 0) |
180 self.assertEqual(len(MAILBOX), 0) |
195 self.commit() |
181 req.cnx.commit() |
196 self.assertEqual(len(MAILBOX), 2) |
182 self.assertEqual(len(MAILBOX), 2) |
197 mail = MAILBOX[0] |
183 mail = MAILBOX[0] |
198 self.assertEqual(mail.subject, '[data] hop') |
184 self.assertEqual(mail.subject, '[data] hop') |
199 mail = MAILBOX[1] |
185 mail = MAILBOX[1] |
200 self.assertEqual(mail.subject, '[data] yes') |
186 self.assertEqual(mail.subject, '[data] yes') |
201 |
187 |
202 Visible actions tests |
188 Visible actions tests |
203 ````````````````````` |
189 ````````````````````` |
204 |
190 |
205 It is easy to write unit tests to test actions which are visible to |
191 It is easy to write unit tests to test actions which are visible to |
210 .. sourcecode:: python |
196 .. sourcecode:: python |
211 |
197 |
212 class ConferenceActionsTC(CubicWebTC): |
198 class ConferenceActionsTC(CubicWebTC): |
213 |
199 |
214 def setup_database(self): |
200 def setup_database(self): |
215 self.conf = self.create_entity('Conference', |
201 with self.admin_access.repo_cnx() as cnx: |
216 title=u'my conf', |
202 self.conf = cnx.create_entity('Conference', |
217 url_id=u'conf', |
203 title=u'my conf', |
218 start_on=date(2010, 1, 27), |
204 url_id=u'conf', |
219 end_on = date(2010, 1, 29), |
205 start_on=date(2010, 1, 27), |
220 call_open=True, |
206 end_on = date(2010, 1, 29), |
221 reverse_is_chair_at=chair, |
207 call_open=True, |
222 reverse_is_reviewer_at=reviewer) |
208 reverse_is_chair_at=chair, |
|
209 reverse_is_reviewer_at=reviewer) |
223 |
210 |
224 def test_admin(self): |
211 def test_admin(self): |
225 req = self.request() |
212 with self.admin_access.web_request() as req: |
226 rset = req.find_entities('Conference') |
213 rset = req.find('Conference').one() |
227 self.assertListEqual(self.pactions(req, rset), |
214 self.assertListEqual(self.pactions(req, rset), |
228 [('workflow', workflow.WorkflowActions), |
215 [('workflow', workflow.WorkflowActions), |
229 ('edit', confactions.ModifyAction), |
216 ('edit', confactions.ModifyAction), |
230 ('managepermission', actions.ManagePermissionsAction), |
217 ('managepermission', actions.ManagePermissionsAction), |
231 ('addrelated', actions.AddRelatedActions), |
218 ('addrelated', actions.AddRelatedActions), |
232 ('delete', actions.DeleteAction), |
219 ('delete', actions.DeleteAction), |
233 ('generate_badge_action', badges.GenerateBadgeAction), |
220 ('generate_badge_action', badges.GenerateBadgeAction), |
234 ('addtalkinconf', confactions.AddTalkInConferenceAction) |
221 ('addtalkinconf', confactions.AddTalkInConferenceAction) |
235 ]) |
222 ]) |
236 self.assertListEqual(self.action_submenu(req, rset, 'addrelated'), |
223 self.assertListEqual(self.action_submenu(req, rset, 'addrelated'), |
237 [(u'add Track in_conf Conference object', |
224 [(u'add Track in_conf Conference object', |
238 u'http://testing.fr/cubicweb/add/Track' |
225 u'http://testing.fr/cubicweb/add/Track' |
239 u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&' |
226 u'?__linkto=in_conf%%3A%(conf)s%%3Asubject&' |
240 u'__redirectpath=conference%%2Fconf&' |
227 u'__redirectpath=conference%%2Fconf&' |
241 u'__redirectvid=' % {'conf': self.conf.eid}), |
228 u'__redirectvid=' % {'conf': self.conf.eid}), |
242 ]) |
229 ]) |
243 |
230 |
244 You just have to execute a rql query corresponding to the view you want to test, |
231 You just have to execute a rql query corresponding to the view you want to test, |
245 and to compare the result of |
232 and to compare the result of |
246 :meth:`~cubicweb.devtools.testlib.CubicWebTC.pactions` with the list of actions |
233 :meth:`~cubicweb.devtools.testlib.CubicWebTC.pactions` with the list of actions |
247 that must be visible in the interface. This is a list of tuples. The first |
234 that must be visible in the interface. This is a list of tuples. The first |
288 namespace, else both your subclass *and* this parent class will be run. |
275 namespace, else both your subclass *and* this parent class will be run. |
289 |
276 |
290 Cache heavy database setup |
277 Cache heavy database setup |
291 ------------------------------- |
278 ------------------------------- |
292 |
279 |
293 Some tests suite require a complex setup of the database that takes seconds (or |
280 Some tests suite require a complex setup of the database that takes |
294 event minutes) to complete. Doing the whole setup for all individual tests make |
281 seconds (or event minutes) to complete. Doing the whole setup for all |
295 the whole run very slow. The ``CubicWebTC`` class offer a simple way to prepare |
282 individual tests make the whole run very slow. The ``CubicWebTC`` |
296 specific database once for multiple tests. The `test_db_id` class attribute of |
283 class offer a simple way to prepare specific database once for |
297 your ``CubicWebTC`` must be set a unique identifier and the |
284 multiple tests. The `test_db_id` class attribute of your |
298 :meth:`pre_setup_database` class method build the cached content. As the |
285 ``CubicWebTC`` must be set a unique identifier and the |
299 :meth:`pre_setup_database` method is not grantee to be called, you must not set |
286 :meth:`pre_setup_database` class method build the cached content. As |
300 any class attribut to be used during test there. Databases for each `test_db_id` |
287 the :meth:`pre_setup_database` method is not garantee to be called |
301 are automatically created if not already in cache. Clearing the cache is up to |
288 every time a test method is run, you must not set any class attribute |
302 the user. Cache files are found in the :file:`data/database` subdirectory of your |
289 to be used during test *there*. Databases for each `test_db_id` are |
303 test directory. |
290 automatically created if not already in cache. Clearing the cache is |
|
291 up to the user. Cache files are found in the :file:`data/database` |
|
292 subdirectory of your test directory. |
304 |
293 |
305 .. warning:: |
294 .. warning:: |
306 |
295 |
307 Take care to always have the same :meth:`pre_setup_database` function for all |
296 Take care to always have the same :meth:`pre_setup_database` |
308 call with a given `test_db_id` otherwise you test will have unpredictable |
297 function for all calls with a given `test_db_id` otherwise your test |
309 result given the first encountered one. |
298 will have unpredictable results depending on the first encountered one. |
|
299 |
310 |
300 |
311 Testing on a real-life database |
301 Testing on a real-life database |
312 ------------------------------- |
302 ------------------------------- |
313 |
303 |
314 The ``CubicWebTC`` class uses the `cubicweb.devtools.ApptestConfiguration` |
304 The ``CubicWebTC`` class uses the `cubicweb.devtools.ApptestConfiguration` |
330 class BlogRealDatabaseTC(CubicWebTC): |
320 class BlogRealDatabaseTC(CubicWebTC): |
331 _config = RealDatabaseConfiguration('blog', |
321 _config = RealDatabaseConfiguration('blog', |
332 sourcefile='/path/to/realdb_sources') |
322 sourcefile='/path/to/realdb_sources') |
333 |
323 |
334 def test_blog_rss(self): |
324 def test_blog_rss(self): |
335 req = self.request() |
325 with self.admin_access.web_request() as req: |
336 rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, ' |
326 rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, ' |
337 'B created_by U, U login "logilab", B creation_date D') |
327 'B created_by U, U login "logilab", B creation_date D') |
338 self.view('rss', rset, req=req) |
328 self.view('rss', rset, req=req) |
339 |
329 |
340 |
330 |
341 Testing with other cubes |
331 Testing with other cubes |
342 ------------------------ |
332 ------------------------ |
527 be thrown away once the response is sent. |
522 be thrown away once the response is sent. |
528 |
523 |
529 The web publisher handles the transaction: |
524 The web publisher handles the transaction: |
530 |
525 |
531 * commit / rollback is done automatically |
526 * commit / rollback is done automatically |
532 * you should not commit / rollback explicitly |
527 |
533 |
528 * you should not commit / rollback explicitly, except if you really |
534 Because a session lives for a long time, and database connections are a limited |
529 need it |
535 resource, we can't bind a session to its own database connection for all its |
|
536 lifetime. The repository handles a pool of connections (4 by default), and it's |
|
537 responsible to attribute them as needed. |
|
538 |
530 |
539 Let's detail the process: |
531 Let's detail the process: |
540 |
532 |
541 1. an incoming RQL query comes from a client to the repository |
533 1. an incoming RQL query comes from a client to the web stack |
542 |
534 |
543 2. the repository attributes a database connection to the session |
535 2. the web stack opens an authenticated database connection for the |
544 |
536 request, which is associated to a user session |
545 3. the repository's querier executes the query |
537 |
|
538 3. the query is executed (through the repository connection) |
546 |
539 |
547 4. this query may trigger hooks. Hooks and operation may execute some rql queries |
540 4. this query may trigger hooks. Hooks and operation may execute some rql queries |
548 through `_cw.execute`. Those queries go directly to the querier, hence don't |
541 through `cnx.execute`. |
549 touch the database connection, they use the one attributed in 2. |
|
550 |
542 |
551 5. the repository gets the result of the query in 1. If it was a RQL read query, |
543 5. the repository gets the result of the query in 1. If it was a RQL read query, |
552 the database connection is released. If it was a write query, the connection |
544 the database connection is released. If it was a write query, the connection |
553 is then tied to the session until the transaction is commited or rollbacked. |
545 is then tied to the session until the transaction is commited or rollbacked. |
554 |
546 |
555 6. results are sent back to the client |
547 6. results are sent back to the client |
556 |
548 |
557 This implies several things: |
549 This implies several things: |
558 |
550 |
559 * when using a request, or code executed in hooks, this database connection |
551 * when using a request, or code executed in hooks, this database |
560 handling is totally transparent |
552 connection handling is totally transparent |
561 |
553 |
562 * however, take care when writing tests: you are usually faking / testing both the |
554 * however, take care when writing tests: you are usually faking / |
563 server and the client side, so you have to decide when to use RepoAccess.client_cnx / |
555 testing both the server and the client side, so you have to decide |
564 RepoAccess.repo_cnx. Ask yourself "where the code I want to test will be running, |
556 when to use RepoAccess.client_cnx or RepoAccess.repo_cnx. Ask |
565 client or repository side ?". The response is usually : use a client connection :) |
557 yourself "where the code I want to test will be running, client or |
566 However, if you really need using a server-side object: |
558 repository side ?". The response is usually: use a repo (since the |
567 |
559 "client connection" concept is going away in a couple of releases). |
568 - commit / rollback will free the database connection (unless explicitly told |
|
569 not to do so). |
|
570 |
|
571 - if you issue a query after that without asking for a database connection |
|
572 (`session.get_cnxset()`), you will end up with a 'None type has no attribute |
|
573 source()' error |
|