Merge ~cjwatson/launchpad:test-notification-unittest into launchpad:master
- Git
- lp:~cjwatson/launchpad
- test-notification-unittest
- Merge into master
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | 5321fefb88fe518ff55dc694d70367c0322395ff |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~cjwatson/launchpad:test-notification-unittest |
Merge into: | launchpad:master |
Diff against target: |
625 lines (+274/-258) 4 files modified
dev/null (+0/-88) lib/lp/services/webapp/notifications.py (+2/-135) lib/lp/services/webapp/tests/test_doc.py (+0/-5) lib/lp/services/webapp/tests/test_notifications.py (+272/-30) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andrey Fedoseev (community) | Approve | ||
Review via email: mp+426667@code.launchpad.net |
Commit message
Rewrite notification doctests as unit tests
Description of the change
The `zope.container` removal in c4be9ccd819340c
To post a comment you must log in.
Revision history for this message
Andrey Fedoseev (andrey-fedoseev) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/services/webapp/doc/notification-text-escape.rst b/lib/lp/services/webapp/doc/notification-text-escape.rst |
2 | deleted file mode 100644 |
3 | index 4c13fb6..0000000 |
4 | --- a/lib/lp/services/webapp/doc/notification-text-escape.rst |
5 | +++ /dev/null |
6 | @@ -1,88 +0,0 @@ |
7 | -Notification Text Escaping |
8 | -========================== |
9 | - |
10 | -There are a number of user actions that may generate on-screen |
11 | -notifications, such as moving a bug or deleting a branch. Some of |
12 | -these notifications display potentially unsafe text that is obtained |
13 | -from the user. In order to prevent a cross-site-scripting attack, |
14 | -HTML characters in notifications must be escaped. However, there are |
15 | -special cases where notifications from known safe sources must be |
16 | -allowed to pass HTML through. This document exercises these |
17 | -mechanisms. |
18 | - |
19 | - >>> from lp.services.webapp.notifications import ( |
20 | - ... NotificationResponse, NotificationRequest) |
21 | - >>> def new_response(): |
22 | - ... response = NotificationResponse() |
23 | - ... request = NotificationRequest() |
24 | - ... request.response = response |
25 | - ... response._request = request |
26 | - ... return response |
27 | - >>> |
28 | - |
29 | -Plain text passed into the object's addNotification() method is |
30 | -unchanged: |
31 | - |
32 | - >>> response = new_response() |
33 | - >>> response.addNotification('clean') |
34 | - >>> for notification in response.notifications: |
35 | - ... print(notification.message) |
36 | - clean |
37 | - |
38 | -But text containing markup is CGI-escaped: |
39 | - |
40 | - >>> response = new_response() |
41 | - >>> response.addNotification(u'<br/>dirty') |
42 | - >>> for notification in response.notifications: |
43 | - ... print(notification.message) |
44 | - <br/>dirty |
45 | - |
46 | - |
47 | -If the object passed to addNotification() publishes the |
48 | -IStructuredString interface, then a string will be returned with the |
49 | -appropriate sections escaped and unescaped. |
50 | - |
51 | - >>> from lp.services.webapp.interfaces import IStructuredString |
52 | - >>> from lp.services.webapp.escaping import structured |
53 | - >>> msg = u'<b>%(escaped)s</b>' |
54 | - >>> structured_text = structured(msg, escaped=u'<br/>foo') |
55 | - >>> IStructuredString.providedBy(structured_text) |
56 | - True |
57 | - >>> print(structured_text.escapedtext) |
58 | - <b><br/>foo</b> |
59 | - |
60 | - >>> response = new_response() |
61 | - >>> response.addNotification(structured_text) |
62 | - >>> for notification in response.notifications: |
63 | - ... print(notification.message) |
64 | - <b><br/>foo</b> |
65 | - |
66 | -Passing an object to addNotification() that is an instance of |
67 | -zope.i18n.Message will be escaped in the same |
68 | -manner as raw text. |
69 | - |
70 | - >>> import zope.i18n |
71 | - >>> msgtxt = zope.i18n.Message(u'<br/>foo') |
72 | - >>> response = new_response() |
73 | - >>> response.addNotification(msgtxt) |
74 | - >>> for notification in response.notifications: |
75 | - ... print(notification.message) |
76 | - <br/>foo |
77 | - |
78 | -To pass internationalized text that contains markup, one may call |
79 | -structured() directly with an internationalized object. structured() |
80 | -performs the translation and substitution, and the resulting object |
81 | -may then be passed to addNotification(). |
82 | - |
83 | - >>> from lp import _ |
84 | - >>> msgid = _(u'<good/>%(evil)s') |
85 | - >>> escapee = '<evil/>' |
86 | - >>> text = structured(msgid, evil=escapee) |
87 | - >>> print(text.escapedtext) |
88 | - <good/><evil/> |
89 | - |
90 | - >>> response = new_response() |
91 | - >>> response.addNotification(text) |
92 | - >>> for notification in response.notifications: |
93 | - ... print(notification.message) |
94 | - <good/><evil/> |
95 | diff --git a/lib/lp/services/webapp/notifications.py b/lib/lp/services/webapp/notifications.py |
96 | index 9e76431..12a4516 100644 |
97 | --- a/lib/lp/services/webapp/notifications.py |
98 | +++ b/lib/lp/services/webapp/notifications.py |
99 | @@ -41,35 +41,7 @@ class NotificationRequest: |
100 | """NotificationRequest extracts notifications to display to the user |
101 | from the request and session |
102 | |
103 | - It is designed to be mixed in with an IBrowserRequest |
104 | - |
105 | - By default, there are no notifications |
106 | - |
107 | - >>> request = NotificationRequest() |
108 | - >>> len(request.notifications) |
109 | - 0 |
110 | - >>> INotificationRequest.providedBy(request) |
111 | - True |
112 | - |
113 | - >>> request = NotificationRequest() |
114 | - >>> session = ISession(request)[SESSION_KEY] |
115 | - >>> notifications = NotificationList() |
116 | - >>> session['notifications'] = notifications |
117 | - >>> notifications.append(Notification(0, 'Fnord')) |
118 | - >>> for notification in request.notifications: |
119 | - ... print(notification.message) |
120 | - Fnord |
121 | - |
122 | - Note that NotificationRequest.notifications also returns any notifications |
123 | - that have been added so far in this request, making it the single source |
124 | - you need to interogate to display notifications to the user. |
125 | - |
126 | - >>> response = INotificationResponse(request) |
127 | - >>> response.addNotification('Aargh') |
128 | - >>> for notification in request.notifications: |
129 | - ... print(notification.message) |
130 | - Fnord |
131 | - Aargh |
132 | + It is designed to be mixed in with an IBrowserRequest. |
133 | """ |
134 | |
135 | @property |
136 | @@ -86,84 +58,6 @@ class NotificationResponse: |
137 | |
138 | It needs to be mixed in with an IHTTPApplicationResponse so its redirect |
139 | method intercepts the default behaviour. |
140 | - |
141 | - >>> class MyNotificationResponse(NotificationResponse, MockResponse): |
142 | - ... pass |
143 | - >>> response = MyNotificationResponse() |
144 | - >>> INotificationResponse.providedBy(response) |
145 | - True |
146 | - >>> request = NotificationRequest() |
147 | - >>> request.response = response |
148 | - >>> response._request = request |
149 | - >>> request.principal = None # full IRequests are zope.security |
150 | - ... # participations, and NotificationResponse.redirect expects a |
151 | - ... # principal, as in the full IRequest interface. |
152 | - |
153 | - >>> len(response.notifications) |
154 | - 0 |
155 | - |
156 | - >>> response.addNotification("something") |
157 | - >>> len(response.notifications) |
158 | - 1 |
159 | - |
160 | - >>> response.removeAllNotifications() |
161 | - >>> len(response.notifications) |
162 | - 0 |
163 | - |
164 | - >>> msg = structured("<b>%(escaped)s</b>", escaped="<Fnord>") |
165 | - >>> response.addNotification(msg) |
166 | - |
167 | - >>> response.addNotification("Whatever", BrowserNotificationLevel.DEBUG) |
168 | - >>> response.addDebugNotification('Debug') |
169 | - >>> response.addInfoNotification('Info') |
170 | - >>> response.addWarningNotification('Warning') |
171 | - |
172 | - And an odd one to test Bug #54987 |
173 | - |
174 | - >>> from lp import _ |
175 | - >>> response.addErrorNotification(_('Error${value}', mapping={'value':''})) |
176 | - |
177 | - >>> INotificationList.providedBy(response.notifications) |
178 | - True |
179 | - |
180 | - >>> for notification in response.notifications: |
181 | - ... print("%d -- %s" % (notification.level, notification.message)) |
182 | - 20 -- <b><Fnord></b> |
183 | - 10 -- Whatever |
184 | - 10 -- Debug |
185 | - 20 -- Info |
186 | - 30 -- Warning |
187 | - 40 -- Error |
188 | - |
189 | - >>> response.redirect("http://example.com?foo=bar") |
190 | - 302: http://example.com?foo=bar |
191 | - |
192 | - Once redirect has been called, any notifications that have been set |
193 | - are stored in the session |
194 | - |
195 | - >>> for notification in ISession(request)[SESSION_KEY]['notifications']: |
196 | - ... print("%d -- %s" % (notification.level, notification.message)) |
197 | - ... break |
198 | - 20 -- <b><Fnord></b> |
199 | - |
200 | - If there are no notifications, the session is not touched. This ensures |
201 | - that we don't needlessly burden the session storage. |
202 | - |
203 | - >>> response = MyNotificationResponse() |
204 | - >>> request = NotificationRequest() |
205 | - >>> request.response = response |
206 | - >>> response._request = request |
207 | - |
208 | - >>> session = ISession(request)[SESSION_KEY] |
209 | - >>> del ISession(request)[SESSION_KEY]['notifications'] |
210 | - >>> 'notifications' in session |
211 | - False |
212 | - >>> len(response.notifications) |
213 | - 0 |
214 | - >>> response.redirect("http://example.com") |
215 | - 302: http://example.com |
216 | - >>> 'notifications' in session |
217 | - False |
218 | """ |
219 | |
220 | # We stuff our Notifications here until we are sure we should persist it |
221 | @@ -245,34 +139,7 @@ class NotificationResponse: |
222 | |
223 | @implementer(INotificationList) |
224 | class NotificationList(list): |
225 | - """ |
226 | - Collection of INotification instances with a creation date |
227 | - |
228 | - >>> notifications = NotificationList() |
229 | - >>> notifications.created <= datetime.utcnow() |
230 | - True |
231 | - >>> notifications[0] |
232 | - Traceback (most recent call last): |
233 | - ... |
234 | - IndexError: list index out of range |
235 | - |
236 | - >>> debug = BrowserNotificationLevel.DEBUG |
237 | - >>> error = BrowserNotificationLevel.ERROR |
238 | - >>> notifications.append(Notification(error, u'An error')) |
239 | - >>> notifications.append(Notification(debug, u'A debug message')) |
240 | - >>> for notification in notifications: |
241 | - ... print(notification.message) |
242 | - An error |
243 | - A debug message |
244 | - |
245 | - The __getitem__ method is also overloaded to allow TALES expressions |
246 | - to easily retrieve lists of notifications that match a particular |
247 | - notification level. |
248 | - |
249 | - >>> for notification in notifications['debug']: |
250 | - ... print(notification.message) |
251 | - A debug message |
252 | - """ |
253 | + """Collection of INotification instances with a creation date.""" |
254 | |
255 | created = None |
256 | |
257 | diff --git a/lib/lp/services/webapp/tests/test_doc.py b/lib/lp/services/webapp/tests/test_doc.py |
258 | index fa82b88..b8a2e9c 100644 |
259 | --- a/lib/lp/services/webapp/tests/test_doc.py |
260 | +++ b/lib/lp/services/webapp/tests/test_doc.py |
261 | @@ -8,7 +8,6 @@ Run the doctests and pagetests. |
262 | import os |
263 | |
264 | from lp.services.testing import build_test_suite |
265 | -from lp.services.webapp.tests import test_notifications |
266 | from lp.testing.layers import ( |
267 | FunctionalLayer, |
268 | LaunchpadFunctionalLayer, |
269 | @@ -29,10 +28,6 @@ special = { |
270 | '../doc/canonical_url.rst', |
271 | setUp=setUp, tearDown=tearDown, |
272 | layer=FunctionalLayer,), |
273 | - 'notification-text-escape.rst': LayeredDocFileSuite( |
274 | - '../doc/notification-text-escape.rst', |
275 | - setUp=test_notifications.setUp, |
276 | - stdout_logging=False, layer=FunctionalLayer), |
277 | 'test_adapter.rst': LayeredDocFileSuite( |
278 | '../doc/test_adapter.rst', |
279 | setUp=setGlobs, |
280 | diff --git a/lib/lp/services/webapp/tests/test_notifications.py b/lib/lp/services/webapp/tests/test_notifications.py |
281 | index fda9346..434dac1 100644 |
282 | --- a/lib/lp/services/webapp/tests/test_notifications.py |
283 | +++ b/lib/lp/services/webapp/tests/test_notifications.py |
284 | @@ -3,10 +3,13 @@ |
285 | |
286 | """Module docstring goes here.""" |
287 | |
288 | -from doctest import DocTestSuite |
289 | -import unittest |
290 | +from datetime import datetime |
291 | |
292 | -from zope.component import provideAdapter |
293 | +from testtools.matchers import ( |
294 | + MatchesListwise, |
295 | + MatchesStructure, |
296 | + ) |
297 | +from zope.i18n import Message |
298 | from zope.interface import implementer |
299 | from zope.publisher.browser import TestRequest |
300 | from zope.publisher.interfaces.browser import IBrowserRequest |
301 | @@ -16,12 +19,24 @@ from zope.session.interfaces import ( |
302 | ISessionData, |
303 | ) |
304 | |
305 | +from lp import _ |
306 | from lp.services.webapp.escaping import structured |
307 | from lp.services.webapp.interfaces import ( |
308 | + BrowserNotificationLevel, |
309 | + INotificationList, |
310 | INotificationRequest, |
311 | INotificationResponse, |
312 | + IStructuredString, |
313 | + ) |
314 | +from lp.services.webapp.notifications import ( |
315 | + Notification, |
316 | + NotificationList, |
317 | + NotificationRequest, |
318 | + NotificationResponse, |
319 | + SESSION_KEY, |
320 | ) |
321 | -from lp.services.webapp.notifications import NotificationResponse |
322 | +from lp.testing import TestCase |
323 | +from lp.testing.fixture import ZopeAdapterFixture |
324 | from lp.testing.layers import FunctionalLayer |
325 | |
326 | |
327 | @@ -48,11 +63,20 @@ class MockSessionData(dict): |
328 | @implementer(IHTTPApplicationResponse) |
329 | class MockHTTPApplicationResponse: |
330 | |
331 | + def __init__(self, *args, **kwargs): |
332 | + super().__init__(*args, **kwargs) |
333 | + self.redirect_log = [] |
334 | + |
335 | def redirect(self, location, status=None, trusted=False): |
336 | - """Just report the redirection to the doctest""" |
337 | + """Just log the redirection.""" |
338 | if status is None: |
339 | status = 302 |
340 | - print('%d: %s' % (status, location)) |
341 | + self.redirect_log.append((status, location)) |
342 | + |
343 | + |
344 | +class MyNotificationResponse( |
345 | + NotificationResponse, MockHTTPApplicationResponse): |
346 | + pass |
347 | |
348 | |
349 | def adaptNotificationRequestToResponse(request): |
350 | @@ -65,33 +89,251 @@ def adaptNotificationRequestToResponse(request): |
351 | return response |
352 | |
353 | |
354 | -def setUp(test): |
355 | - mock_session = MockSession() |
356 | - provideAdapter(lambda x: mock_session, (INotificationRequest,), ISession) |
357 | - provideAdapter(lambda x: mock_session, (INotificationResponse,), ISession) |
358 | - provideAdapter( |
359 | - adaptNotificationRequestToResponse, |
360 | - (INotificationRequest,), INotificationResponse) |
361 | +class TestNotificationsBase: |
362 | + |
363 | + def setUp(self): |
364 | + super().setUp() |
365 | + |
366 | + mock_session = MockSession() |
367 | + self.useFixture(ZopeAdapterFixture( |
368 | + lambda x: mock_session, (INotificationRequest,), ISession)) |
369 | + self.useFixture(ZopeAdapterFixture( |
370 | + lambda x: mock_session, (INotificationResponse,), ISession)) |
371 | + self.useFixture(ZopeAdapterFixture( |
372 | + adaptNotificationRequestToResponse, |
373 | + (INotificationRequest,), INotificationResponse)) |
374 | + |
375 | + mock_browser_request = TestRequest() |
376 | + self.useFixture(ZopeAdapterFixture( |
377 | + lambda x: mock_browser_request, (INotificationRequest,), |
378 | + IBrowserRequest)) |
379 | + |
380 | + |
381 | +class TestNotificationRequest(TestNotificationsBase, TestCase): |
382 | + |
383 | + layer = FunctionalLayer |
384 | + |
385 | + def test_provides_interface(self): |
386 | + request = NotificationRequest() |
387 | + self.assertTrue(INotificationRequest.providedBy(request)) |
388 | + |
389 | + def test_no_notifications_by_default(self): |
390 | + # By default, there are no notifications. |
391 | + request = NotificationRequest() |
392 | + self.assertEqual(0, len(request.notifications)) |
393 | + |
394 | + def test_single_notification(self): |
395 | + request = NotificationRequest() |
396 | + session = ISession(request)[SESSION_KEY] |
397 | + notifications = NotificationList() |
398 | + session["notifications"] = notifications |
399 | + notifications.append(Notification(0, "Fnord")) |
400 | + self.assertThat( |
401 | + request.notifications, |
402 | + MatchesListwise([MatchesStructure.byEquality(message="Fnord")])) |
403 | + |
404 | + def test_multiple_notifications(self): |
405 | + # NotificationRequest.notifications also returns any notifications |
406 | + # that have been added so far in this request, making it the single |
407 | + # source you need to interrogate to display notifications to the |
408 | + # user. |
409 | + request = NotificationRequest() |
410 | + session = ISession(request)[SESSION_KEY] |
411 | + notifications = NotificationList() |
412 | + session["notifications"] = notifications |
413 | + notifications.append(Notification(0, "Fnord")) |
414 | + response = INotificationResponse(request) |
415 | + response.addNotification("Aargh") |
416 | + self.assertThat( |
417 | + request.notifications, |
418 | + MatchesListwise([ |
419 | + MatchesStructure.byEquality(message="Fnord"), |
420 | + MatchesStructure.byEquality(message="Aargh"), |
421 | + ])) |
422 | + |
423 | + |
424 | +class TestNotificationResponse(TestNotificationsBase, TestCase): |
425 | + |
426 | + layer = FunctionalLayer |
427 | + |
428 | + def test_provides_interface(self): |
429 | + response = NotificationResponse() |
430 | + self.assertTrue(INotificationResponse.providedBy(response)) |
431 | + |
432 | + def makeNotificationResponse(self): |
433 | + # Return a `NotificationResponse` linked to a `NotificationRequest`. |
434 | + response = MyNotificationResponse() |
435 | + request = NotificationRequest() |
436 | + request.response = response |
437 | + response._request = request |
438 | + # Full IRequests are zope.security participations, and |
439 | + # NotificationResponse.redirect expects a principal, as in the full |
440 | + # IRequest interface. |
441 | + request.principal = None |
442 | + return response |
443 | + |
444 | + def test_no_notifications(self): |
445 | + response = self.makeNotificationResponse() |
446 | + self.assertEqual(0, len(response.notifications)) |
447 | + |
448 | + def test_addNotification(self): |
449 | + response = self.makeNotificationResponse() |
450 | + response.addNotification("something") |
451 | + self.assertEqual(1, len(response.notifications)) |
452 | + |
453 | + def test_removeAllNotifications(self): |
454 | + response = self.makeNotificationResponse() |
455 | + response.addNotification("something") |
456 | + response.removeAllNotifications() |
457 | + self.assertEqual(0, len(response.notifications)) |
458 | + |
459 | + def test_many_notifications(self): |
460 | + response = self.makeNotificationResponse() |
461 | + msg = structured("<b>%(escaped)s</b>", escaped="<Fnord>") |
462 | + response.addNotification(msg) |
463 | + response.addNotification("Whatever", BrowserNotificationLevel.DEBUG) |
464 | + response.addDebugNotification("Debug") |
465 | + response.addInfoNotification("Info") |
466 | + response.addWarningNotification("Warning") |
467 | + # And an odd one to test |
468 | + # https://bugs.launchpad.net/launchpad/+bug/54987 |
469 | + response.addErrorNotification( |
470 | + _("Error${value}", mapping={"value": ""})) |
471 | + |
472 | + self.assertTrue(INotificationList.providedBy(response.notifications)) |
473 | + self.assertThat(response.notifications, MatchesListwise([ |
474 | + MatchesStructure.byEquality( |
475 | + level=20, message="<b><Fnord></b>"), |
476 | + MatchesStructure.byEquality(level=10, message="Whatever"), |
477 | + MatchesStructure.byEquality(level=10, message="Debug"), |
478 | + MatchesStructure.byEquality(level=20, message="Info"), |
479 | + MatchesStructure.byEquality(level=30, message="Warning"), |
480 | + MatchesStructure.byEquality(level=40, message="Error"), |
481 | + ])) |
482 | + |
483 | + def test_no_notifications_session_untouched(self): |
484 | + # If there are no notifications, the session is not touched. This |
485 | + # ensures that we don't needlessly burden the session storage. |
486 | + response = self.makeNotificationResponse() |
487 | + session = ISession(response._request)[SESSION_KEY] |
488 | + self.assertNotIn("notifications", session) |
489 | + self.assertEqual(0, len(response.notifications)) |
490 | + response.redirect("http://example.com") |
491 | + self.assertEqual([(302, "http://example.com")], response.redirect_log) |
492 | + self.assertNotIn("notifications", session) |
493 | + |
494 | + |
495 | +class TestNotificationResponseTextEscaping(TestNotificationsBase, TestCase): |
496 | + """Test notification text escaping. |
497 | + |
498 | + There are a number of user actions that may generate on-screen |
499 | + notifications, such as moving a bug or deleting a branch. Some of these |
500 | + notifications display potentially unsafe text that is obtained from the |
501 | + user. In order to prevent a cross-site-scripting attack, HTML |
502 | + characters in notifications must be escaped. However, there are special |
503 | + cases where notifications from known safe sources must be allowed to |
504 | + pass HTML through. |
505 | + """ |
506 | + |
507 | + layer = FunctionalLayer |
508 | + |
509 | + def makeNotificationResponse(self): |
510 | + # Return a `NotificationResponse` linked to a `NotificationRequest`. |
511 | + response = MyNotificationResponse() |
512 | + request = NotificationRequest() |
513 | + request.response = response |
514 | + response._request = request |
515 | + return response |
516 | + |
517 | + def test_addNotification_plain_text(self): |
518 | + # Plain text is left unchanged. |
519 | + response = self.makeNotificationResponse() |
520 | + response.addNotification("clean") |
521 | + self.assertThat( |
522 | + response.notifications, |
523 | + MatchesListwise([MatchesStructure.byEquality(message="clean")])) |
524 | + |
525 | + def test_addNotification_html(self): |
526 | + # HTML is escaped. |
527 | + response = self.makeNotificationResponse() |
528 | + response.addNotification("<br/>dirty") |
529 | + self.assertThat( |
530 | + response.notifications, |
531 | + MatchesListwise([ |
532 | + MatchesStructure.byEquality(message="<br/>dirty"), |
533 | + ])) |
534 | + |
535 | + def test_addNotification_structured_string(self): |
536 | + # If the object passed to `addNotification` publishes the |
537 | + # `IStructuredString` interface, then a string is returned with the |
538 | + # appropriate sections escaped and unescaped. |
539 | + structured_text = structured("<b>%(escaped)s</b>", escaped="<br/>foo") |
540 | + self.assertTrue(IStructuredString.providedBy(structured_text)) |
541 | + self.assertEqual("<b><br/>foo</b>", structured_text.escapedtext) |
542 | + response = self.makeNotificationResponse() |
543 | + response.addNotification(structured_text) |
544 | + self.assertThat( |
545 | + response.notifications, |
546 | + MatchesListwise([ |
547 | + MatchesStructure.byEquality(message="<b><br/>foo</b>"), |
548 | + ])) |
549 | + |
550 | + def test_addNotification_i18n_message(self): |
551 | + # An instance of `zope.i18n.Message` is escaped in the same manner |
552 | + # as raw text. |
553 | + response = self.makeNotificationResponse() |
554 | + response.addNotification(Message("<br/>foo")) |
555 | + self.assertThat( |
556 | + response.notifications, |
557 | + MatchesListwise([ |
558 | + MatchesStructure.byEquality(message="<br/>foo"), |
559 | + ])) |
560 | + |
561 | + def test_addNotification_structured_internationalized(self): |
562 | + # To pass internationalized text that contains markup, one may call |
563 | + # `structured` directly with an internationalized object. |
564 | + # `structured` performs the translation and substitution, and the |
565 | + # resulting object may then be passed to `addNotification`. |
566 | + structured_text = structured(_("<good/>%(evil)s"), evil="<evil/>") |
567 | + self.assertEqual("<good/><evil/>", structured_text.escapedtext) |
568 | + response = self.makeNotificationResponse() |
569 | + response.addNotification(structured_text) |
570 | + self.assertThat( |
571 | + response.notifications, |
572 | + MatchesListwise([ |
573 | + MatchesStructure.byEquality(message="<good/><evil/>"), |
574 | + ])) |
575 | |
576 | - mock_browser_request = TestRequest() |
577 | - provideAdapter( |
578 | - lambda x: mock_browser_request, (INotificationRequest,), |
579 | - IBrowserRequest) |
580 | |
581 | - test.globs['MockResponse'] = MockHTTPApplicationResponse |
582 | - test.globs['structured'] = structured |
583 | +class TestNotificationList(TestNotificationsBase, TestCase): |
584 | |
585 | + layer = FunctionalLayer |
586 | |
587 | -def test_suite(): |
588 | - suite = unittest.TestSuite() |
589 | - doctest_suite = DocTestSuite( |
590 | - 'lp.services.webapp.notifications', |
591 | - setUp=setUp, |
592 | - ) |
593 | - doctest_suite.layer = FunctionalLayer |
594 | - suite.addTest(doctest_suite) |
595 | - return suite |
596 | + def test_empty(self): |
597 | + notifications = NotificationList() |
598 | + self.assertLessEqual(notifications.created, datetime.utcnow()) |
599 | + self.assertRaises(IndexError, notifications.__getitem__, 0) |
600 | |
601 | + def test_iterate(self): |
602 | + notifications = NotificationList() |
603 | + notifications.append( |
604 | + Notification(BrowserNotificationLevel.ERROR, "An error")) |
605 | + notifications.append( |
606 | + Notification(BrowserNotificationLevel.DEBUG, "A debug message")) |
607 | + self.assertThat(notifications, MatchesListwise([ |
608 | + MatchesStructure.byEquality(message="An error"), |
609 | + MatchesStructure.byEquality(message="A debug message"), |
610 | + ])) |
611 | |
612 | -if __name__ == '__main__': |
613 | - unittest.main(defaultTest='test_suite') |
614 | + def test___getitem__(self): |
615 | + # The __getitem__ method is also overloaded to allow TALES |
616 | + # expressions to easily retrieve lists of notifications that match a |
617 | + # particular notification level. |
618 | + notifications = NotificationList() |
619 | + notifications.append( |
620 | + Notification(BrowserNotificationLevel.ERROR, "An error")) |
621 | + notifications.append( |
622 | + Notification(BrowserNotificationLevel.DEBUG, "A debug message")) |
623 | + self.assertThat(notifications["debug"], MatchesListwise([ |
624 | + MatchesStructure.byEquality(message="A debug message"), |
625 | + ])) |