Merge lp:~nataliabidart/software-center/winged-migration into lp:software-center
- winged-migration
- Merge into trunk
Proposed by
Natalia Bidart
Status: | Merged |
---|---|
Merged at revision: | 3058 |
Proposed branch: | lp:~nataliabidart/software-center/winged-migration |
Merge into: | lp:software-center |
Diff against target: |
4677 lines (+4546/-76) 8 files modified
data/ui/sso/sso.ui (+920/-0) run-tests.sh (+5/-2) setup.py (+76/-74) software-center-sso-gtk (+33/-0) softwarecenter/sso/__init__.py (+18/-0) softwarecenter/sso/gui.py (+1168/-0) softwarecenter/sso/tests/__init__.py (+26/-0) softwarecenter/sso/tests/test_gui.py (+2300/-0) |
To merge this branch: | bzr merge lp:~nataliabidart/software-center/winged-migration |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
software-store-developers | Pending | ||
Review via email: mp+112135@code.launchpad.net |
Commit message
- Moved the code for the GTK+ SSO UI from the ubuntu-sso-client project to this project.
Description of the change
To post a comment you must log in.
- 3056. By Natalia Bidart
-
Renaming ubuntu-
sso-login- gtk executable to software- center- sso-gtk.
Revision history for this message
dobey (dobey) wrote : | # |
- 3057. By Natalia Bidart
-
Removing the dependency on twisted's trial to run the GTK+ SSO test suite.
- 3058. By Natalia Bidart
-
Remove outdated reference to trial.
- 3059. By Natalia Bidart
-
Merged trunk in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'data/ui/sso' |
2 | === added file 'data/ui/sso/sso.ui' |
3 | --- data/ui/sso/sso.ui 1970-01-01 00:00:00 +0000 |
4 | +++ data/ui/sso/sso.ui 2012-06-28 15:39:18 +0000 |
5 | @@ -0,0 +1,920 @@ |
6 | +<?xml version="1.0" encoding="UTF-8"?> |
7 | +<interface> |
8 | + <requires lib="gtk+" version="2.16"/> |
9 | + <object class="GtkVBox" id="enter_details_vbox"> |
10 | + <property name="visible">True</property> |
11 | + <property name="can_focus">False</property> |
12 | + <property name="spacing">5</property> |
13 | + <child> |
14 | + <object class="GtkHBox" id="emails_hbox"> |
15 | + <property name="visible">True</property> |
16 | + <property name="can_focus">False</property> |
17 | + <property name="spacing">5</property> |
18 | + <property name="homogeneous">True</property> |
19 | + <child> |
20 | + <placeholder/> |
21 | + </child> |
22 | + <child> |
23 | + <placeholder/> |
24 | + </child> |
25 | + </object> |
26 | + <packing> |
27 | + <property name="expand">False</property> |
28 | + <property name="fill">True</property> |
29 | + <property name="position">0</property> |
30 | + </packing> |
31 | + </child> |
32 | + <child> |
33 | + <object class="GtkHBox" id="passwords_hbox"> |
34 | + <property name="visible">True</property> |
35 | + <property name="can_focus">False</property> |
36 | + <property name="spacing">5</property> |
37 | + <property name="homogeneous">True</property> |
38 | + <child> |
39 | + <placeholder/> |
40 | + </child> |
41 | + <child> |
42 | + <placeholder/> |
43 | + </child> |
44 | + </object> |
45 | + <packing> |
46 | + <property name="expand">False</property> |
47 | + <property name="fill">True</property> |
48 | + <property name="position">1</property> |
49 | + </packing> |
50 | + </child> |
51 | + <child> |
52 | + <object class="GtkLabel" id="password_help_label"> |
53 | + <property name="visible">True</property> |
54 | + <property name="can_focus">False</property> |
55 | + <property name="label">password help</property> |
56 | + <property name="wrap">True</property> |
57 | + </object> |
58 | + <packing> |
59 | + <property name="expand">False</property> |
60 | + <property name="fill">True</property> |
61 | + <property name="position">2</property> |
62 | + </packing> |
63 | + </child> |
64 | + <child> |
65 | + <object class="GtkAlignment" id="alignment5"> |
66 | + <property name="visible">True</property> |
67 | + <property name="can_focus">False</property> |
68 | + <property name="xscale">0</property> |
69 | + <property name="yscale">0</property> |
70 | + <child> |
71 | + <object class="GtkHBox" id="hbox1"> |
72 | + <property name="visible">True</property> |
73 | + <property name="can_focus">False</property> |
74 | + <child> |
75 | + <object class="GtkVBox" id="captcha_vbox"> |
76 | + <property name="width_request">300</property> |
77 | + <property name="height_request">60</property> |
78 | + <property name="visible">True</property> |
79 | + <property name="can_focus">False</property> |
80 | + <child> |
81 | + <object class="GtkEventBox" id="captcha_loading"> |
82 | + <property name="width_request">300</property> |
83 | + <property name="height_request">60</property> |
84 | + <property name="visible">True</property> |
85 | + <property name="can_focus">False</property> |
86 | + <child> |
87 | + <placeholder/> |
88 | + </child> |
89 | + </object> |
90 | + <packing> |
91 | + <property name="expand">False</property> |
92 | + <property name="fill">False</property> |
93 | + <property name="position">0</property> |
94 | + </packing> |
95 | + </child> |
96 | + <child> |
97 | + <object class="GtkImage" id="captcha_image"> |
98 | + <property name="width_request">300</property> |
99 | + <property name="visible">True</property> |
100 | + <property name="can_focus">False</property> |
101 | + <property name="stock">gtk-missing-image</property> |
102 | + </object> |
103 | + <packing> |
104 | + <property name="expand">True</property> |
105 | + <property name="fill">True</property> |
106 | + <property name="position">1</property> |
107 | + </packing> |
108 | + </child> |
109 | + </object> |
110 | + <packing> |
111 | + <property name="expand">False</property> |
112 | + <property name="fill">False</property> |
113 | + <property name="position">0</property> |
114 | + </packing> |
115 | + </child> |
116 | + <child> |
117 | + <object class="GtkVBox" id="vbox1"> |
118 | + <property name="visible">True</property> |
119 | + <property name="can_focus">False</property> |
120 | + <child> |
121 | + <object class="GtkButton" id="captcha_reload_button"> |
122 | + <property name="use_action_appearance">False</property> |
123 | + <property name="visible">True</property> |
124 | + <property name="can_focus">True</property> |
125 | + <property name="receives_default">True</property> |
126 | + <property name="use_action_appearance">False</property> |
127 | + <property name="relief">none</property> |
128 | + <property name="focus_on_click">False</property> |
129 | + <signal name="clicked" handler="on_captcha_reload_button_clicked" swapped="no"/> |
130 | + <child> |
131 | + <object class="GtkImage" id="image1"> |
132 | + <property name="visible">True</property> |
133 | + <property name="can_focus">False</property> |
134 | + <property name="icon_name">reload</property> |
135 | + </object> |
136 | + </child> |
137 | + </object> |
138 | + <packing> |
139 | + <property name="expand">False</property> |
140 | + <property name="fill">True</property> |
141 | + <property name="position">0</property> |
142 | + </packing> |
143 | + </child> |
144 | + <child> |
145 | + <placeholder/> |
146 | + </child> |
147 | + <child> |
148 | + <placeholder/> |
149 | + </child> |
150 | + </object> |
151 | + <packing> |
152 | + <property name="expand">False</property> |
153 | + <property name="fill">True</property> |
154 | + <property name="position">1</property> |
155 | + </packing> |
156 | + </child> |
157 | + </object> |
158 | + </child> |
159 | + </object> |
160 | + <packing> |
161 | + <property name="expand">False</property> |
162 | + <property name="fill">True</property> |
163 | + <property name="position">3</property> |
164 | + </packing> |
165 | + </child> |
166 | + <child> |
167 | + <object class="GtkVBox" id="captcha_solution_vbox"> |
168 | + <property name="visible">True</property> |
169 | + <property name="can_focus">False</property> |
170 | + <child> |
171 | + <placeholder/> |
172 | + </child> |
173 | + </object> |
174 | + <packing> |
175 | + <property name="expand">False</property> |
176 | + <property name="fill">True</property> |
177 | + <property name="position">4</property> |
178 | + </packing> |
179 | + </child> |
180 | + <child> |
181 | + <object class="GtkCheckButton" id="yes_to_updates_checkbutton"> |
182 | + <property name="label" translatable="yes">yes to updates</property> |
183 | + <property name="use_action_appearance">False</property> |
184 | + <property name="visible">True</property> |
185 | + <property name="can_focus">True</property> |
186 | + <property name="receives_default">False</property> |
187 | + <property name="use_action_appearance">False</property> |
188 | + <property name="active">True</property> |
189 | + <property name="draw_indicator">True</property> |
190 | + </object> |
191 | + <packing> |
192 | + <property name="expand">False</property> |
193 | + <property name="fill">True</property> |
194 | + <property name="position">5</property> |
195 | + </packing> |
196 | + </child> |
197 | + <child> |
198 | + <object class="GtkVBox" id="tc_vbox"> |
199 | + <property name="visible">True</property> |
200 | + <property name="can_focus">False</property> |
201 | + <property name="spacing">5</property> |
202 | + <child> |
203 | + <object class="GtkCheckButton" id="yes_to_tc_checkbutton"> |
204 | + <property name="label" translatable="yes">yes to tc</property> |
205 | + <property name="use_action_appearance">False</property> |
206 | + <property name="visible">True</property> |
207 | + <property name="can_focus">True</property> |
208 | + <property name="receives_default">False</property> |
209 | + <property name="use_action_appearance">False</property> |
210 | + <property name="draw_indicator">True</property> |
211 | + </object> |
212 | + <packing> |
213 | + <property name="expand">False</property> |
214 | + <property name="fill">True</property> |
215 | + <property name="position">0</property> |
216 | + </packing> |
217 | + </child> |
218 | + <child> |
219 | + <object class="GtkHButtonBox" id="hbuttonbox3"> |
220 | + <property name="visible">True</property> |
221 | + <property name="can_focus">False</property> |
222 | + <property name="layout_style">start</property> |
223 | + <child> |
224 | + <object class="GtkButton" id="tc_button"> |
225 | + <property name="label">show tc</property> |
226 | + <property name="use_action_appearance">False</property> |
227 | + <property name="visible">True</property> |
228 | + <property name="can_focus">True</property> |
229 | + <property name="receives_default">True</property> |
230 | + <property name="use_action_appearance">False</property> |
231 | + <signal name="clicked" handler="on_tc_button_clicked" swapped="no"/> |
232 | + </object> |
233 | + <packing> |
234 | + <property name="expand">False</property> |
235 | + <property name="fill">False</property> |
236 | + <property name="position">1</property> |
237 | + </packing> |
238 | + </child> |
239 | + </object> |
240 | + <packing> |
241 | + <property name="expand">False</property> |
242 | + <property name="fill">True</property> |
243 | + <property name="position">1</property> |
244 | + </packing> |
245 | + </child> |
246 | + <child> |
247 | + <object class="GtkLabel" id="tc_warning_label"> |
248 | + <property name="visible">True</property> |
249 | + <property name="can_focus">False</property> |
250 | + <property name="xalign">0</property> |
251 | + <property name="label">tc warning</property> |
252 | + <property name="wrap">True</property> |
253 | + </object> |
254 | + <packing> |
255 | + <property name="expand">True</property> |
256 | + <property name="fill">True</property> |
257 | + <property name="position">2</property> |
258 | + </packing> |
259 | + </child> |
260 | + </object> |
261 | + <packing> |
262 | + <property name="expand">False</property> |
263 | + <property name="fill">True</property> |
264 | + <property name="position">6</property> |
265 | + </packing> |
266 | + </child> |
267 | + <child> |
268 | + <object class="GtkHBox" id="hbox2"> |
269 | + <property name="visible">True</property> |
270 | + <property name="can_focus">False</property> |
271 | + <property name="spacing">5</property> |
272 | + <child> |
273 | + <object class="GtkHButtonBox" id="hbuttonbox9"> |
274 | + <property name="visible">True</property> |
275 | + <property name="can_focus">False</property> |
276 | + <property name="layout_style">start</property> |
277 | + <child> |
278 | + <object class="GtkLinkButton" id="login_button"> |
279 | + <property name="label">login button</property> |
280 | + <property name="use_action_appearance">False</property> |
281 | + <property name="visible">True</property> |
282 | + <property name="can_focus">True</property> |
283 | + <property name="receives_default">True</property> |
284 | + <property name="use_action_appearance">False</property> |
285 | + <property name="relief">none</property> |
286 | + <property name="uri">foo</property> |
287 | + <signal name="activate-link" handler="on_activate_link" swapped="no"/> |
288 | + <signal name="clicked" handler="on_sign_in_button_clicked" swapped="no"/> |
289 | + </object> |
290 | + <packing> |
291 | + <property name="expand">False</property> |
292 | + <property name="fill">False</property> |
293 | + <property name="position">0</property> |
294 | + </packing> |
295 | + </child> |
296 | + </object> |
297 | + <packing> |
298 | + <property name="expand">False</property> |
299 | + <property name="fill">True</property> |
300 | + <property name="position">0</property> |
301 | + </packing> |
302 | + </child> |
303 | + <child> |
304 | + <object class="GtkHButtonBox" id="hbuttonbox1"> |
305 | + <property name="visible">True</property> |
306 | + <property name="can_focus">False</property> |
307 | + <property name="spacing">5</property> |
308 | + <property name="layout_style">end</property> |
309 | + <child> |
310 | + <object class="GtkButton" id="join_cancel_button"> |
311 | + <property name="label">gtk-cancel</property> |
312 | + <property name="use_action_appearance">False</property> |
313 | + <property name="visible">True</property> |
314 | + <property name="can_focus">True</property> |
315 | + <property name="receives_default">True</property> |
316 | + <property name="use_action_appearance">False</property> |
317 | + <property name="use_stock">True</property> |
318 | + </object> |
319 | + <packing> |
320 | + <property name="expand">False</property> |
321 | + <property name="fill">False</property> |
322 | + <property name="position">0</property> |
323 | + </packing> |
324 | + </child> |
325 | + <child> |
326 | + <object class="GtkButton" id="join_ok_button"> |
327 | + <property name="label">gtk-go-forward</property> |
328 | + <property name="use_action_appearance">False</property> |
329 | + <property name="visible">True</property> |
330 | + <property name="can_focus">True</property> |
331 | + <property name="receives_default">True</property> |
332 | + <property name="use_action_appearance">False</property> |
333 | + <property name="use_stock">True</property> |
334 | + <signal name="clicked" handler="on_join_ok_button_clicked" swapped="no"/> |
335 | + </object> |
336 | + <packing> |
337 | + <property name="expand">False</property> |
338 | + <property name="fill">False</property> |
339 | + <property name="position">1</property> |
340 | + </packing> |
341 | + </child> |
342 | + </object> |
343 | + <packing> |
344 | + <property name="expand">False</property> |
345 | + <property name="fill">True</property> |
346 | + <property name="pack_type">end</property> |
347 | + <property name="position">1</property> |
348 | + </packing> |
349 | + </child> |
350 | + </object> |
351 | + <packing> |
352 | + <property name="expand">False</property> |
353 | + <property name="fill">True</property> |
354 | + <property name="pack_type">end</property> |
355 | + <property name="position">7</property> |
356 | + </packing> |
357 | + </child> |
358 | + </object> |
359 | + <object class="GtkVBox" id="finish_vbox"> |
360 | + <property name="visible">True</property> |
361 | + <property name="can_focus">False</property> |
362 | + <property name="spacing">10</property> |
363 | + <child> |
364 | + <object class="GtkLabel" id="finish_label"> |
365 | + <property name="visible">True</property> |
366 | + <property name="can_focus">False</property> |
367 | + <property name="wrap">True</property> |
368 | + </object> |
369 | + <packing> |
370 | + <property name="expand">True</property> |
371 | + <property name="fill">True</property> |
372 | + <property name="position">0</property> |
373 | + </packing> |
374 | + </child> |
375 | + <child> |
376 | + <object class="GtkHButtonBox" id="hbuttonbox8"> |
377 | + <property name="visible">True</property> |
378 | + <property name="can_focus">False</property> |
379 | + <property name="layout_style">end</property> |
380 | + <child> |
381 | + <object class="GtkButton" id="finish_close_button"> |
382 | + <property name="label">gtk-close</property> |
383 | + <property name="use_action_appearance">False</property> |
384 | + <property name="visible">True</property> |
385 | + <property name="can_focus">True</property> |
386 | + <property name="receives_default">True</property> |
387 | + <property name="use_action_appearance">False</property> |
388 | + <property name="use_stock">True</property> |
389 | + <signal name="clicked" handler="on_close_clicked" swapped="no"/> |
390 | + </object> |
391 | + <packing> |
392 | + <property name="expand">False</property> |
393 | + <property name="fill">False</property> |
394 | + <property name="position">0</property> |
395 | + </packing> |
396 | + </child> |
397 | + </object> |
398 | + <packing> |
399 | + <property name="expand">False</property> |
400 | + <property name="fill">True</property> |
401 | + <property name="position">1</property> |
402 | + </packing> |
403 | + </child> |
404 | + </object> |
405 | + <object class="GtkVBox" id="login_vbox"> |
406 | + <property name="visible">True</property> |
407 | + <property name="can_focus">False</property> |
408 | + <property name="spacing">10</property> |
409 | + <child> |
410 | + <object class="GtkAlignment" id="alignment3"> |
411 | + <property name="visible">True</property> |
412 | + <property name="can_focus">False</property> |
413 | + <property name="xscale">0</property> |
414 | + <property name="yscale">0</property> |
415 | + <child> |
416 | + <object class="GtkVBox" id="login_details_vbox"> |
417 | + <property name="visible">True</property> |
418 | + <property name="can_focus">False</property> |
419 | + <property name="spacing">5</property> |
420 | + <child> |
421 | + <placeholder/> |
422 | + </child> |
423 | + <child> |
424 | + <placeholder/> |
425 | + </child> |
426 | + </object> |
427 | + </child> |
428 | + </object> |
429 | + <packing> |
430 | + <property name="expand">True</property> |
431 | + <property name="fill">True</property> |
432 | + <property name="position">0</property> |
433 | + </packing> |
434 | + </child> |
435 | + <child> |
436 | + <object class="GtkHBox" id="hbox3"> |
437 | + <property name="visible">True</property> |
438 | + <property name="can_focus">False</property> |
439 | + <property name="spacing">5</property> |
440 | + <child> |
441 | + <object class="GtkHButtonBox" id="hbuttonbox10"> |
442 | + <property name="visible">True</property> |
443 | + <property name="can_focus">False</property> |
444 | + <property name="layout_style">start</property> |
445 | + <child> |
446 | + <object class="GtkLinkButton" id="forgotten_password_button"> |
447 | + <property name="label" translatable="yes">forgot password button</property> |
448 | + <property name="use_action_appearance">False</property> |
449 | + <property name="visible">True</property> |
450 | + <property name="can_focus">True</property> |
451 | + <property name="receives_default">True</property> |
452 | + <property name="has_tooltip">True</property> |
453 | + <property name="use_action_appearance">False</property> |
454 | + <property name="relief">none</property> |
455 | + <property name="uri">foo</property> |
456 | + <signal name="activate-link" handler="on_activate_link" swapped="no"/> |
457 | + <signal name="clicked" handler="on_forgotten_password_button_clicked" swapped="no"/> |
458 | + </object> |
459 | + <packing> |
460 | + <property name="expand">False</property> |
461 | + <property name="fill">False</property> |
462 | + <property name="padding">10</property> |
463 | + <property name="position">0</property> |
464 | + </packing> |
465 | + </child> |
466 | + </object> |
467 | + <packing> |
468 | + <property name="expand">False</property> |
469 | + <property name="fill">True</property> |
470 | + <property name="position">0</property> |
471 | + </packing> |
472 | + </child> |
473 | + <child> |
474 | + <object class="GtkHButtonBox" id="hbuttonbox5"> |
475 | + <property name="visible">True</property> |
476 | + <property name="can_focus">False</property> |
477 | + <property name="spacing">5</property> |
478 | + <property name="layout_style">end</property> |
479 | + <child> |
480 | + <object class="GtkButton" id="login_cancel_button"> |
481 | + <property name="label">gtk-cancel</property> |
482 | + <property name="use_action_appearance">False</property> |
483 | + <property name="visible">True</property> |
484 | + <property name="can_focus">True</property> |
485 | + <property name="receives_default">True</property> |
486 | + <property name="use_action_appearance">False</property> |
487 | + <property name="use_stock">True</property> |
488 | + </object> |
489 | + <packing> |
490 | + <property name="expand">False</property> |
491 | + <property name="fill">False</property> |
492 | + <property name="position">0</property> |
493 | + </packing> |
494 | + </child> |
495 | + <child> |
496 | + <object class="GtkButton" id="login_back_button"> |
497 | + <property name="label">gtk-go-back</property> |
498 | + <property name="use_action_appearance">False</property> |
499 | + <property name="visible">True</property> |
500 | + <property name="can_focus">True</property> |
501 | + <property name="receives_default">True</property> |
502 | + <property name="use_action_appearance">False</property> |
503 | + <property name="use_stock">True</property> |
504 | + <signal name="clicked" handler="on_login_back_button_clicked" swapped="no"/> |
505 | + </object> |
506 | + <packing> |
507 | + <property name="expand">False</property> |
508 | + <property name="fill">False</property> |
509 | + <property name="position">1</property> |
510 | + </packing> |
511 | + </child> |
512 | + <child> |
513 | + <object class="GtkButton" id="login_ok_button"> |
514 | + <property name="label">gtk-connect</property> |
515 | + <property name="use_action_appearance">False</property> |
516 | + <property name="visible">True</property> |
517 | + <property name="can_focus">True</property> |
518 | + <property name="receives_default">True</property> |
519 | + <property name="use_action_appearance">False</property> |
520 | + <property name="use_stock">True</property> |
521 | + <signal name="clicked" handler="on_login_connect_button_clicked" swapped="no"/> |
522 | + </object> |
523 | + <packing> |
524 | + <property name="expand">False</property> |
525 | + <property name="fill">False</property> |
526 | + <property name="position">2</property> |
527 | + </packing> |
528 | + </child> |
529 | + </object> |
530 | + <packing> |
531 | + <property name="expand">False</property> |
532 | + <property name="fill">True</property> |
533 | + <property name="pack_type">end</property> |
534 | + <property name="position">1</property> |
535 | + </packing> |
536 | + </child> |
537 | + </object> |
538 | + <packing> |
539 | + <property name="expand">False</property> |
540 | + <property name="fill">True</property> |
541 | + <property name="position">1</property> |
542 | + </packing> |
543 | + </child> |
544 | + </object> |
545 | + <object class="GtkVBox" id="processing_vbox"> |
546 | + <property name="visible">True</property> |
547 | + <property name="can_focus">False</property> |
548 | + <property name="spacing">10</property> |
549 | + <child> |
550 | + <placeholder/> |
551 | + </child> |
552 | + </object> |
553 | + <object class="GtkVBox" id="request_password_token_vbox"> |
554 | + <property name="visible">True</property> |
555 | + <property name="can_focus">False</property> |
556 | + <property name="spacing">10</property> |
557 | + <child> |
558 | + <object class="GtkAlignment" id="alignment2"> |
559 | + <property name="visible">True</property> |
560 | + <property name="can_focus">False</property> |
561 | + <property name="xscale">0</property> |
562 | + <property name="yscale">0</property> |
563 | + <child> |
564 | + <object class="GtkVBox" id="request_password_token_details_vbox"> |
565 | + <property name="visible">True</property> |
566 | + <property name="can_focus">False</property> |
567 | + <property name="spacing">5</property> |
568 | + <child> |
569 | + <placeholder/> |
570 | + </child> |
571 | + </object> |
572 | + </child> |
573 | + </object> |
574 | + <packing> |
575 | + <property name="expand">True</property> |
576 | + <property name="fill">True</property> |
577 | + <property name="position">0</property> |
578 | + </packing> |
579 | + </child> |
580 | + <child> |
581 | + <object class="GtkHButtonBox" id="hbuttonbox7"> |
582 | + <property name="visible">True</property> |
583 | + <property name="can_focus">False</property> |
584 | + <property name="spacing">5</property> |
585 | + <property name="layout_style">end</property> |
586 | + <child> |
587 | + <object class="GtkButton" id="request_password_token_cancel_button"> |
588 | + <property name="label">gtk-cancel</property> |
589 | + <property name="use_action_appearance">False</property> |
590 | + <property name="visible">True</property> |
591 | + <property name="can_focus">True</property> |
592 | + <property name="receives_default">True</property> |
593 | + <property name="use_action_appearance">False</property> |
594 | + <property name="use_stock">True</property> |
595 | + </object> |
596 | + <packing> |
597 | + <property name="expand">False</property> |
598 | + <property name="fill">False</property> |
599 | + <property name="position">0</property> |
600 | + </packing> |
601 | + </child> |
602 | + <child> |
603 | + <object class="GtkButton" id="request_password_token_back_button"> |
604 | + <property name="label">gtk-go-back</property> |
605 | + <property name="use_action_appearance">False</property> |
606 | + <property name="visible">True</property> |
607 | + <property name="can_focus">True</property> |
608 | + <property name="receives_default">True</property> |
609 | + <property name="use_action_appearance">False</property> |
610 | + <property name="use_stock">True</property> |
611 | + <signal name="clicked" handler="on_request_password_token_back_button_clicked" swapped="no"/> |
612 | + </object> |
613 | + <packing> |
614 | + <property name="expand">False</property> |
615 | + <property name="fill">False</property> |
616 | + <property name="position">1</property> |
617 | + </packing> |
618 | + </child> |
619 | + <child> |
620 | + <object class="GtkButton" id="request_password_token_ok_button"> |
621 | + <property name="label">gtk-ok</property> |
622 | + <property name="use_action_appearance">False</property> |
623 | + <property name="visible">True</property> |
624 | + <property name="can_focus">True</property> |
625 | + <property name="receives_default">True</property> |
626 | + <property name="use_action_appearance">False</property> |
627 | + <property name="use_stock">True</property> |
628 | + <signal name="clicked" handler="on_request_password_token_ok_button_clicked" swapped="no"/> |
629 | + </object> |
630 | + <packing> |
631 | + <property name="expand">False</property> |
632 | + <property name="fill">False</property> |
633 | + <property name="position">2</property> |
634 | + </packing> |
635 | + </child> |
636 | + </object> |
637 | + <packing> |
638 | + <property name="expand">False</property> |
639 | + <property name="fill">True</property> |
640 | + <property name="position">1</property> |
641 | + </packing> |
642 | + </child> |
643 | + </object> |
644 | + <object class="GtkVBox" id="set_new_password_vbox"> |
645 | + <property name="visible">True</property> |
646 | + <property name="can_focus">False</property> |
647 | + <property name="spacing">10</property> |
648 | + <child> |
649 | + <object class="GtkVBox" id="vbox2"> |
650 | + <property name="visible">True</property> |
651 | + <property name="can_focus">False</property> |
652 | + <child> |
653 | + <object class="GtkLabel" id="reset_password_help_label"> |
654 | + <property name="visible">True</property> |
655 | + <property name="can_focus">False</property> |
656 | + <property name="label">label</property> |
657 | + <property name="wrap">True</property> |
658 | + </object> |
659 | + <packing> |
660 | + <property name="expand">False</property> |
661 | + <property name="fill">True</property> |
662 | + <property name="position">0</property> |
663 | + </packing> |
664 | + </child> |
665 | + <child> |
666 | + <object class="GtkAlignment" id="alignment1"> |
667 | + <property name="visible">True</property> |
668 | + <property name="can_focus">False</property> |
669 | + <property name="xscale">0</property> |
670 | + <property name="yscale">0</property> |
671 | + <child> |
672 | + <object class="GtkVBox" id="set_new_password_details_vbox"> |
673 | + <property name="visible">True</property> |
674 | + <property name="can_focus">False</property> |
675 | + <property name="spacing">5</property> |
676 | + <child> |
677 | + <placeholder/> |
678 | + </child> |
679 | + <child> |
680 | + <placeholder/> |
681 | + </child> |
682 | + <child> |
683 | + <placeholder/> |
684 | + </child> |
685 | + </object> |
686 | + </child> |
687 | + </object> |
688 | + <packing> |
689 | + <property name="expand">True</property> |
690 | + <property name="fill">True</property> |
691 | + <property name="position">1</property> |
692 | + </packing> |
693 | + </child> |
694 | + </object> |
695 | + <packing> |
696 | + <property name="expand">True</property> |
697 | + <property name="fill">True</property> |
698 | + <property name="position">0</property> |
699 | + </packing> |
700 | + </child> |
701 | + <child> |
702 | + <object class="GtkHButtonBox" id="hbuttonbox6"> |
703 | + <property name="visible">True</property> |
704 | + <property name="can_focus">False</property> |
705 | + <property name="spacing">5</property> |
706 | + <property name="layout_style">end</property> |
707 | + <child> |
708 | + <object class="GtkButton" id="set_new_password_cancel_button"> |
709 | + <property name="label">gtk-cancel</property> |
710 | + <property name="use_action_appearance">False</property> |
711 | + <property name="visible">True</property> |
712 | + <property name="can_focus">True</property> |
713 | + <property name="receives_default">True</property> |
714 | + <property name="use_action_appearance">False</property> |
715 | + <property name="use_stock">True</property> |
716 | + </object> |
717 | + <packing> |
718 | + <property name="expand">False</property> |
719 | + <property name="fill">False</property> |
720 | + <property name="position">0</property> |
721 | + </packing> |
722 | + </child> |
723 | + <child> |
724 | + <object class="GtkButton" id="set_new_password_ok_button"> |
725 | + <property name="label">gtk-ok</property> |
726 | + <property name="use_action_appearance">False</property> |
727 | + <property name="visible">True</property> |
728 | + <property name="can_focus">True</property> |
729 | + <property name="receives_default">True</property> |
730 | + <property name="use_action_appearance">False</property> |
731 | + <property name="use_stock">True</property> |
732 | + <signal name="clicked" handler="on_set_new_password_ok_button_clicked" swapped="no"/> |
733 | + </object> |
734 | + <packing> |
735 | + <property name="expand">False</property> |
736 | + <property name="fill">False</property> |
737 | + <property name="position">1</property> |
738 | + </packing> |
739 | + </child> |
740 | + </object> |
741 | + <packing> |
742 | + <property name="expand">False</property> |
743 | + <property name="fill">True</property> |
744 | + <property name="position">1</property> |
745 | + </packing> |
746 | + </child> |
747 | + </object> |
748 | + <object class="GtkVBox" id="tc_browser_vbox"> |
749 | + <property name="visible">True</property> |
750 | + <property name="can_focus">False</property> |
751 | + <signal name="hide" handler="on_tc_browser_vbox_hide" swapped="no"/> |
752 | + <child> |
753 | + <object class="GtkScrolledWindow" id="tc_browser_window"> |
754 | + <property name="visible">True</property> |
755 | + <property name="can_focus">True</property> |
756 | + <property name="border_width">10</property> |
757 | + <property name="hscrollbar_policy">never</property> |
758 | + <property name="shadow_type">in</property> |
759 | + <child> |
760 | + <placeholder/> |
761 | + </child> |
762 | + </object> |
763 | + <packing> |
764 | + <property name="expand">True</property> |
765 | + <property name="fill">True</property> |
766 | + <property name="position">0</property> |
767 | + </packing> |
768 | + </child> |
769 | + <child> |
770 | + <object class="GtkHButtonBox" id="hbuttonbox4"> |
771 | + <property name="visible">True</property> |
772 | + <property name="can_focus">False</property> |
773 | + <property name="layout_style">end</property> |
774 | + <child> |
775 | + <object class="GtkButton" id="tc_back_button"> |
776 | + <property name="label">gtk-go-back</property> |
777 | + <property name="use_action_appearance">False</property> |
778 | + <property name="visible">True</property> |
779 | + <property name="can_focus">True</property> |
780 | + <property name="receives_default">True</property> |
781 | + <property name="use_action_appearance">False</property> |
782 | + <property name="use_stock">True</property> |
783 | + <signal name="clicked" handler="on_tc_back_button_clicked" swapped="no"/> |
784 | + </object> |
785 | + <packing> |
786 | + <property name="expand">False</property> |
787 | + <property name="fill">False</property> |
788 | + <property name="position">0</property> |
789 | + </packing> |
790 | + </child> |
791 | + </object> |
792 | + <packing> |
793 | + <property name="expand">False</property> |
794 | + <property name="fill">True</property> |
795 | + <property name="position">1</property> |
796 | + </packing> |
797 | + </child> |
798 | + </object> |
799 | + <object class="GtkVBox" id="verify_email_vbox"> |
800 | + <property name="visible">True</property> |
801 | + <property name="can_focus">False</property> |
802 | + <property name="spacing">10</property> |
803 | + <child> |
804 | + <object class="GtkAlignment" id="alignment4"> |
805 | + <property name="visible">True</property> |
806 | + <property name="can_focus">False</property> |
807 | + <property name="xscale">0</property> |
808 | + <property name="yscale">0</property> |
809 | + <child> |
810 | + <object class="GtkVBox" id="verify_email_details_vbox"> |
811 | + <property name="visible">True</property> |
812 | + <property name="can_focus">False</property> |
813 | + <child> |
814 | + <placeholder/> |
815 | + </child> |
816 | + </object> |
817 | + </child> |
818 | + </object> |
819 | + <packing> |
820 | + <property name="expand">True</property> |
821 | + <property name="fill">True</property> |
822 | + <property name="position">0</property> |
823 | + </packing> |
824 | + </child> |
825 | + <child> |
826 | + <object class="GtkHButtonBox" id="hbuttonbox2"> |
827 | + <property name="visible">True</property> |
828 | + <property name="can_focus">False</property> |
829 | + <property name="spacing">5</property> |
830 | + <property name="layout_style">end</property> |
831 | + <child> |
832 | + <object class="GtkButton" id="verify_token_button"> |
833 | + <property name="label">gtk-ok</property> |
834 | + <property name="use_action_appearance">False</property> |
835 | + <property name="visible">True</property> |
836 | + <property name="can_focus">True</property> |
837 | + <property name="receives_default">True</property> |
838 | + <property name="use_action_appearance">False</property> |
839 | + <property name="use_stock">True</property> |
840 | + <signal name="clicked" handler="on_verify_token_button_clicked" swapped="no"/> |
841 | + </object> |
842 | + <packing> |
843 | + <property name="expand">False</property> |
844 | + <property name="fill">False</property> |
845 | + <property name="position">0</property> |
846 | + </packing> |
847 | + </child> |
848 | + </object> |
849 | + <packing> |
850 | + <property name="expand">False</property> |
851 | + <property name="fill">True</property> |
852 | + <property name="position">1</property> |
853 | + </packing> |
854 | + </child> |
855 | + </object> |
856 | + <object class="GtkWindow" id="window"> |
857 | + <property name="can_focus">False</property> |
858 | + <property name="border_width">10</property> |
859 | + <property name="window_position">center</property> |
860 | + <signal name="delete-event" handler="on_close_clicked" swapped="no"/> |
861 | + <child> |
862 | + <object class="GtkVBox" id="window_vbox"> |
863 | + <property name="visible">True</property> |
864 | + <property name="can_focus">False</property> |
865 | + <property name="spacing">5</property> |
866 | + <child> |
867 | + <object class="GtkLabel" id="header_label"> |
868 | + <property name="visible">True</property> |
869 | + <property name="can_focus">False</property> |
870 | + <property name="xalign">0</property> |
871 | + <property name="label" translatable="yes">Header Label </property> |
872 | + <property name="wrap">True</property> |
873 | + </object> |
874 | + <packing> |
875 | + <property name="expand">False</property> |
876 | + <property name="fill">True</property> |
877 | + <property name="padding">5</property> |
878 | + <property name="position">0</property> |
879 | + </packing> |
880 | + </child> |
881 | + <child> |
882 | + <object class="GtkLabel" id="help_label"> |
883 | + <property name="visible">True</property> |
884 | + <property name="can_focus">False</property> |
885 | + <property name="xalign">0</property> |
886 | + <property name="label" translatable="yes">help label</property> |
887 | + <property name="wrap">True</property> |
888 | + </object> |
889 | + <packing> |
890 | + <property name="expand">False</property> |
891 | + <property name="fill">True</property> |
892 | + <property name="position">1</property> |
893 | + </packing> |
894 | + </child> |
895 | + <child> |
896 | + <object class="GtkLabel" id="warning_label"> |
897 | + <property name="visible">True</property> |
898 | + <property name="can_focus">False</property> |
899 | + <property name="xalign">0</property> |
900 | + <property name="label" translatable="yes">warning label</property> |
901 | + <property name="wrap">True</property> |
902 | + </object> |
903 | + <packing> |
904 | + <property name="expand">False</property> |
905 | + <property name="fill">True</property> |
906 | + <property name="position">2</property> |
907 | + </packing> |
908 | + </child> |
909 | + <child> |
910 | + <object class="GtkNotebook" id="content"> |
911 | + <property name="visible">True</property> |
912 | + <property name="can_focus">True</property> |
913 | + <property name="show_tabs">False</property> |
914 | + <property name="show_border">False</property> |
915 | + </object> |
916 | + <packing> |
917 | + <property name="expand">True</property> |
918 | + <property name="fill">True</property> |
919 | + <property name="position">3</property> |
920 | + </packing> |
921 | + </child> |
922 | + </object> |
923 | + </child> |
924 | + </object> |
925 | +</interface> |
926 | |
927 | === modified file 'run-tests.sh' |
928 | --- run-tests.sh 2012-06-20 09:32:04 +0000 |
929 | +++ run-tests.sh 2012-06-28 15:39:18 +0000 |
930 | @@ -1,4 +1,4 @@ |
931 | -#!/bin/sh |
932 | +#!/bin/bash |
933 | |
934 | set -e |
935 | |
936 | @@ -83,7 +83,10 @@ |
937 | done |
938 | } |
939 | |
940 | -if [ $# -gt 0 ]; then |
941 | +if [ "$1" == "--sso-gtk" ]; then |
942 | + # Run the SSO GTK+ suite |
943 | + $PYTHON discover -s softwarecenter/sso/ |
944 | +elif [ $# -gt 0 ]; then |
945 | # run the requested tests if arguments were given, |
946 | # otherwise run the whole suite |
947 | # example of custom params (discover all the tests under the tests/gtk3 dir): |
948 | |
949 | === modified file 'setup.py' |
950 | --- setup.py 2012-03-15 22:36:31 +0000 |
951 | +++ setup.py 2012-06-28 15:39:18 +0000 |
952 | @@ -83,77 +83,79 @@ |
953 | call(["po4a", "po/help/po4a.conf"]) |
954 | |
955 | # real setup |
956 | -setup(name="software-center", version=VERSION, |
957 | - scripts=["software-center", |
958 | - # gtk3 |
959 | - "utils/submit_review_gtk3.py", |
960 | - "utils/report_review_gtk3.py", |
961 | - "utils/submit_usefulness_gtk3.py", |
962 | - "utils/delete_review_gtk3.py", |
963 | - "utils/modify_review_gtk3.py", |
964 | - # db helpers |
965 | - "utils/update-software-center", |
966 | - "utils/update-software-center-channels", |
967 | - "utils/update-software-center-agent", |
968 | - # generic helpers |
969 | - "utils/expunge-cache.py", |
970 | - ] + glob.glob("utils/piston-helpers/*.py"), |
971 | - packages=['softwarecenter', |
972 | - 'softwarecenter.backend', |
973 | - 'softwarecenter.backend.installbackend_impl', |
974 | - 'softwarecenter.backend.channel_impl', |
975 | - 'softwarecenter.backend.oneconfhandler', |
976 | - 'softwarecenter.backend.piston', |
977 | - 'softwarecenter.backend.reviews', |
978 | - 'softwarecenter.db', |
979 | - 'softwarecenter.db.pkginfo_impl', |
980 | - 'softwarecenter.db.history_impl', |
981 | - 'softwarecenter.distro', |
982 | - 'softwarecenter.ui', |
983 | - 'softwarecenter.ui.gtk3', |
984 | - 'softwarecenter.ui.gtk3.dialogs', |
985 | - 'softwarecenter.ui.gtk3.models', |
986 | - 'softwarecenter.ui.gtk3.panes', |
987 | - 'softwarecenter.ui.gtk3.session', |
988 | - 'softwarecenter.ui.gtk3.views', |
989 | - 'softwarecenter.ui.gtk3.widgets', |
990 | - 'softwarecenter.ui.qml', |
991 | - ], |
992 | - data_files=[ |
993 | - # gtk3 |
994 | - ('share/software-center/ui/gtk3/', |
995 | - glob.glob("data/ui/gtk3/*.ui")), |
996 | - ('share/software-center/ui/gtk3/css/', |
997 | - glob.glob("data/ui/gtk3/css/*.css")), |
998 | - ('share/software-center/ui/gtk3/art/', |
999 | - glob.glob("data/ui/gtk3/art/*.png")), |
1000 | - ('share/software-center/ui/gtk3/art/icons', |
1001 | - glob.glob("data/ui/gtk3/art/icons/*.png")), |
1002 | - ('share/software-center/default_banner', |
1003 | - glob.glob("data/default_banner/*")), |
1004 | - # dbus |
1005 | - ('../etc/dbus-1/system.d/', |
1006 | - ["data/com.ubuntu.SoftwareCenter.conf"]), |
1007 | - # images |
1008 | - ('share/software-center/images/', |
1009 | - glob.glob("data/images/*.png") + |
1010 | - glob.glob("data/images/*.gif")), |
1011 | - ('share/software-center/icons/', |
1012 | - glob.glob("data/emblems/*.png")), |
1013 | - # xapian |
1014 | - ('share/apt-xapian-index/plugins', |
1015 | - glob.glob("apt-xapian-index-plugin/*.py")), |
1016 | - # apport |
1017 | - ('share/apport/package-hooks/', |
1018 | - ['debian/source_software-center.py']), |
1019 | - # extra software channels (can be distro specific) |
1020 | - ('/usr/share/app-install/channels/', |
1021 | - glob.glob("data/channels/%s/*" % DISTRO)), |
1022 | - ], |
1023 | - cmdclass={"build": build_extra.build_extra, |
1024 | - "build_i18n": build_i18n.build_i18n, |
1025 | - "build_help": build_help.build_help, |
1026 | - "build_icons": build_icons.build_icons, |
1027 | - "lint": PocketLint, |
1028 | - }, |
1029 | - ) |
1030 | +setup( |
1031 | + name="software-center", |
1032 | + version=VERSION, |
1033 | + scripts=[ |
1034 | + "software-center", |
1035 | + # gtk3 |
1036 | + "utils/submit_review_gtk3.py", |
1037 | + "utils/report_review_gtk3.py", |
1038 | + "utils/submit_usefulness_gtk3.py", |
1039 | + "utils/delete_review_gtk3.py", |
1040 | + "utils/modify_review_gtk3.py", |
1041 | + # db helpers |
1042 | + "utils/update-software-center", |
1043 | + "utils/update-software-center-channels", |
1044 | + "utils/update-software-center-agent", |
1045 | + # generic helpers |
1046 | + "utils/expunge-cache.py", |
1047 | + ] + glob.glob("utils/piston-helpers/*.py"), |
1048 | + packages=[ |
1049 | + 'softwarecenter', |
1050 | + 'softwarecenter.backend', |
1051 | + 'softwarecenter.backend.installbackend_impl', |
1052 | + 'softwarecenter.backend.channel_impl', |
1053 | + 'softwarecenter.backend.oneconfhandler', |
1054 | + 'softwarecenter.backend.piston', |
1055 | + 'softwarecenter.backend.reviews', |
1056 | + 'softwarecenter.db', |
1057 | + 'softwarecenter.db.pkginfo_impl', |
1058 | + 'softwarecenter.db.history_impl', |
1059 | + 'softwarecenter.distro', |
1060 | + 'softwarecenter.sso', |
1061 | + 'softwarecenter.ui', |
1062 | + 'softwarecenter.ui.gtk3', |
1063 | + 'softwarecenter.ui.gtk3.dialogs', |
1064 | + 'softwarecenter.ui.gtk3.models', |
1065 | + 'softwarecenter.ui.gtk3.panes', |
1066 | + 'softwarecenter.ui.gtk3.session', |
1067 | + 'softwarecenter.ui.gtk3.views', |
1068 | + 'softwarecenter.ui.gtk3.widgets', |
1069 | + 'softwarecenter.ui.qml', |
1070 | + ], |
1071 | + data_files=[ |
1072 | + # gtk3 |
1073 | + ('share/software-center/ui/gtk3/', glob.glob("data/ui/gtk3/*.ui")), |
1074 | + ('share/software-center/ui/gtk3/css/', |
1075 | + glob.glob("data/ui/gtk3/css/*.css")), |
1076 | + ('share/software-center/ui/gtk3/art/', |
1077 | + glob.glob("data/ui/gtk3/art/*.png")), |
1078 | + ('share/software-center/ui/gtk3/art/icons', |
1079 | + glob.glob("data/ui/gtk3/art/icons/*.png")), |
1080 | + ('share/software-center/default_banner', |
1081 | + glob.glob("data/default_banner/*")), |
1082 | + # dbus |
1083 | + ('../etc/dbus-1/system.d/', ["data/com.ubuntu.SoftwareCenter.conf"]), |
1084 | + # images |
1085 | + ('share/software-center/images/', |
1086 | + glob.glob("data/images/*.png") + glob.glob("data/images/*.gif")), |
1087 | + ('share/software-center/icons/', glob.glob("data/emblems/*.png")), |
1088 | + # xapian |
1089 | + ('share/apt-xapian-index/plugins', |
1090 | + glob.glob("apt-xapian-index-plugin/*.py")), |
1091 | + # apport |
1092 | + ('share/apport/package-hooks/', ['debian/source_software-center.py']), |
1093 | + # extra software channels (can be distro specific) |
1094 | + ('share/app-install/channels/', |
1095 | + glob.glob("data/channels/%s/*" % DISTRO)), |
1096 | + ('lib/ubuntu-sso-client', ['software-center-sso-gtk']), |
1097 | + ], |
1098 | + cmdclass={ |
1099 | + "build": build_extra.build_extra, |
1100 | + "build_i18n": build_i18n.build_i18n, |
1101 | + "build_help": build_help.build_help, |
1102 | + "build_icons": build_icons.build_icons, |
1103 | + "lint": PocketLint, |
1104 | + }, |
1105 | +) |
1106 | |
1107 | === added file 'software-center-sso-gtk' |
1108 | --- software-center-sso-gtk 1970-01-01 00:00:00 +0000 |
1109 | +++ software-center-sso-gtk 2012-06-28 15:39:18 +0000 |
1110 | @@ -0,0 +1,33 @@ |
1111 | +#!/usr/bin/env python |
1112 | +# -*- coding: utf-8 -*- |
1113 | +# |
1114 | +# Copyright 2012 Canonical Ltd. |
1115 | +# |
1116 | +# This program is free software: you can redistribute it and/or modify it |
1117 | +# under the terms of the GNU General Public License version 3, as published |
1118 | +# by the Free Software Foundation. |
1119 | +# |
1120 | +# This program is distributed in the hope that it will be useful, but |
1121 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1122 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1123 | +# PURPOSE. See the GNU General Public License for more details. |
1124 | +# |
1125 | +# You should have received a copy of the GNU General Public License along |
1126 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
1127 | +# |
1128 | + |
1129 | +"""Start the SSO GTK+ UI.""" |
1130 | + |
1131 | +# Invalid name "software-center-sso-gtk", pylint: disable=C0103 |
1132 | +# Access to a protected member, pylint: disable=W0212 |
1133 | + |
1134 | +from softwarecenter.sso import gui |
1135 | +from ubuntu_sso.utils.ui import parse_args |
1136 | + |
1137 | +from dbus.mainloop.glib import DBusGMainLoop |
1138 | +DBusGMainLoop(set_as_default=True) |
1139 | + |
1140 | + |
1141 | +if __name__ == "__main__": |
1142 | + args = parse_args() |
1143 | + gui.run(**dict(args._get_kwargs())) |
1144 | |
1145 | === added directory 'softwarecenter/sso' |
1146 | === added file 'softwarecenter/sso/__init__.py' |
1147 | --- softwarecenter/sso/__init__.py 1970-01-01 00:00:00 +0000 |
1148 | +++ softwarecenter/sso/__init__.py 2012-06-28 15:39:18 +0000 |
1149 | @@ -0,0 +1,18 @@ |
1150 | +# -*- coding: utf-8 -*- |
1151 | +# |
1152 | +# Copyright 2009-2012 Canonical Ltd. |
1153 | +# |
1154 | +# This program is free software: you can redistribute it and/or modify it |
1155 | +# under the terms of the GNU General Public License version 3, as published |
1156 | +# by the Free Software Foundation. |
1157 | +# |
1158 | +# This program is distributed in the hope that it will be useful, but |
1159 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1160 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1161 | +# PURPOSE. See the GNU General Public License for more details. |
1162 | +# |
1163 | +# You should have received a copy of the GNU General Public License along |
1164 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
1165 | +# |
1166 | + |
1167 | +"""Ubuntu Single Sign On GTK+ graphical interface.""" |
1168 | |
1169 | === added file 'softwarecenter/sso/gui.py' |
1170 | --- softwarecenter/sso/gui.py 1970-01-01 00:00:00 +0000 |
1171 | +++ softwarecenter/sso/gui.py 2012-06-28 15:39:18 +0000 |
1172 | @@ -0,0 +1,1168 @@ |
1173 | +# -*- coding: utf-8 -*- |
1174 | +# |
1175 | +# Copyright 2010-2012 Canonical Ltd. |
1176 | +# |
1177 | +# This program is free software: you can redistribute it and/or modify it |
1178 | +# under the terms of the GNU General Public License version 3, as published |
1179 | +# by the Free Software Foundation. |
1180 | +# |
1181 | +# This program is distributed in the hope that it will be useful, but |
1182 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
1183 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1184 | +# PURPOSE. See the GNU General Public License for more details. |
1185 | +# |
1186 | +# You should have received a copy of the GNU General Public License along |
1187 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
1188 | +# |
1189 | + |
1190 | +"""The Ubuntu Single Sign On GTK+ graphical user interface.""" |
1191 | + |
1192 | +import logging |
1193 | +import os |
1194 | +import sys |
1195 | +import tempfile |
1196 | +import webbrowser |
1197 | + |
1198 | +from functools import wraps, partial |
1199 | + |
1200 | +import dbus |
1201 | + |
1202 | +# pylint: disable=E0611,F0401 |
1203 | +from gi.repository import Gdk, Gtk |
1204 | +from gi.repository.GdkX11 import X11Window |
1205 | +# pylint: enable=E0611,F0401 |
1206 | + |
1207 | +from ubuntu_sso import ( |
1208 | + DBUS_BUS_NAME, |
1209 | + DBUS_ACCOUNT_PATH, |
1210 | + DBUS_IFACE_USER_NAME, |
1211 | + NO_OP, |
1212 | + USER_CANCELLATION, |
1213 | + USER_SUCCESS, |
1214 | +) |
1215 | +from ubuntu_sso.logger import setup_gui_logging |
1216 | +from ubuntu_sso.utils import ui as ui_strings |
1217 | +from ubuntu_sso.utils.ui import ( |
1218 | + CAPTCHA_LOAD_ERROR, |
1219 | + CAPTCHA_RELOAD_TOOLTIP, |
1220 | + CONNECT_HELP_LABEL, |
1221 | + EMAIL_MISMATCH, |
1222 | + EMAIL_INVALID, |
1223 | + ERROR, |
1224 | + FIELD_REQUIRED, |
1225 | + FORGOTTEN_PASSWORD_BUTTON, |
1226 | + GENERIC_BACKEND_ERROR, |
1227 | + is_min_required_password, |
1228 | + is_correct_email, |
1229 | + JOIN_HEADER_LABEL, |
1230 | + LOADING, |
1231 | + LOGIN_BUTTON_LABEL, |
1232 | + LOGIN_HEADER_LABEL, |
1233 | + NEXT, |
1234 | + ONE_MOMENT_PLEASE, |
1235 | + PASSWORD_CHANGED, |
1236 | + PASSWORD_HELP, |
1237 | + PASSWORD_MISMATCH, |
1238 | + PASSWORD_TOO_WEAK, |
1239 | + REQUEST_PASSWORD_TOKEN_LABEL, |
1240 | + RESET_PASSWORD, |
1241 | + SET_NEW_PASSWORD_LABEL, |
1242 | + SUCCESS, |
1243 | + TC_BUTTON, |
1244 | + TC_NOT_ACCEPTED, |
1245 | + VERIFY_EMAIL_LABEL, |
1246 | + YES_TO_TC, |
1247 | + YES_TO_UPDATES, |
1248 | +) |
1249 | + |
1250 | +# Instance of 'UbuntuSSOClientGUI' has no 'yyy' member |
1251 | +# pylint: disable=E1101 |
1252 | + |
1253 | + |
1254 | +logger = setup_gui_logging('ubuntu_sso.gui.gtk') |
1255 | + |
1256 | + |
1257 | +# pylint: disable=C0103 |
1258 | +def parse_color(color): |
1259 | + """Parse a string color into Gdk.Color.""" |
1260 | + c = Gdk.RGBA() |
1261 | + result = c.parse(color) |
1262 | + if not result: |
1263 | + logger.warning('Could not parse color %r.', color) |
1264 | + return c |
1265 | +# pylint: enable=C0103 |
1266 | + |
1267 | +DEFAULT_WIDTH = 30 |
1268 | +# To be replaced by values from the theme (LP: #616526) |
1269 | +HELP_TEXT_COLOR = parse_color("#bfbfbf") |
1270 | +WARNING_TEXT_COLOR = parse_color("red") |
1271 | +LARGE_MARKUP = u'<span size="x-large">%s</span>' |
1272 | + |
1273 | + |
1274 | +# SSL properties and certs location |
1275 | +STRICT_SSL_PROP = 'ssl-strict' |
1276 | +CERTS_FILE_PROP = 'ssl-ca-file' |
1277 | +CA_CERT_FILE = '/etc/ssl/certs/ca-certificates.crt' |
1278 | + |
1279 | + |
1280 | +def log_call(f): |
1281 | + """Decorator to log call funtions.""" |
1282 | + |
1283 | + @wraps(f) |
1284 | + def inner(*args, **kwargs): |
1285 | + """Execute 'f' logging the call as INFO.""" |
1286 | + logger.info('%s: args %r, kwargs %r.', f.__name__, args, kwargs) |
1287 | + return f(*args, **kwargs) |
1288 | + |
1289 | + return inner |
1290 | + |
1291 | + |
1292 | +def get_sso_client(): |
1293 | + bus = dbus.SessionBus() |
1294 | + obj = bus.get_object(bus_name=DBUS_BUS_NAME, |
1295 | + object_path=DBUS_ACCOUNT_PATH, |
1296 | + follow_name_owner_changes=True) |
1297 | + result = dbus.Interface(obj, dbus_interface=DBUS_IFACE_USER_NAME) |
1298 | + result.disconnect_from_signal = lambda _, sig: sig.remove() |
1299 | + return result |
1300 | + |
1301 | + |
1302 | +def get_data_file(*args): |
1303 | + result = os.path.abspath(os.path.join(os.path.dirname(__file__), |
1304 | + '..', '..', 'data')) |
1305 | + if not os.path.exists(result): |
1306 | + import softwarecenter.paths |
1307 | + result = softwarecenter.paths.datadir |
1308 | + |
1309 | + result = os.path.join(result, 'ui', 'sso', *args) |
1310 | + logger.info('Using data dir: %r', result) |
1311 | + return result |
1312 | + |
1313 | + |
1314 | +class LabeledEntry(Gtk.Entry): |
1315 | + """An entry that displays the label within itself ina grey color.""" |
1316 | + |
1317 | + # Use of super on an old style class |
1318 | + # pylint: disable=E1002 |
1319 | + |
1320 | + def __init__(self, label, is_password=False, *args, **kwargs): |
1321 | + self.label = label |
1322 | + self.is_password = is_password |
1323 | + self.warning = None |
1324 | + |
1325 | + super(LabeledEntry, self).__init__(*args, **kwargs) |
1326 | + |
1327 | + self.set_width_chars(DEFAULT_WIDTH) |
1328 | + self._set_label(self, None) |
1329 | + self.set_tooltip_text(self.label) |
1330 | + self.connect('focus-in-event', self._clear_text) |
1331 | + self.connect('focus-out-event', self._set_label) |
1332 | + self.clear_warning() |
1333 | + self.show() |
1334 | + |
1335 | + def _clear_text(self, *args, **kwargs): |
1336 | + """Clear text and restore text color.""" |
1337 | + self.set_text(self.get_text()) |
1338 | + |
1339 | + # restore to theme's default |
1340 | + self.override_color(Gtk.StateFlags.NORMAL, None) |
1341 | + |
1342 | + if self.is_password: |
1343 | + self.set_visibility(False) |
1344 | + |
1345 | + return False # propagate the event further |
1346 | + |
1347 | + def _set_label(self, *args, **kwargs): |
1348 | + """Set the proper label and proper coloring.""" |
1349 | + if self.get_text(): |
1350 | + return |
1351 | + |
1352 | + self.set_text(self.label) |
1353 | + self.override_color(Gtk.StateFlags.NORMAL, HELP_TEXT_COLOR) |
1354 | + |
1355 | + if self.is_password: |
1356 | + self.set_visibility(True) |
1357 | + |
1358 | + return False # propagate the event further |
1359 | + |
1360 | + def get_text(self): |
1361 | + """Get text only if it's not the label nor empty.""" |
1362 | + result = super(LabeledEntry, self).get_text().decode('utf8') |
1363 | + if result == self.label or result.isspace(): |
1364 | + result = u'' |
1365 | + return result |
1366 | + |
1367 | + def set_warning(self, warning_msg): |
1368 | + """Display warning as secondary icon, set 'warning_msg' as tooltip.""" |
1369 | + self.warning = warning_msg |
1370 | + self.set_property('secondary-icon-stock', Gtk.STOCK_DIALOG_WARNING) |
1371 | + self.set_property('secondary-icon-sensitive', True) |
1372 | + self.set_property('secondary-icon-activatable', False) |
1373 | + self.set_property('secondary-icon-tooltip-text', warning_msg) |
1374 | + |
1375 | + def clear_warning(self): |
1376 | + """Remove any warning.""" |
1377 | + self.warning = None |
1378 | + self.set_property('secondary-icon-stock', None) |
1379 | + self.set_property('secondary-icon-sensitive', False) |
1380 | + self.set_property('secondary-icon-activatable', False) |
1381 | + self.set_property('secondary-icon-tooltip-text', None) |
1382 | + |
1383 | + |
1384 | +class UbuntuSSOClientGUI(object): |
1385 | + """Ubuntu single sign-on GUI.""" |
1386 | + |
1387 | + def __init__(self, app_name, **kwargs): |
1388 | + """Create the GUI and initialize widgets.""" |
1389 | + logger.debug('UbuntuSSOClientGUI: app_name %r, kwargs %r.', |
1390 | + app_name, kwargs) |
1391 | + |
1392 | + self._captcha_filename = tempfile.mktemp() |
1393 | + self._captcha_id = None |
1394 | + self._signals_receivers = {} |
1395 | + self._done = False # whether the whole process was completed or not |
1396 | + |
1397 | + self.app_name = app_name |
1398 | + self.app_label = u'<b>%s</b>' % self.app_name |
1399 | + self.ping_url = kwargs.get('ping_url', u'') |
1400 | + self.tc_url = kwargs.get('tc_url', u'') |
1401 | + self.help_text = kwargs.get('help_text', u'') |
1402 | + self.login_only = kwargs.get('login_only', False) |
1403 | + window_id = kwargs.get('window_id', 0) |
1404 | + self.close_callback = kwargs.get('close_callback', NO_OP) |
1405 | + self.backend = None |
1406 | + self.user_email = None |
1407 | + self.user_password = None |
1408 | + |
1409 | + ui_filename = get_data_file('sso.ui') |
1410 | + builder = Gtk.Builder() |
1411 | + builder.add_from_file(ui_filename) |
1412 | + builder.connect_signals(self) |
1413 | + |
1414 | + self.widgets = [] |
1415 | + self.warnings = [] |
1416 | + self.cancels = [] |
1417 | + for obj in builder.get_objects(): |
1418 | + name = getattr(obj, 'name', None) |
1419 | + if name is None and isinstance(obj, Gtk.Buildable): |
1420 | + # work around bug lp:507739 |
1421 | + name = Gtk.Buildable.get_name(obj) |
1422 | + if name is None: |
1423 | + logging.warn("%s has no name (??)", obj) |
1424 | + else: |
1425 | + self.widgets.append(name) |
1426 | + setattr(self, name, obj) |
1427 | + if 'warning' in name: |
1428 | + self.warnings.append(obj) |
1429 | + obj.set_text('') |
1430 | + if 'cancel_button' in name: |
1431 | + obj.connect('clicked', self.on_close_clicked) |
1432 | + self.cancels.append(obj) |
1433 | + |
1434 | + # Connect the activate-link signal here |
1435 | + # GtkBuilder in GTK 3 seems to not do this |
1436 | + self.login_button.connect('activate-link', self.on_activate_link) |
1437 | + self.forgotten_password_button.connect('activate-link', |
1438 | + self.on_activate_link) |
1439 | + |
1440 | + self.entries = (u'name_entry', u'email1_entry', u'email2_entry', |
1441 | + u'password1_entry', u'password2_entry', |
1442 | + u'captcha_solution_entry', u'email_token_entry', |
1443 | + u'login_email_entry', u'login_password_entry', |
1444 | + u'reset_email_entry', u'reset_code_entry', |
1445 | + u'reset_password1_entry', u'reset_password2_entry') |
1446 | + |
1447 | + for name in self.entries: |
1448 | + label = getattr(ui_strings, name.upper()) |
1449 | + is_password = 'password' in name |
1450 | + entry = LabeledEntry(label=label, is_password=is_password) |
1451 | + entry.set_activates_default(True) |
1452 | + setattr(self, name, entry) |
1453 | + |
1454 | + self.window.set_icon_name('ubuntu-logo') |
1455 | + |
1456 | + self.pages = (self.enter_details_vbox, self.processing_vbox, |
1457 | + self.verify_email_vbox, self.finish_vbox, |
1458 | + self.tc_browser_vbox, self.login_vbox, |
1459 | + self.request_password_token_vbox, |
1460 | + self.set_new_password_vbox) |
1461 | + |
1462 | + self._signals = { |
1463 | + 'CaptchaGenerated': |
1464 | + self._filter_by_app_name(self.on_captcha_generated), |
1465 | + 'CaptchaGenerationError': |
1466 | + self._filter_by_app_name(self.on_captcha_generation_error), |
1467 | + 'UserRegistered': |
1468 | + self._filter_by_app_name(self.on_user_registered), |
1469 | + 'UserRegistrationError': |
1470 | + self._filter_by_app_name(self.on_user_registration_error), |
1471 | + 'EmailValidated': |
1472 | + self._filter_by_app_name(self.on_email_validated), |
1473 | + 'EmailValidationError': |
1474 | + self._filter_by_app_name(self.on_email_validation_error), |
1475 | + 'LoggedIn': |
1476 | + self._filter_by_app_name(self.on_logged_in), |
1477 | + 'LoginError': |
1478 | + self._filter_by_app_name(self.on_login_error), |
1479 | + 'UserNotValidated': |
1480 | + self._filter_by_app_name(self.on_user_not_validated), |
1481 | + 'PasswordResetTokenSent': |
1482 | + self._filter_by_app_name(self.on_password_reset_token_sent), |
1483 | + 'PasswordResetError': |
1484 | + self._filter_by_app_name(self.on_password_reset_error), |
1485 | + 'PasswordChanged': |
1486 | + self._filter_by_app_name(self.on_password_changed), |
1487 | + 'PasswordChangeError': |
1488 | + self._filter_by_app_name(self.on_password_change_error), |
1489 | + } |
1490 | + |
1491 | + if window_id != 0: |
1492 | + # be as robust as possible: |
1493 | + # if the window_id is not "good", set_transient_for will fail |
1494 | + # awfully, and we don't want that: if the window_id is bad we can |
1495 | + # still do everything as a standalone window. Also, |
1496 | + # window_foreign_new may return None breaking set_transient_for. |
1497 | + try: |
1498 | + display = Gdk.Display.get_default() |
1499 | + # this is not working, we need to create a XLib.window |
1500 | + # as a second parameter to foreign_new_for_display |
1501 | + win = X11Window.foreign_new_for_display(display, None) |
1502 | + self.window.realize() |
1503 | + self.window.window.set_transient_for(win) |
1504 | + except: # pylint: disable=W0702 |
1505 | + msg = 'UbuntuSSOClientGUI: failed set_transient_for win id %r' |
1506 | + logger.exception(msg, window_id) |
1507 | + |
1508 | + self.yes_to_updates_checkbutton.hide() |
1509 | + self.start_backend() |
1510 | + |
1511 | + def start_backend(self): |
1512 | + """Start the backend, show the window when ready.""" |
1513 | + self.backend = get_sso_client() |
1514 | + |
1515 | + logger.debug('UbuntuSSOClientGUI: backend created: %r', self.backend) |
1516 | + |
1517 | + self._setup_signals() |
1518 | + self._append_pages() |
1519 | + self.window.show() |
1520 | + |
1521 | + @property |
1522 | + def success_vbox(self): |
1523 | + """The success page.""" |
1524 | + message = SUCCESS % {'app_name': self.app_name} |
1525 | + message = LARGE_MARKUP % message |
1526 | + self.finish_vbox.label.set_markup(message) |
1527 | + return self.finish_vbox |
1528 | + |
1529 | + @property |
1530 | + def error_vbox(self): |
1531 | + """The error page.""" |
1532 | + self.finish_vbox.label.set_markup(LARGE_MARKUP % ERROR) |
1533 | + return self.finish_vbox |
1534 | + |
1535 | + # helpers |
1536 | + |
1537 | + def _filter_by_app_name(self, f): |
1538 | + """Excecute the decorated function only for 'self.app_name'.""" |
1539 | + |
1540 | + @wraps(f) |
1541 | + def inner(app_name, *args, **kwargs): |
1542 | + """Execute 'f' only if 'app_name' matches 'self.app_name'.""" |
1543 | + result = None |
1544 | + if app_name == self.app_name: |
1545 | + result = f(app_name, *args, **kwargs) |
1546 | + else: |
1547 | + logger.info('%s: ignoring call since received app_name ' |
1548 | + '%r (expected %r)', |
1549 | + f.__name__, app_name, self.app_name) |
1550 | + return result |
1551 | + |
1552 | + return inner |
1553 | + |
1554 | + def _setup_signals(self): |
1555 | + """Bind signals to callbacks to be able to test the pages.""" |
1556 | + for signal, method in self._signals.items(): |
1557 | + actual = self._signals_receivers.get(signal) |
1558 | + if actual is not None: |
1559 | + msg = 'Signal %r is already connected with %r.' |
1560 | + logger.warning(msg, signal, actual) |
1561 | + |
1562 | + match = self.backend.connect_to_signal(signal, method) |
1563 | + self._signals_receivers[signal] = match |
1564 | + |
1565 | + def _add_spinner_to_container(self, container, legend=None): |
1566 | + """Add a spinner to 'container'.""" |
1567 | + spinner = Gtk.Spinner() |
1568 | + spinner.start() |
1569 | + |
1570 | + label = Gtk.Label() |
1571 | + if legend: |
1572 | + label.set_text(legend) |
1573 | + else: |
1574 | + label.set_text(LOADING) |
1575 | + |
1576 | + hbox = Gtk.HBox(spacing=5) |
1577 | + hbox.pack_start(spinner, expand=False, fill=True, padding=0) |
1578 | + hbox.pack_start(label, expand=False, fill=True, padding=0) |
1579 | + |
1580 | + alignment = Gtk.Alignment(xalign=0.5, yalign=0.5, |
1581 | + xscale=0, yscale=0) |
1582 | + alignment.add(hbox) |
1583 | + alignment.show_all() |
1584 | + |
1585 | + # remove children to avoid: |
1586 | + # GtkWarning: Attempting to add a widget with type GtkAlignment to a |
1587 | + # GtkEventBox, but as a GtkBin subclass a GtkEventBox can only contain |
1588 | + # one widget at a time |
1589 | + for child in container.get_children(): |
1590 | + container.remove(child) |
1591 | + container.add(alignment) |
1592 | + |
1593 | + def _set_warning_message(self, widget, message): |
1594 | + """Set 'message' as text for 'widget'.""" |
1595 | + widget.set_text(message) |
1596 | + widget.override_color(Gtk.StateFlags.NORMAL, WARNING_TEXT_COLOR) |
1597 | + widget.show() |
1598 | + |
1599 | + def _clear_warnings(self): |
1600 | + """Clear all warning messages.""" |
1601 | + for widget in self.warnings: |
1602 | + widget.set_text('') |
1603 | + for widget in self.entries: |
1604 | + getattr(self, widget).clear_warning() |
1605 | + |
1606 | + def _non_empty_input(self, widget): |
1607 | + """Return weather widget has non empty content.""" |
1608 | + text = widget.get_text() |
1609 | + return bool(text and not text.isspace()) |
1610 | + |
1611 | + def _handle_error(self, remote_call, handler, error): |
1612 | + """Handle any error when calling the remote backend.""" |
1613 | + logger.error('Remote call %r failed with: %r', remote_call, error) |
1614 | + errordict = {'message': GENERIC_BACKEND_ERROR} |
1615 | + handler(self.app_name, errordict) |
1616 | + |
1617 | + # build pages |
1618 | + |
1619 | + def _append_pages(self): |
1620 | + """Append all the requires pages to main widget.""" |
1621 | + self._append_page(self._build_processing_page()) |
1622 | + self._append_page(self._build_finish_page()) |
1623 | + self._append_page(self._build_login_page()) |
1624 | + self._append_page(self._build_request_password_token_page()) |
1625 | + self._append_page(self._build_set_new_password_page()) |
1626 | + self._append_page(self._build_verify_email_page()) |
1627 | + |
1628 | + if not self.login_only: |
1629 | + self._append_page(self._build_enter_details_page()) |
1630 | + self._append_page(self._build_tc_page()) |
1631 | + self.login_button.grab_focus() |
1632 | + self._set_current_page(self.enter_details_vbox) |
1633 | + else: |
1634 | + self.login_back_button.hide() |
1635 | + self.login_ok_button.grab_focus() |
1636 | + self.login_vbox.help_text = self.help_text |
1637 | + self._set_current_page(self.login_vbox) |
1638 | + |
1639 | + def _append_page(self, page): |
1640 | + """Append 'page' to the 'window'.""" |
1641 | + self.content.append_page(page, None) |
1642 | + |
1643 | + def _set_header(self, header): |
1644 | + """Set 'header' as the window title and header.""" |
1645 | + self.header_label.set_markup(LARGE_MARKUP % header) |
1646 | + self.window.set_title(self.header_label.get_text()) # avoid markup |
1647 | + |
1648 | + def _set_current_page(self, current_page, warning_text=None): |
1649 | + """Hide all the pages but 'current_page'.""" |
1650 | + if hasattr(current_page, 'header'): |
1651 | + self._set_header(current_page.header) |
1652 | + |
1653 | + if hasattr(current_page, 'help_text'): |
1654 | + self.help_label.set_markup(current_page.help_text) |
1655 | + |
1656 | + if warning_text is not None: |
1657 | + self._set_warning_message(self.warning_label, warning_text) |
1658 | + else: |
1659 | + self.warning_label.set_text('') |
1660 | + |
1661 | + self.content.set_current_page(self.content.page_num(current_page)) |
1662 | + |
1663 | + if current_page.default_widget is not None: |
1664 | + current_page.default_widget.grab_default() |
1665 | + |
1666 | + def _generate_captcha(self): |
1667 | + """Ask for a new captcha; update the ui to reflect the fact.""" |
1668 | + logger.info('Calling generate_captcha with filename path at %r', |
1669 | + self._captcha_filename) |
1670 | + self.warning_label.set_text('') |
1671 | + f = self.backend.generate_captcha |
1672 | + error_handler = partial(self._handle_error, f, |
1673 | + self.on_captcha_generation_error) |
1674 | + f(self.app_name, self._captcha_filename, |
1675 | + reply_handler=NO_OP, error_handler=error_handler) |
1676 | + self._set_captcha_loading() |
1677 | + |
1678 | + def _set_captcha_loading(self): |
1679 | + """Present a spinner to the user while the captcha is downloaded.""" |
1680 | + self.captcha_image.hide() |
1681 | + self._add_spinner_to_container(self.captcha_loading) |
1682 | + self.captcha_loading.override_background_color(Gtk.StateFlags.NORMAL, |
1683 | + parse_color('white')) |
1684 | + self.captcha_loading.show_all() |
1685 | + self.join_ok_button.set_sensitive(False) |
1686 | + |
1687 | + def _set_captcha_image(self): |
1688 | + """Present a captcha image to the user to be resolved.""" |
1689 | + self.captcha_loading.hide() |
1690 | + self.join_ok_button.set_sensitive(True) |
1691 | + self.captcha_image.set_from_file(self._captcha_filename) |
1692 | + self.captcha_image.show() |
1693 | + |
1694 | + def _build_enter_details_page(self): |
1695 | + """Build the enter details page.""" |
1696 | + d = {'app_name': self.app_label} |
1697 | + self.enter_details_vbox.header = JOIN_HEADER_LABEL % d |
1698 | + self.enter_details_vbox.help_text = self.help_text |
1699 | + self.enter_details_vbox.default_widget = self.join_ok_button |
1700 | + self.join_ok_button.set_can_default(True) |
1701 | + |
1702 | + self.enter_details_vbox.pack_start(self.name_entry, |
1703 | + expand=False, fill=True, padding=0) |
1704 | + self.enter_details_vbox.reorder_child(self.name_entry, 0) |
1705 | + entry = self.captcha_solution_entry |
1706 | + self.captcha_solution_vbox.pack_start(entry, |
1707 | + expand=False, fill=True, padding=0) |
1708 | + msg = CAPTCHA_RELOAD_TOOLTIP |
1709 | + self.captcha_reload_button.set_tooltip_text(msg) |
1710 | + |
1711 | + self.emails_hbox.pack_start(self.email1_entry, |
1712 | + expand=False, fill=True, padding=0) |
1713 | + self.emails_hbox.pack_start(self.email2_entry, |
1714 | + expand=False, fill=True, padding=0) |
1715 | + |
1716 | + self.passwords_hbox.pack_start(self.password1_entry, |
1717 | + expand=False, fill=True, padding=0) |
1718 | + self.passwords_hbox.pack_start(self.password2_entry, |
1719 | + expand=False, fill=True, padding=0) |
1720 | + help_msg = '<small>%s</small>' % PASSWORD_HELP |
1721 | + self.password_help_label.set_markup(help_msg) |
1722 | + |
1723 | + if not os.path.exists(self._captcha_filename): |
1724 | + self._generate_captcha() |
1725 | + else: |
1726 | + self._set_captcha_image() |
1727 | + |
1728 | + msg = YES_TO_UPDATES % {'app_name': self.app_name} |
1729 | + self.yes_to_updates_checkbutton.set_label(msg) |
1730 | + |
1731 | + msg = YES_TO_TC % {'app_name': self.app_name} |
1732 | + self.yes_to_tc_checkbutton.set_label(msg) |
1733 | + self.tc_button.set_label(TC_BUTTON) |
1734 | + |
1735 | + if not self.tc_url: |
1736 | + self.tc_vbox.hide() |
1737 | + self.login_button.set_label(LOGIN_BUTTON_LABEL) |
1738 | + |
1739 | + return self.enter_details_vbox |
1740 | + |
1741 | + def _build_tc_page(self): |
1742 | + """Build the Terms & Conditions page.""" |
1743 | + self.tc_browser_vbox.help_text = '' |
1744 | + self.tc_browser_vbox.default_widget = self.tc_back_button |
1745 | + self.tc_browser_vbox.default_widget.set_can_default(True) |
1746 | + return self.tc_browser_vbox |
1747 | + |
1748 | + def _build_processing_page(self): |
1749 | + """Build the processing page with a spinner.""" |
1750 | + self.processing_vbox.default_widget = None |
1751 | + self._add_spinner_to_container(self.processing_vbox, |
1752 | + legend=ONE_MOMENT_PLEASE) |
1753 | + return self.processing_vbox |
1754 | + |
1755 | + def _build_verify_email_page(self): |
1756 | + """Build the verify email page.""" |
1757 | + self.verify_email_vbox.default_widget = self.verify_token_button |
1758 | + self.verify_email_vbox.default_widget.set_can_default(True) |
1759 | + |
1760 | + self.verify_email_details_vbox.pack_start(self.email_token_entry, |
1761 | + expand=False, fill=True, padding=0) |
1762 | + return self.verify_email_vbox |
1763 | + |
1764 | + def _build_finish_page(self): |
1765 | + """Build the success page.""" |
1766 | + self.finish_vbox.default_widget = self.finish_close_button |
1767 | + self.finish_vbox.default_widget.set_can_default(True) |
1768 | + self.finish_vbox.label = self.finish_label |
1769 | + return self.finish_vbox |
1770 | + |
1771 | + def _build_login_page(self): |
1772 | + """Build the login page.""" |
1773 | + d = {'app_name': self.app_label} |
1774 | + self.login_vbox.header = LOGIN_HEADER_LABEL % d |
1775 | + self.login_vbox.help_text = CONNECT_HELP_LABEL % d |
1776 | + self.login_vbox.default_widget = self.login_ok_button |
1777 | + self.login_vbox.default_widget.set_can_default(True) |
1778 | + |
1779 | + self.login_details_vbox.pack_start(self.login_email_entry, |
1780 | + expand=True, fill=True, padding=0) |
1781 | + self.login_details_vbox.reorder_child(self.login_email_entry, 0) |
1782 | + self.login_details_vbox.pack_start(self.login_password_entry, |
1783 | + expand=True, fill=True, padding=0) |
1784 | + self.login_details_vbox.reorder_child(self.login_password_entry, 1) |
1785 | + |
1786 | + msg = FORGOTTEN_PASSWORD_BUTTON |
1787 | + self.forgotten_password_button.set_label(msg) |
1788 | + self.login_ok_button.grab_focus() |
1789 | + |
1790 | + return self.login_vbox |
1791 | + |
1792 | + def _build_request_password_token_page(self): |
1793 | + """Build the login page.""" |
1794 | + self.request_password_token_vbox.header = RESET_PASSWORD |
1795 | + text = REQUEST_PASSWORD_TOKEN_LABEL % {'app_name': self.app_label} |
1796 | + self.request_password_token_vbox.help_text = text |
1797 | + btn = self.request_password_token_ok_button |
1798 | + btn.set_can_default(True) |
1799 | + self.request_password_token_vbox.default_widget = btn |
1800 | + |
1801 | + entry = self.reset_email_entry |
1802 | + self.request_password_token_details_vbox.pack_start(entry, |
1803 | + expand=False, fill=True, padding=0) |
1804 | + cb = self.on_reset_email_entry_changed |
1805 | + self.reset_email_entry.connect('changed', cb) |
1806 | + self.request_password_token_ok_button.set_label(NEXT) |
1807 | + self.request_password_token_ok_button.set_sensitive(False) |
1808 | + |
1809 | + return self.request_password_token_vbox |
1810 | + |
1811 | + def _build_set_new_password_page(self): |
1812 | + """Build the login page.""" |
1813 | + self.set_new_password_vbox.header = RESET_PASSWORD |
1814 | + self.set_new_password_vbox.help_text = SET_NEW_PASSWORD_LABEL |
1815 | + btn = self.set_new_password_ok_button |
1816 | + btn.set_can_default(True) |
1817 | + self.set_new_password_vbox.default_widget = btn |
1818 | + |
1819 | + for entry in (self.reset_code_entry, |
1820 | + self.reset_password1_entry, |
1821 | + self.reset_password2_entry): |
1822 | + self.set_new_password_details_vbox.pack_start(entry, |
1823 | + expand=False, fill=True, padding=0) |
1824 | + |
1825 | + cb = self.on_set_new_password_entries_changed |
1826 | + self.reset_code_entry.connect('changed', cb) |
1827 | + self.reset_password1_entry.connect('changed', cb) |
1828 | + self.reset_password2_entry.connect('changed', cb) |
1829 | + help_msg = '<small>%s</small>' % PASSWORD_HELP |
1830 | + self.reset_password_help_label.set_markup(help_msg) |
1831 | + |
1832 | + self.set_new_password_ok_button.set_label(RESET_PASSWORD) |
1833 | + self.set_new_password_ok_button.set_sensitive(False) |
1834 | + |
1835 | + return self.set_new_password_vbox |
1836 | + |
1837 | + def _validate_email(self, email1, email2=None): |
1838 | + """Validate 'email1', return error message if not valid. |
1839 | + |
1840 | + If 'email2' is given, must match 'email1'. |
1841 | + """ |
1842 | + if email2 is not None and email1 != email2: |
1843 | + return EMAIL_MISMATCH |
1844 | + |
1845 | + if not email1: |
1846 | + return FIELD_REQUIRED |
1847 | + |
1848 | + if not is_correct_email(email1): |
1849 | + return EMAIL_INVALID |
1850 | + |
1851 | + def _validate_password(self, password1, password2=None): |
1852 | + """Validate 'password1', return error message if not valid. |
1853 | + |
1854 | + If 'password2' is given, must match 'email1'. |
1855 | + """ |
1856 | + if password2 is not None and password1 != password2: |
1857 | + return PASSWORD_MISMATCH |
1858 | + |
1859 | + if not is_min_required_password(password1): |
1860 | + return PASSWORD_TOO_WEAK |
1861 | + |
1862 | + # GTK callbacks |
1863 | + |
1864 | + def destroy(self): |
1865 | + """Destroy this UI.""" |
1866 | + self.window.hide() |
1867 | + self.window.destroy() |
1868 | + |
1869 | + def connect(self, signal_name, handler, *args, **kwargs): |
1870 | + """Connect 'signal_name' with 'handler'.""" |
1871 | + logger.debug('connect: signal %r, handler %r, args %r, kwargs, %r', |
1872 | + signal_name, handler, args, kwargs) |
1873 | + self.window.connect(signal_name, handler, *args, **kwargs) |
1874 | + |
1875 | + def finish_success(self): |
1876 | + """The whole process was completed succesfully. Show success page.""" |
1877 | + self._done = True |
1878 | + self._set_current_page(self.success_vbox) |
1879 | + |
1880 | + def finish_error(self): |
1881 | + """The whole process was not completed succesfully. Show error page.""" |
1882 | + self._done = True |
1883 | + self._set_current_page(self.error_vbox) |
1884 | + |
1885 | + def on_activate_link(self, button): |
1886 | + """Do nothing, used for LinkButtons that are used as regular ones.""" |
1887 | + return True |
1888 | + |
1889 | + def on_close_clicked(self, *args, **kwargs): |
1890 | + """Call self.close_callback if defined.""" |
1891 | + if os.path.exists(self._captcha_filename): |
1892 | + os.remove(self._captcha_filename) |
1893 | + |
1894 | + for signal, match in self._signals_receivers.items(): |
1895 | + self.backend.disconnect_from_signal(signal, match) |
1896 | + |
1897 | + # hide the main window |
1898 | + if self.window is not None: |
1899 | + self.window.hide() |
1900 | + |
1901 | + # process any pending events before callbacking with result |
1902 | + while Gtk.events_pending(): |
1903 | + Gtk.main_iteration() |
1904 | + |
1905 | + return_code = USER_SUCCESS |
1906 | + if not self._done: |
1907 | + return_code = USER_CANCELLATION |
1908 | + logger.info('Return code will be %r.', return_code) |
1909 | + |
1910 | + # call user defined callback |
1911 | + logger.debug('Calling custom close_callback %r with params %r, %r', |
1912 | + self.close_callback, args, kwargs) |
1913 | + self.close_callback(*args, **kwargs) |
1914 | + |
1915 | + sys.exit(return_code) |
1916 | + |
1917 | + def on_sign_in_button_clicked(self, *args, **kwargs): |
1918 | + """User wants to sign in, present the Login page.""" |
1919 | + self._set_current_page(self.login_vbox) |
1920 | + |
1921 | + def on_join_ok_button_clicked(self, *args, **kwargs): |
1922 | + """Submit info for processing, present the processing vbox.""" |
1923 | + if not self.join_ok_button.is_sensitive(): |
1924 | + return |
1925 | + |
1926 | + self._clear_warnings() |
1927 | + |
1928 | + error = False |
1929 | + |
1930 | + name = self.name_entry.get_text() |
1931 | + if not name: |
1932 | + self.name_entry.set_warning(FIELD_REQUIRED) |
1933 | + logger.warning('on_join_ok_button_clicked: name not set.') |
1934 | + error = True |
1935 | + |
1936 | + # check email |
1937 | + email1 = self.email1_entry.get_text() |
1938 | + email2 = self.email2_entry.get_text() |
1939 | + msg = self._validate_email(email1, email2) |
1940 | + if msg is not None: |
1941 | + self.email1_entry.set_warning(msg) |
1942 | + self.email2_entry.set_warning(msg) |
1943 | + logger.warning('on_join_ok_button_clicked: email is not valid.') |
1944 | + error = True |
1945 | + |
1946 | + # check password |
1947 | + password1 = self.password1_entry.get_text() |
1948 | + password2 = self.password2_entry.get_text() |
1949 | + msg = self._validate_password(password1, password2) |
1950 | + if msg is not None: |
1951 | + self.password1_entry.set_warning(msg) |
1952 | + self.password2_entry.set_warning(msg) |
1953 | + logger.warning('on_join_ok_button_clicked: password is not valid.') |
1954 | + error = True |
1955 | + |
1956 | + # check T&C |
1957 | + if self.tc_url and not self.yes_to_tc_checkbutton.get_active(): |
1958 | + self._set_warning_message(self.tc_warning_label, |
1959 | + TC_NOT_ACCEPTED % {'app_name': self.app_name}) |
1960 | + logger.warning('on_join_ok_button_clicked: terms and conditions ' |
1961 | + 'not accepted.') |
1962 | + error = True |
1963 | + |
1964 | + captcha_solution = self.captcha_solution_entry.get_text() |
1965 | + if not captcha_solution: |
1966 | + self.captcha_solution_entry.set_warning(FIELD_REQUIRED) |
1967 | + logger.warning('on_join_ok_button_clicked: captcha solution not ' |
1968 | + 'set.') |
1969 | + error = True |
1970 | + |
1971 | + if error: |
1972 | + logger.warning('on_join_ok_button_clicked: validation failed.') |
1973 | + return |
1974 | + |
1975 | + logger.info('on_join_ok_button_clicked: validation success!') |
1976 | + |
1977 | + self._set_current_page(self.processing_vbox) |
1978 | + self.user_email = email1 |
1979 | + self.user_password = password1 |
1980 | + |
1981 | + logger.info('Calling register_user with email %r, password <hidden>,' |
1982 | + ' name %r, captcha_id %r and captcha_solution %r.', email1, |
1983 | + name, self._captcha_id, captcha_solution) |
1984 | + |
1985 | + f = self.backend.register_user |
1986 | + error_handler = partial(self._handle_error, f, |
1987 | + self.on_user_registration_error) |
1988 | + f(self.app_name, self.user_email, self.user_password, name, |
1989 | + self._captcha_id, captcha_solution, |
1990 | + reply_handler=NO_OP, error_handler=error_handler) |
1991 | + |
1992 | + def on_verify_token_button_clicked(self, *args, **kwargs): |
1993 | + """The user entered the email token, let's verify!""" |
1994 | + if not self.verify_token_button.is_sensitive(): |
1995 | + return |
1996 | + |
1997 | + self._clear_warnings() |
1998 | + |
1999 | + email_token = self.email_token_entry.get_text() |
2000 | + if not email_token: |
2001 | + self.email_token_entry.set_warning(FIELD_REQUIRED) |
2002 | + return |
2003 | + |
2004 | + email = self.user_email |
2005 | + password = self.user_password |
2006 | + |
2007 | + args = (self.app_name, email, password, email_token) |
2008 | + if self.ping_url: |
2009 | + f = self.backend.validate_email_and_ping |
2010 | + args = args + (self.ping_url,) |
2011 | + else: |
2012 | + f = self.backend.validate_email |
2013 | + |
2014 | + logger.info('Calling validate_email with email %r, password <hidden>, ' |
2015 | + 'app_name %r and email_token %r.', email, self.app_name, |
2016 | + email_token) |
2017 | + error_handler = partial(self._handle_error, f, |
2018 | + self.on_email_validation_error) |
2019 | + f(*args, reply_handler=NO_OP, error_handler=error_handler) |
2020 | + |
2021 | + self._set_current_page(self.processing_vbox) |
2022 | + |
2023 | + def on_login_connect_button_clicked(self, *args, **kwargs): |
2024 | + """User wants to connect!""" |
2025 | + if not self.login_ok_button.is_sensitive(): |
2026 | + return |
2027 | + |
2028 | + self._clear_warnings() |
2029 | + |
2030 | + error = False |
2031 | + |
2032 | + email = self.login_email_entry.get_text() |
2033 | + msg = self._validate_email(email) |
2034 | + if msg is not None: |
2035 | + self.login_email_entry.set_warning(msg) |
2036 | + error = True |
2037 | + |
2038 | + password = self.login_password_entry.get_text() |
2039 | + if not password: |
2040 | + self.login_password_entry.set_warning(FIELD_REQUIRED) |
2041 | + error = True |
2042 | + |
2043 | + if error: |
2044 | + return |
2045 | + |
2046 | + args = (self.app_name, email, password) |
2047 | + if self.ping_url: |
2048 | + f = self.backend.login_and_ping |
2049 | + args = args + (self.ping_url,) |
2050 | + else: |
2051 | + f = self.backend.login |
2052 | + |
2053 | + error_handler = partial(self._handle_error, f, self.on_login_error) |
2054 | + f(*args, reply_handler=NO_OP, error_handler=error_handler) |
2055 | + |
2056 | + self._set_current_page(self.processing_vbox) |
2057 | + self.user_email = email |
2058 | + self.user_password = password |
2059 | + |
2060 | + def on_login_back_button_clicked(self, *args, **kwargs): |
2061 | + """User wants to go to the previous page.""" |
2062 | + self._set_current_page(self.enter_details_vbox) |
2063 | + |
2064 | + def on_forgotten_password_button_clicked(self, *args, **kwargs): |
2065 | + """User wants to reset the password.""" |
2066 | + self._set_current_page(self.request_password_token_vbox) |
2067 | + |
2068 | + def on_request_password_token_ok_button_clicked(self, *args, **kwargs): |
2069 | + """User entered the email address to reset the password.""" |
2070 | + if not self.request_password_token_ok_button.is_sensitive(): |
2071 | + return |
2072 | + |
2073 | + self._clear_warnings() |
2074 | + |
2075 | + email = self.reset_email_entry.get_text() |
2076 | + msg = self._validate_email(email) |
2077 | + if msg is not None: |
2078 | + self.reset_email_entry.set_warning(msg) |
2079 | + return |
2080 | + |
2081 | + logger.info('Calling request_password_reset_token with %r.', email) |
2082 | + f = self.backend.request_password_reset_token |
2083 | + error_handler = partial(self._handle_error, f, |
2084 | + self.on_password_reset_error) |
2085 | + f(self.app_name, email, |
2086 | + reply_handler=NO_OP, error_handler=error_handler) |
2087 | + |
2088 | + self._set_current_page(self.processing_vbox) |
2089 | + |
2090 | + def on_request_password_token_back_button_clicked(self, *args, **kwargs): |
2091 | + """User wants to go to the previous page.""" |
2092 | + self._set_current_page(self.login_vbox) |
2093 | + |
2094 | + def on_reset_email_entry_changed(self, widget, *args, **kwargs): |
2095 | + """User is changing the 'widget' entry in the reset email page.""" |
2096 | + sensitive = self._non_empty_input(widget) |
2097 | + self.request_password_token_ok_button.set_sensitive(sensitive) |
2098 | + |
2099 | + def on_set_new_password_entries_changed(self, *args, **kwargs): |
2100 | + """User is changing the 'widget' entry in the reset password page.""" |
2101 | + sensitive = True |
2102 | + for entry in (self.reset_code_entry, |
2103 | + self.reset_password1_entry, |
2104 | + self.reset_password2_entry): |
2105 | + sensitive &= self._non_empty_input(entry) |
2106 | + self.set_new_password_ok_button.set_sensitive(sensitive) |
2107 | + |
2108 | + def on_set_new_password_ok_button_clicked(self, *args, **kwargs): |
2109 | + """User entered reset code and new passwords.""" |
2110 | + if not self.set_new_password_ok_button.is_sensitive(): |
2111 | + return |
2112 | + |
2113 | + self._clear_warnings() |
2114 | + |
2115 | + error = False |
2116 | + |
2117 | + token = self.reset_code_entry.get_text() |
2118 | + if not token: |
2119 | + self.reset_code_entry.set_warning(FIELD_REQUIRED) |
2120 | + error = True |
2121 | + |
2122 | + password1 = self.reset_password1_entry.get_text() |
2123 | + password2 = self.reset_password2_entry.get_text() |
2124 | + msg = self._validate_password(password1, password2) |
2125 | + if msg is not None: |
2126 | + self.reset_password1_entry.set_warning(msg) |
2127 | + self.reset_password2_entry.set_warning(msg) |
2128 | + error = True |
2129 | + |
2130 | + if error: |
2131 | + return |
2132 | + |
2133 | + email = self.reset_email_entry.get_text() |
2134 | + logger.info('Calling set_new_password with email %r, token %r and ' |
2135 | + 'new password: <hidden>.', email, token) |
2136 | + f = self.backend.set_new_password |
2137 | + error_handler = partial(self._handle_error, f, |
2138 | + self.on_password_change_error) |
2139 | + f(self.app_name, email, token, password1, |
2140 | + reply_handler=NO_OP, error_handler=error_handler) |
2141 | + |
2142 | + self._set_current_page(self.processing_vbox) |
2143 | + |
2144 | + def _webkit_init_ssl(self): |
2145 | + """Set the WebKit ssl strictness.""" |
2146 | + # delay the import of webkit to be able to build without it |
2147 | + from gi.repository import WebKit # pylint: disable=E0611 |
2148 | + |
2149 | + # Set the Soup session to be strict and use system CA certs |
2150 | + session = WebKit.get_default_session() |
2151 | + session.set_property(STRICT_SSL_PROP, True) |
2152 | + session.set_property(CERTS_FILE_PROP, CA_CERT_FILE) |
2153 | + |
2154 | + def _add_webkit_browser(self): |
2155 | + """Add the webkit browser for the t&c.""" |
2156 | + # delay the import of webkit to be able to build without it |
2157 | + from gi.repository import WebKit # pylint: disable=E0611 |
2158 | + |
2159 | + self._webkit_init_ssl() |
2160 | + |
2161 | + browser = WebKit.WebView() |
2162 | + |
2163 | + browser.connect('notify::load-status', |
2164 | + self.on_tc_browser_notify_load_status) |
2165 | + browser.connect('navigation-policy-decision-requested', |
2166 | + self.on_tc_browser_navigation_requested) |
2167 | + |
2168 | + settings = browser.get_settings() |
2169 | + settings.set_property("enable-plugins", False) |
2170 | + settings.set_property("enable-default-context-menu", False) |
2171 | + |
2172 | + # webkit_web_view_open has been deprecated since version 1.1.1 and |
2173 | + # should not be used in newly-written code. Use |
2174 | + # webkit_web_view_load_uri() instead. |
2175 | + browser.load_uri(self.tc_url) |
2176 | + browser.show() |
2177 | + self.tc_browser_window.add(browser) |
2178 | + |
2179 | + def on_tc_button_clicked(self, *args, **kwargs): |
2180 | + """The T&C button was clicked, create the browser and load terms.""" |
2181 | + if self.tc_browser_window.get_child() is None: |
2182 | + self._add_webkit_browser() |
2183 | + self._set_current_page(self.processing_vbox) |
2184 | + else: |
2185 | + self._set_current_page(self.tc_browser_vbox) |
2186 | + |
2187 | + def on_tc_back_button_clicked(self, *args, **kwargs): |
2188 | + """T & C 'back' button was clicked, return to the previous page.""" |
2189 | + self._set_current_page(self.enter_details_vbox) |
2190 | + |
2191 | + def on_tc_browser_notify_load_status(self, browser, *args, **kwargs): |
2192 | + """The T&C page is being loaded.""" |
2193 | + from gi.repository import WebKit # pylint: disable=E0611 |
2194 | + |
2195 | + if browser.get_load_status().real == WebKit.LoadStatus.FINISHED: |
2196 | + self._set_current_page(self.tc_browser_vbox) |
2197 | + |
2198 | + def on_tc_browser_navigation_requested(self, browser, frame, request, |
2199 | + action, decision, *args, **kwargs): |
2200 | + """The user wants to navigate within the T&C browser.""" |
2201 | + from gi.repository import WebKit # pylint: disable=E0611 |
2202 | + |
2203 | + if action is not None and \ |
2204 | + action.get_reason() == WebKit.WebNavigationReason.LINK_CLICKED: |
2205 | + if decision is not None: |
2206 | + decision.ignore() |
2207 | + url = action.get_original_uri() |
2208 | + webbrowser.open(url) |
2209 | + else: |
2210 | + if decision is not None: |
2211 | + decision.use() |
2212 | + |
2213 | + def on_tc_browser_vbox_hide(self, *args, **kwargs): |
2214 | + """The T&C page is no longer being shown.""" |
2215 | + children = self.tc_browser_window.get_children() |
2216 | + if len(children) > 0: |
2217 | + browser = children[0] |
2218 | + self.tc_browser_window.remove(browser) |
2219 | + browser.destroy() |
2220 | + del browser |
2221 | + |
2222 | + def on_captcha_reload_button_clicked(self, *args, **kwargs): |
2223 | + """User clicked the reload captcha button.""" |
2224 | + self._generate_captcha() |
2225 | + |
2226 | + # backend callbacks |
2227 | + |
2228 | + def _build_general_error_message(self, errordict): |
2229 | + """Concatenate __all__ and message from the errordict.""" |
2230 | + result = None |
2231 | + msg1 = errordict.get('__all__') |
2232 | + msg2 = errordict.get('message') |
2233 | + if msg1 is not None and msg2 is not None: |
2234 | + result = '\n'.join((msg1, msg2)) |
2235 | + else: |
2236 | + result = msg1 if msg1 is not None else msg2 |
2237 | + return result |
2238 | + |
2239 | + @log_call |
2240 | + def on_captcha_generated(self, app_name, captcha_id, *args, **kwargs): |
2241 | + """Captcha image has been generated and is available to be shown.""" |
2242 | + if captcha_id is None: |
2243 | + logger.warning('on_captcha_generated: captcha_id is None for ' |
2244 | + 'app_name %r.', app_name) |
2245 | + self._captcha_id = captcha_id |
2246 | + self._set_captcha_image() |
2247 | + |
2248 | + @log_call |
2249 | + def on_captcha_generation_error(self, app_name, error, *args, **kwargs): |
2250 | + """Captcha image generation failed.""" |
2251 | + self._set_warning_message(self.warning_label, CAPTCHA_LOAD_ERROR) |
2252 | + self._generate_captcha() |
2253 | + |
2254 | + @log_call |
2255 | + def on_user_registered(self, app_name, email, *args, **kwargs): |
2256 | + """Registration can go on, user needs to verify email.""" |
2257 | + help_text = VERIFY_EMAIL_LABEL % {'app_name': self.app_name, |
2258 | + 'email': email} |
2259 | + self.verify_email_vbox.help_text = help_text |
2260 | + self._set_current_page(self.verify_email_vbox) |
2261 | + |
2262 | + @log_call |
2263 | + def on_user_registration_error(self, app_name, error, *args, **kwargs): |
2264 | + """Error in the data provided for registration.""" |
2265 | + msg = error.get('email') |
2266 | + if msg is not None: |
2267 | + self.email1_entry.set_warning(msg) |
2268 | + self.email2_entry.set_warning(msg) |
2269 | + |
2270 | + msg = error.get('password') |
2271 | + if msg is not None: |
2272 | + self.password1_entry.set_warning(msg) |
2273 | + self.password2_entry.set_warning(msg) |
2274 | + |
2275 | + msg = self._build_general_error_message(error) |
2276 | + self._generate_captcha() |
2277 | + self._set_current_page(self.enter_details_vbox, warning_text=msg) |
2278 | + |
2279 | + @log_call |
2280 | + def on_email_validated(self, app_name, email, *args, **kwargs): |
2281 | + """User email was successfully verified.""" |
2282 | + self.finish_success() |
2283 | + |
2284 | + @log_call |
2285 | + def on_email_validation_error(self, app_name, error, *args, **kwargs): |
2286 | + """User email validation failed.""" |
2287 | + msg = error.get('email_token') |
2288 | + if msg is not None: |
2289 | + self.email_token_entry.set_warning(msg) |
2290 | + |
2291 | + msg = self._build_general_error_message(error) |
2292 | + self._set_current_page(self.verify_email_vbox, warning_text=msg) |
2293 | + |
2294 | + @log_call |
2295 | + def on_logged_in(self, app_name, email, *args, **kwargs): |
2296 | + """User was successfully logged in.""" |
2297 | + self.finish_success() |
2298 | + |
2299 | + @log_call |
2300 | + def on_login_error(self, app_name, error, *args, **kwargs): |
2301 | + """User was not successfully logged in.""" |
2302 | + msg = self._build_general_error_message(error) |
2303 | + self._set_current_page(self.login_vbox, warning_text=msg) |
2304 | + |
2305 | + @log_call |
2306 | + def on_user_not_validated(self, app_name, email, *args, **kwargs): |
2307 | + """User was not validated.""" |
2308 | + self.on_user_registered(app_name, email) |
2309 | + |
2310 | + @log_call |
2311 | + def on_password_reset_token_sent(self, app_name, email, *args, **kwargs): |
2312 | + """Password reset token was successfully sent.""" |
2313 | + msg = SET_NEW_PASSWORD_LABEL % {'email': email} |
2314 | + self.set_new_password_vbox.help_text = msg |
2315 | + self._set_current_page(self.set_new_password_vbox) |
2316 | + |
2317 | + @log_call |
2318 | + def on_password_reset_error(self, app_name, error, *args, **kwargs): |
2319 | + """Password reset failed.""" |
2320 | + msg = self._build_general_error_message(error) |
2321 | + self._set_current_page(self.login_vbox, warning_text=msg) |
2322 | + |
2323 | + @log_call |
2324 | + def on_password_changed(self, app_name, email, *args, **kwargs): |
2325 | + """Password was successfully changed.""" |
2326 | + self._set_current_page(self.login_vbox, |
2327 | + warning_text=PASSWORD_CHANGED) |
2328 | + |
2329 | + @log_call |
2330 | + def on_password_change_error(self, app_name, error, *args, **kwargs): |
2331 | + """Password reset failed.""" |
2332 | + msg = self._build_general_error_message(error) |
2333 | + self._set_current_page(self.request_password_token_vbox, |
2334 | + warning_text=msg) |
2335 | + |
2336 | + |
2337 | +def run(**kwargs): |
2338 | + """Start the GTK mainloop and open the main window.""" |
2339 | + UbuntuSSOClientGUI(close_callback=Gtk.main_quit, **kwargs) |
2340 | + Gtk.main() |
2341 | |
2342 | === added directory 'softwarecenter/sso/tests' |
2343 | === added file 'softwarecenter/sso/tests/__init__.py' |
2344 | --- softwarecenter/sso/tests/__init__.py 1970-01-01 00:00:00 +0000 |
2345 | +++ softwarecenter/sso/tests/__init__.py 2012-06-28 15:39:18 +0000 |
2346 | @@ -0,0 +1,26 @@ |
2347 | +# -*- coding: utf-8 -*- |
2348 | +# |
2349 | +# Copyright 2009-2012 Canonical Ltd. |
2350 | +# |
2351 | +# This program is free software: you can redistribute it and/or modify it |
2352 | +# under the terms of the GNU General Public License version 3, as published |
2353 | +# by the Free Software Foundation. |
2354 | +# |
2355 | + |
2356 | +"""Tests for the Ubuntu SSO GTK+ graphical interface.""" |
2357 | + |
2358 | +APP_NAME = u'I ♥ Ubuntu' |
2359 | +CAPTCHA_ID = u'test ñiña' |
2360 | +CAPTCHA_SOLUTION = u'william Byrd ñandú' |
2361 | +EMAIL = u'test@example.com' |
2362 | +EMAIL_TOKEN = u'B2P☺ gtf' |
2363 | +HELP_TEXT = u'☛ Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' \ |
2364 | +'Nam sed lorem nibh. Suspendisse gravida nulla non nunc suscipit pulvinar ' \ |
2365 | +'tempus ut augue. Morbi consequat, ligula a elementum pretium, ' \ |
2366 | +'dolor nulla tempus metus, sed viverra nisi risus non velit.' |
2367 | +NAME = u'Juanito ☀ Pérez' |
2368 | +PASSWORD = u'h3lloWorld☑ ' |
2369 | +PING_URL = u'http://localhost/ping-me/' |
2370 | +RESET_PASSWORD_TOKEN = u'8G5Wtq' |
2371 | +TC_URL = u'http://localhost/' |
2372 | +UNKNOWN_ERROR = u'Something went very wrong! ☹' |
2373 | |
2374 | === added file 'softwarecenter/sso/tests/test_gui.py' |
2375 | --- softwarecenter/sso/tests/test_gui.py 1970-01-01 00:00:00 +0000 |
2376 | +++ softwarecenter/sso/tests/test_gui.py 2012-06-28 15:39:18 +0000 |
2377 | @@ -0,0 +1,2300 @@ |
2378 | +# -*- coding: utf-8 -*- |
2379 | +# |
2380 | +# Copyright 2010-2012 Canonical Ltd. |
2381 | +# |
2382 | +# This program is free software: you can redistribute it and/or modify it |
2383 | +# under the terms of the GNU General Public License version 3, as published |
2384 | +# by the Free Software Foundation. |
2385 | +# |
2386 | +# This program is distributed in the hope that it will be useful, but |
2387 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
2388 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
2389 | +# PURPOSE. See the GNU General Public License for more details. |
2390 | +# |
2391 | +# You should have received a copy of the GNU General Public License along |
2392 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
2393 | + |
2394 | +"""Tests for the GUI for registration/login.""" |
2395 | + |
2396 | +import itertools |
2397 | +import logging |
2398 | +import os |
2399 | + |
2400 | +from collections import defaultdict |
2401 | +from functools import partial |
2402 | +from unittest import skip, TestCase |
2403 | + |
2404 | +# pylint: disable=E0611 |
2405 | +from gi.repository import Gdk, Gtk, WebKit |
2406 | +# pylint: enable=E0611 |
2407 | +from mock import patch |
2408 | + |
2409 | +from softwarecenter.sso import gui |
2410 | +from softwarecenter.sso.tests import ( |
2411 | + APP_NAME, |
2412 | + CAPTCHA_ID, |
2413 | + CAPTCHA_SOLUTION, |
2414 | + EMAIL, |
2415 | + EMAIL_TOKEN, |
2416 | + HELP_TEXT, |
2417 | + NAME, |
2418 | + PASSWORD, |
2419 | + PING_URL, |
2420 | + RESET_PASSWORD_TOKEN, |
2421 | + TC_URL, |
2422 | + UNKNOWN_ERROR, |
2423 | +) |
2424 | + |
2425 | +# Access to a protected member 'yyy' of a client class |
2426 | +# pylint: disable=W0212 |
2427 | + |
2428 | +# Instance of 'UbuntuSSOClientGUI' has no 'yyy' member |
2429 | +# pylint: disable=E1101,E1103 |
2430 | + |
2431 | +# Use of super on an old style class |
2432 | +# pylint: disable=E1002 |
2433 | + |
2434 | + |
2435 | +class FakedSSOBackend(object): |
2436 | + """Fake a SSO Backend.""" |
2437 | + |
2438 | + def __init__(self, *args, **kwargs): |
2439 | + self._args = args |
2440 | + self._kwargs = kwargs |
2441 | + self._called = {} |
2442 | + self.callbacks = defaultdict(list) |
2443 | + |
2444 | + for i in ('generate_captcha', 'login', 'login_and_ping', |
2445 | + 'register_user', 'validate_email', |
2446 | + 'validate_email_and_ping', |
2447 | + 'request_password_reset_token', |
2448 | + 'set_new_password'): |
2449 | + setattr(self, i, self._record_call(i)) |
2450 | + |
2451 | + def connect_to_signal(self, signal_name, callback): |
2452 | + """Connect a callback to a given signal.""" |
2453 | + self.callbacks[signal_name].append(callback) |
2454 | + return callback |
2455 | + |
2456 | + def disconnect_from_signal(self, signal_name, match): |
2457 | + """Disconnect from a given signal.""" |
2458 | + self.callbacks[signal_name].remove(match) |
2459 | + if len(self.callbacks[signal_name]) == 0: |
2460 | + self.callbacks.pop(signal_name) |
2461 | + |
2462 | + def _record_call(self, func_name): |
2463 | + """Store values when calling 'func_name'.""" |
2464 | + |
2465 | + def inner(*args, **kwargs): |
2466 | + """Fake 'func_name'.""" |
2467 | + self._called[func_name] = (args, kwargs) |
2468 | + |
2469 | + return inner |
2470 | + |
2471 | + |
2472 | +class FakedLogger(object): |
2473 | + |
2474 | + def __init__(self): |
2475 | + self.records = defaultdict(list) |
2476 | + self.debug = partial(self.log, logging.DEBUG) |
2477 | + self.info = partial(self.log, logging.INFO) |
2478 | + self.warning = partial(self.log, logging.WARNING) |
2479 | + self.error = self.exception = partial(self.log, logging.ERROR) |
2480 | + |
2481 | + def log(self, level, msg, *args): |
2482 | + """Fake a logging operation.""" |
2483 | + self.records[level].append(msg % args) |
2484 | + |
2485 | + def check(self, level, *msgs): |
2486 | + """Return whether there is a log entry for 'level' with 'msgs'.""" |
2487 | + result = all(any(msg in r for r in self.records[level]) |
2488 | + for msg in msgs) |
2489 | + return result |
2490 | + |
2491 | + def reset(self): |
2492 | + """Reset internal state.""" |
2493 | + self.records.clear() |
2494 | + |
2495 | + |
2496 | +class Settings(dict): |
2497 | + """Faked embedded browser settings.""" |
2498 | + |
2499 | + def get_property(self, prop_name): |
2500 | + """Alias for __getitem__.""" |
2501 | + return self[prop_name] |
2502 | + |
2503 | + def set_property(self, prop_name, newval): |
2504 | + """Alias for __setitem__.""" |
2505 | + self[prop_name] = newval |
2506 | + |
2507 | + |
2508 | +class FakedEmbeddedBrowser(Gtk.TextView): |
2509 | + """Faked an embedded browser.""" |
2510 | + |
2511 | + def __init__(self): |
2512 | + super(FakedEmbeddedBrowser, self).__init__() |
2513 | + self._props = {} |
2514 | + self._signals = defaultdict(list) |
2515 | + self._settings = Settings() |
2516 | + |
2517 | + def connect(self, signal_name, callback): |
2518 | + """Connect 'signal_name' with 'callback'.""" |
2519 | + self._signals[signal_name].append(callback) |
2520 | + |
2521 | + def load_uri(self, uri): |
2522 | + """Navigate to the given 'uri'.""" |
2523 | + self._props['uri'] = uri |
2524 | + |
2525 | + def set_property(self, prop_name, newval): |
2526 | + """Set 'prop_name' to 'newval'.""" |
2527 | + self._props[prop_name] = newval |
2528 | + |
2529 | + def get_property(self, prop_name): |
2530 | + """Return the current value for 'prop_name'.""" |
2531 | + return self._props[prop_name] |
2532 | + |
2533 | + def get_settings(self,): |
2534 | + """Return the current settings.""" |
2535 | + return self._settings |
2536 | + |
2537 | + def get_load_status(self): |
2538 | + """Return the current load status.""" |
2539 | + return WebKit.LoadStatus.FINISHED |
2540 | + |
2541 | + def show(self): |
2542 | + """Show this instance.""" |
2543 | + self.set_property('visible', True) |
2544 | + |
2545 | + |
2546 | +class BasicTestCase(TestCase): |
2547 | + """Test case with a helper tracker.""" |
2548 | + |
2549 | + def setUp(self): |
2550 | + """Init.""" |
2551 | + super(BasicTestCase, self).setUp() |
2552 | + self._called = False # helper |
2553 | + |
2554 | + self.memento = FakedLogger() |
2555 | + self.patch(gui, 'logger', self.memento) |
2556 | + self.patch(gui.sys, 'exit', lambda *a: None) |
2557 | + |
2558 | + def _set_called(self, *args, **kwargs): |
2559 | + """Set _called to True.""" |
2560 | + self._called = (args, kwargs) |
2561 | + |
2562 | + def patch(self, obj, attr, new_value): |
2563 | + patcher = patch.object(obj, attr, new_value) |
2564 | + patcher.start() |
2565 | + self.addCleanup(patcher.stop) |
2566 | + |
2567 | + def assert_color_equal(self, rgba_color, gdk_color): |
2568 | + """Check that 'rgba_color' is the same as 'gdk_color'.""" |
2569 | + tmp = Gdk.RGBA() |
2570 | + assert tmp.parse(gdk_color.to_string()) |
2571 | + |
2572 | + msg = 'Text color must be %r (got %r instead).' |
2573 | + self.assertEqual(rgba_color, tmp, msg % (rgba_color, tmp)) |
2574 | + |
2575 | + def assert_backend_called(self, method, *args, **kwargs): |
2576 | + """Check that 'method(*args, **kwargs)' was called in the backend.""" |
2577 | + self.assertIn(method, self.ui.backend._called) |
2578 | + |
2579 | + call = self.ui.backend._called[method] |
2580 | + self.assertEqual(call[0], args) |
2581 | + |
2582 | + reply_handler = call[1].pop('reply_handler') |
2583 | + self.assertEqual(reply_handler, gui.NO_OP) |
2584 | + |
2585 | + error_handler = call[1].pop('error_handler') |
2586 | + self.assertEqual(error_handler.func, self.ui._handle_error) |
2587 | + |
2588 | + self.assertEqual(call[1], kwargs) |
2589 | + |
2590 | + |
2591 | +class LabeledEntryTestCase(BasicTestCase): |
2592 | + """Test suite for the labeled entry.""" |
2593 | + |
2594 | + def setUp(self): |
2595 | + """Init.""" |
2596 | + super(LabeledEntryTestCase, self).setUp() |
2597 | + self.label = 'Test me please' |
2598 | + self.entry = gui.LabeledEntry(label=self.label) |
2599 | + |
2600 | + # we need a window to be able to realize ourselves |
2601 | + window = Gtk.Window() |
2602 | + window.add(self.entry) |
2603 | + window.show_all() |
2604 | + self.addCleanup(window.hide) |
2605 | + self.addCleanup(window.destroy) |
2606 | + |
2607 | + def grab_focus(self, focus_in=True): |
2608 | + """Grab focus on widget, if None use self.entry.""" |
2609 | + direction = 'in' if focus_in else 'out' |
2610 | + self.entry.emit('focus-%s-event' % direction, None) |
2611 | + |
2612 | + # Bad first argument 'LabeledEntry' given to super class |
2613 | + # pylint: disable=E1003 |
2614 | + def assert_correct_label(self): |
2615 | + """Check that the entry has the correct label.""" |
2616 | + # text content is correct |
2617 | + msg = 'Text content must be %r (got %r instead).' |
2618 | + expected = self.label |
2619 | + actual = super(gui.LabeledEntry, self.entry).get_text() |
2620 | + self.assertEqual(expected, actual, msg % (expected, actual)) |
2621 | + |
2622 | + # text color is correct |
2623 | + expected = gui.HELP_TEXT_COLOR |
2624 | + actual = self.entry.get_style().text[Gtk.StateFlags.NORMAL] |
2625 | + self.assert_color_equal(expected, actual) |
2626 | + |
2627 | + def test_initial_text(self): |
2628 | + """Entry have the correct text at startup.""" |
2629 | + self.assert_correct_label() |
2630 | + |
2631 | + def test_width_chars(self): |
2632 | + """Entry have the correct width.""" |
2633 | + self.assertEqual(self.entry.get_width_chars(), gui.DEFAULT_WIDTH) |
2634 | + |
2635 | + def test_tooltip(self): |
2636 | + """Entry have the correct tooltip.""" |
2637 | + msg = 'Tooltip must be %r (got %r instead).' |
2638 | + expected = self.label |
2639 | + actual = self.entry.get_tooltip_text() |
2640 | + # tooltip is correct |
2641 | + self.assertEqual(expected, actual, msg % (expected, actual)) |
2642 | + |
2643 | + def test_clear_entry_on_focus_in(self): |
2644 | + """Entry are cleared when focused.""" |
2645 | + self.grab_focus() |
2646 | + |
2647 | + msg = 'Entry must be cleared on focus in.' |
2648 | + self.assertEqual('', self.entry.get_text(), msg) |
2649 | + |
2650 | + def test_text_defaults_to_theme_color_when_focus_in(self): |
2651 | + """Entry restore its text color when focused in.""" |
2652 | + self.patch(self.entry, 'override_color', self._set_called) |
2653 | + |
2654 | + self.grab_focus() |
2655 | + |
2656 | + self.assertEqual(((Gtk.StateFlags.NORMAL, None), {}), self._called, |
2657 | + 'Entry text color must be restore on focus in.') |
2658 | + |
2659 | + def test_refill_entry_on_focus_out_if_no_input(self): |
2660 | + """Entry is re-filled with label when focused out if no user input.""" |
2661 | + |
2662 | + self.grab_focus() # grab focus |
2663 | + self.grab_focus(focus_in=False) # loose focus |
2664 | + |
2665 | + # Entry must be re-filled on focus out |
2666 | + self.assert_correct_label() |
2667 | + |
2668 | + def test_refill_entry_on_focus_out_if_empty_input(self): |
2669 | + """Entry is re-filled with label when focused out if empty input.""" |
2670 | + |
2671 | + self.grab_focus() # grab focus |
2672 | + |
2673 | + self.entry.set_text(' ') # add empty text to the entry |
2674 | + |
2675 | + self.grab_focus(focus_in=False) # loose focus |
2676 | + |
2677 | + # Entry must be re-filled on focus out |
2678 | + self.assert_correct_label() |
2679 | + |
2680 | + def test_preserve_input_on_focus_out_if_user_input(self): |
2681 | + """Entry is unmodified when focused out if user input.""" |
2682 | + msg = 'Entry must be left unmodified on focus out when user input.' |
2683 | + expected = 'test me please' |
2684 | + |
2685 | + self.grab_focus() # grab focus |
2686 | + |
2687 | + self.entry.set_text(expected) # add empty text to the entry |
2688 | + |
2689 | + self.grab_focus(focus_in=False) # loose focus |
2690 | + |
2691 | + self.assertEqual(expected, self.entry.get_text(), msg) |
2692 | + |
2693 | + def test_preserve_input_on_focus_out_and_in_again(self): |
2694 | + """Entry is unmodified when focused out and then in again.""" |
2695 | + msg = 'Entry must be left unmodified on focus out and then in again.' |
2696 | + expected = 'test me I mean it' |
2697 | + |
2698 | + self.grab_focus() # grab focus |
2699 | + |
2700 | + self.entry.set_text(expected) # add text to the entry |
2701 | + |
2702 | + self.grab_focus(focus_in=False) # loose focus |
2703 | + self.grab_focus() # grab focus again! |
2704 | + |
2705 | + self.assertEqual(expected, self.entry.get_text(), msg) |
2706 | + |
2707 | + def test_get_text_ignores_label(self): |
2708 | + """Entry's text is only user input (label is ignored).""" |
2709 | + self.assertEqual(self.entry.get_text(), '') |
2710 | + |
2711 | + def test_get_text_ignores_empty_input(self): |
2712 | + """Entry's text is only user input (empty text is ignored).""" |
2713 | + self.entry.set_text(' ') |
2714 | + self.assertEqual(self.entry.get_text(), '') |
2715 | + |
2716 | + def test_get_text_doesnt_ignore_user_input(self): |
2717 | + """Entry's text is user input.""" |
2718 | + self.entry.set_text('a') |
2719 | + self.assertEqual(self.entry.get_text(), 'a') |
2720 | + |
2721 | + def test_no_warning_by_default(self): |
2722 | + """No secondary icon by default.""" |
2723 | + self.assertEqual(self.entry.warning, None) |
2724 | + self.assertEqual(self.entry.get_property('secondary-icon-stock'), |
2725 | + None) |
2726 | + self.assertEqual(self.entry.get_property('secondary-icon-sensitive'), |
2727 | + False) |
2728 | + self.assertEqual(self.entry.get_property('secondary-icon-activatable'), |
2729 | + False) |
2730 | + prop = self.entry.get_property('secondary-icon-tooltip-text') |
2731 | + self.assertEqual(prop, None) |
2732 | + |
2733 | + def test_set_warning(self): |
2734 | + """Setting a warning show the proper secondary icon.""" |
2735 | + msg = 'You failed!' |
2736 | + self.entry.set_warning(msg) |
2737 | + self.assertEqual(self.entry.warning, msg) |
2738 | + self.assertEqual(self.entry.get_property('secondary-icon-stock'), |
2739 | + Gtk.STOCK_DIALOG_WARNING) |
2740 | + self.assertEqual(self.entry.get_property('secondary-icon-sensitive'), |
2741 | + True) |
2742 | + self.assertEqual(self.entry.get_property('secondary-icon-activatable'), |
2743 | + False) |
2744 | + prop = self.entry.get_property('secondary-icon-tooltip-text') |
2745 | + self.assertEqual(prop, msg) |
2746 | + |
2747 | + def test_clear_warning(self): |
2748 | + """Clearing a warning no longer show the secondary icon.""" |
2749 | + self.entry.clear_warning() |
2750 | + self.assertEqual(self.entry.warning, None) |
2751 | + self.assertEqual(self.entry.get_property('secondary-icon-stock'), |
2752 | + None) |
2753 | + self.assertEqual(self.entry.get_property('secondary-icon-sensitive'), |
2754 | + False) |
2755 | + self.assertEqual(self.entry.get_property('secondary-icon-activatable'), |
2756 | + False) |
2757 | + prop = self.entry.get_property('secondary-icon-tooltip-text') |
2758 | + self.assertEqual(prop, None) |
2759 | + |
2760 | + |
2761 | +class PasswordLabeledEntryTestCase(LabeledEntryTestCase): |
2762 | + """Test suite for the labeled entry when is_password is True.""" |
2763 | + |
2764 | + def setUp(self): |
2765 | + """Init.""" |
2766 | + super(PasswordLabeledEntryTestCase, self).setUp() |
2767 | + self.entry.is_password = True |
2768 | + |
2769 | + def test_password_fields_are_visible_at_startup(self): |
2770 | + """Password entrys show the helping text at startup.""" |
2771 | + self.assertTrue(self.entry.get_visibility(), |
2772 | + 'Password entry should be visible at start up.') |
2773 | + |
2774 | + def test_password_field_is_visible_if_no_input_and_focus_out(self): |
2775 | + """Password entry show the label when focus out.""" |
2776 | + self.grab_focus() # user cliked or TAB'd to the entry |
2777 | + self.grab_focus(focus_in=False) # loose focus |
2778 | + self.assertTrue(self.entry.get_visibility(), |
2779 | + 'Entry should be visible when focus out and no input.') |
2780 | + |
2781 | + def test_password_fields_are_not_visible_when_editing(self): |
2782 | + """Password entrys show the hidden chars instead of the password.""" |
2783 | + self.grab_focus() # user cliked or TAB'd to the entry |
2784 | + self.assertFalse(self.entry.get_visibility(), |
2785 | + 'Entry should not be visible when editing.') |
2786 | + |
2787 | + |
2788 | +class UbuntuSSOClientTestCase(BasicTestCase): |
2789 | + """Basic setup and helper functions.""" |
2790 | + |
2791 | + gui_class = gui.UbuntuSSOClientGUI |
2792 | + kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT) |
2793 | + |
2794 | + def setUp(self): |
2795 | + """Init.""" |
2796 | + super(UbuntuSSOClientTestCase, self).setUp() |
2797 | + self.patch(gui, 'get_sso_client', lambda: FakedSSOBackend()) |
2798 | + self.pages = ('enter_details', 'processing', 'verify_email', 'finish', |
2799 | + 'tc_browser', 'login', 'request_password_token', |
2800 | + 'set_new_password') |
2801 | + self.ui = self.gui_class(**self.kwargs) |
2802 | + self.addCleanup(self.ui.destroy) |
2803 | + self.error = {'message': UNKNOWN_ERROR} |
2804 | + |
2805 | + def assert_entries_are_packed_to_ui(self, container_name, entries): |
2806 | + """Every entry is properly packed in the ui 'container_name'.""" |
2807 | + msg = 'Entry %r must be packed in %r but is not.' |
2808 | + container = getattr(self.ui, container_name) |
2809 | + for kind in entries: |
2810 | + name = '%s_entry' % kind |
2811 | + entry = getattr(self.ui, name) |
2812 | + self.assertIsInstance(entry, gui.LabeledEntry) |
2813 | + self.assertIn(entry, container, msg % (name, container_name)) |
2814 | + |
2815 | + def assert_warnings_visibility(self, visible=False): |
2816 | + """Every warning label should be 'visible'.""" |
2817 | + msg = '%r should have %sempty content.' |
2818 | + for name in self.ui.widgets: |
2819 | + widget = getattr(self.ui, name) |
2820 | + if 'warning' in name: |
2821 | + self.assertEqual('', widget.get_text(), |
2822 | + msg % (name, '' if visible else 'non-')) |
2823 | + elif 'entry' in name: |
2824 | + self.assertEqual(widget.warning, '') |
2825 | + |
2826 | + def assert_correct_label_warning(self, label, message): |
2827 | + """Check that a warning is shown displaying 'message'.""" |
2828 | + # warning label is visible |
2829 | + self.assertTrue(label.get_property('visible')) |
2830 | + |
2831 | + # warning content is correct |
2832 | + actual = label.get_text().decode('utf-8') |
2833 | + self.assertEqual(actual, message) |
2834 | + |
2835 | + # content color is correct |
2836 | + # FIXME - New GTK+ 3.5 breaks this check - see bug #1014772 |
2837 | + # expected = gui.WARNING_TEXT_COLOR |
2838 | + # actual = label.get_style().fg[Gtk.StateFlags.NORMAL] |
2839 | + # self.assert_color_equal(expected, actual) |
2840 | + |
2841 | + def assert_correct_entry_warning(self, entry, message): |
2842 | + """Check that a warning is shown displaying 'message'.""" |
2843 | + self.assertEqual(entry.warning, message) |
2844 | + |
2845 | + def assert_pages_visibility(self, **kwargs): |
2846 | + """The page 'name' is the current page for the content notebook.""" |
2847 | + msg = 'page %r must be self.ui.content\'s current page.' |
2848 | + (name, _), = kwargs.items() |
2849 | + page = getattr(self.ui, '%s_vbox' % name) |
2850 | + self.assertEqual(self.ui.content.get_current_page(), |
2851 | + self.ui.content.page_num(page), msg % name) |
2852 | + |
2853 | + def click_join_with_valid_data(self): |
2854 | + """Move to the next page after entering details.""" |
2855 | + self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID) |
2856 | + |
2857 | + self.ui.name_entry.set_text(NAME) |
2858 | + # match emails |
2859 | + self.ui.email1_entry.set_text(EMAIL) |
2860 | + self.ui.email2_entry.set_text(EMAIL) |
2861 | + # match passwords |
2862 | + self.ui.password1_entry.set_text(PASSWORD) |
2863 | + self.ui.password2_entry.set_text(PASSWORD) |
2864 | + if self.ui.tc_url: |
2865 | + # agree to TC, only if the tc_url is defined, so we catch errors |
2866 | + self.ui.yes_to_tc_checkbutton.set_active(True) |
2867 | + # resolve captcha properly |
2868 | + self.ui.captcha_solution_entry.set_text(CAPTCHA_SOLUTION) |
2869 | + |
2870 | + self.ui.join_ok_button.clicked() |
2871 | + |
2872 | + def click_verify_email_with_valid_data(self): |
2873 | + """Move to the next page after entering email token.""" |
2874 | + self.click_join_with_valid_data() |
2875 | + |
2876 | + # resolve email token properly |
2877 | + self.ui.email_token_entry.set_text(EMAIL_TOKEN) |
2878 | + |
2879 | + self.ui.verify_token_button.clicked() |
2880 | + |
2881 | + def click_connect_with_valid_data(self): |
2882 | + """Move to the next page after entering login info.""" |
2883 | + # enter email |
2884 | + self.ui.login_email_entry.set_text(EMAIL) |
2885 | + # enter password |
2886 | + self.ui.login_password_entry.set_text(PASSWORD) |
2887 | + |
2888 | + self.ui.login_ok_button.clicked() |
2889 | + |
2890 | + def click_request_password_token_with_valid_data(self): |
2891 | + """Move to the next page after requesting for password reset token.""" |
2892 | + # enter email |
2893 | + self.ui.reset_email_entry.set_text(EMAIL) |
2894 | + |
2895 | + self.ui.request_password_token_ok_button.clicked() |
2896 | + |
2897 | + def click_set_new_password_with_valid_data(self): |
2898 | + """Move to the next page after resetting password.""" |
2899 | + # enter reset code |
2900 | + self.ui.reset_code_entry.set_text(RESET_PASSWORD_TOKEN) |
2901 | + # match passwords |
2902 | + self.ui.reset_password1_entry.set_text(PASSWORD) |
2903 | + self.ui.reset_password2_entry.set_text(PASSWORD) |
2904 | + |
2905 | + self.ui.set_new_password_ok_button.clicked() |
2906 | + |
2907 | + |
2908 | +class BasicUbuntuSSOClientTestCase(UbuntuSSOClientTestCase): |
2909 | + """Test suite for basic functionality.""" |
2910 | + |
2911 | + def test_main_window_is_visible_at_startup(self): |
2912 | + """The main window is shown at startup.""" |
2913 | + self.assertTrue(self.ui.window.get_property('visible')) |
2914 | + |
2915 | + def test_main_window_is_resizable(self): |
2916 | + """The main window can be resized.""" |
2917 | + self.assertTrue(self.ui.window.get_property('resizable')) |
2918 | + |
2919 | + def test_closing_main_window_calls_close_callback(self): |
2920 | + """The close_callback is called when closing the main window.""" |
2921 | + self.ui.close_callback = self._set_called |
2922 | + self.ui.on_close_clicked() |
2923 | + self.assertTrue(self._called, |
2924 | + 'close_callback was called when window was closed.') |
2925 | + |
2926 | + def test_close_callback_if_not_set(self): |
2927 | + """The close_callback is a no op if not set.""" |
2928 | + self.ui.on_close_clicked() |
2929 | + # no crash when close_callback is not set |
2930 | + |
2931 | + def test_app_name_is_stored(self): |
2932 | + """The app_name is stored for further use.""" |
2933 | + self.assertIn(APP_NAME, self.ui.app_name) |
2934 | + |
2935 | + def test_signals_are_removed(self): |
2936 | + """The hooked signals are removed at shutdown time.""" |
2937 | + assert len(self.ui.backend.callbacks) > 0 # at least one callback |
2938 | + |
2939 | + self.ui.on_close_clicked() |
2940 | + |
2941 | + self.assertEqual(self.ui.backend.callbacks, {}) |
2942 | + |
2943 | + def test_pages_are_packed_into_container(self): |
2944 | + """All the pages are packed in the main container.""" |
2945 | + children = self.ui.content.get_children() |
2946 | + for page_name in self.pages: |
2947 | + page = getattr(self.ui, '%s_vbox' % page_name) |
2948 | + self.assertIn(page, children) |
2949 | + |
2950 | + def test_initial_text_for_entries(self): |
2951 | + """Entries have the correct text at startup.""" |
2952 | + msg = 'Text for %r must be %r (got %r instead).' |
2953 | + for name in self.ui.entries: |
2954 | + entry = getattr(self.ui, name) |
2955 | + expected = getattr(gui.ui_strings, name.upper()) |
2956 | + actual = entry.label |
2957 | + # text content is correct |
2958 | + self.assertEqual(expected, actual, msg % (name, expected, actual)) |
2959 | + |
2960 | + def test_entries_activates_default(self): |
2961 | + """Entries have the activates default prop set.""" |
2962 | + msg = '%r must have activates_default set to True.' |
2963 | + for name in self.ui.entries: |
2964 | + entry = getattr(self.ui, name) |
2965 | + self.assertTrue(entry.get_activates_default(), msg % (name,)) |
2966 | + |
2967 | + def test_password_fields_are_password(self): |
2968 | + """Password fields have the is_password flag set.""" |
2969 | + msg = '%r should be a password LabeledEntry instance.' |
2970 | + passwords = filter(lambda name: 'password' in name, |
2971 | + self.ui.entries) |
2972 | + for name in passwords: |
2973 | + widget = getattr(self.ui, name) |
2974 | + self.assertTrue(widget.is_password, msg % name) |
2975 | + |
2976 | + def test_warning_fields_are_cleared(self): |
2977 | + """Every warning label should be cleared.""" |
2978 | + self.assert_warnings_visibility() |
2979 | + |
2980 | + def test_cancel_buttons_close_window(self): |
2981 | + """Every cancel button should close the window when clicked.""" |
2982 | + self.patch(self.ui.backend, 'disconnect_from_signal', lambda *a: None) |
2983 | + msg = '%r should close the window when clicked.' |
2984 | + buttons = filter(lambda name: 'cancel_button' in name or |
2985 | + 'close_button' in name, self.ui.widgets) |
2986 | + for name in buttons: |
2987 | + self.ui.close_callback = self._set_called |
2988 | + widget = getattr(self.ui, name) |
2989 | + widget.clicked() |
2990 | + self.assertEqual(self._called, ((widget,), {}), msg % name) |
2991 | + self._called = False |
2992 | + |
2993 | + def test_window_icon(self): |
2994 | + """Main window has the proper icon.""" |
2995 | + self.assertEqual('ubuntu-logo', self.ui.window.get_icon_name()) |
2996 | + |
2997 | + def test_finish_success_shows_success_page(self): |
2998 | + """When calling 'finish_success' the success page is shown.""" |
2999 | + self.ui.finish_success() |
3000 | + self.assert_pages_visibility(finish=True) |
3001 | + self.assertEqual(gui.SUCCESS % {'app_name': APP_NAME}, |
3002 | + self.ui.finish_vbox.label.get_text().decode('utf8')) |
3003 | + result = self.ui.finish_vbox.label.get_text().decode('utf8') |
3004 | + self.assertTrue(self.ui.app_name in result) |
3005 | + |
3006 | + def test_finish_error_shows_error_page(self): |
3007 | + """When calling 'finish_error' the error page is shown.""" |
3008 | + self.ui.finish_error() |
3009 | + self.assert_pages_visibility(finish=True) |
3010 | + self.assertEqual(gui.ERROR, |
3011 | + self.ui.finish_vbox.label.get_text().decode('utf8')) |
3012 | + |
3013 | + |
3014 | +@skip("Apparently, so far we can't use XLib dynamic bindings " |
3015 | + "to complete the call to X11Window.foreign_new_for_display.") |
3016 | +class SetTransientForTestCase(UbuntuSSOClientTestCase): |
3017 | + """Test suite for setting the window as transient for another one.""" |
3018 | + |
3019 | + def test_transient_window_is_none_if_window_id_is_zero(self): |
3020 | + """The transient window is correct.""" |
3021 | + self.patch(gui.X11Window, 'foreign_new_for_display', self._set_called) |
3022 | + ui = self.gui_class(window_id=0, **self.kwargs) |
3023 | + self.addCleanup(ui.destroy) |
3024 | + |
3025 | + self.assertFalse(self._called, 'set_transient_for must not be called.') |
3026 | + |
3027 | + def test_transient_window_is_correct(self): |
3028 | + """The transient window is correct.""" |
3029 | + xid = 5 |
3030 | + self.patch(gui.X11Window, 'foreign_new_for_display', self._set_called) |
3031 | + ui = self.gui_class(window_id=xid, **self.kwargs) |
3032 | + self.addCleanup(ui.destroy) |
3033 | + |
3034 | + self.assertTrue(self.memento.check(logging.ERROR, 'set_transient_for')) |
3035 | + self.assertTrue(self.memento.check(logging.ERROR, str(xid))) |
3036 | + self.assertEqual(self._called, ((xid,), {})) |
3037 | + |
3038 | + def test_transient_window_accepts_negative_id(self): |
3039 | + """The transient window accepts a negative window id.""" |
3040 | + xid = -5 |
3041 | + self.patch(gui.X11Window, 'foreign_new_for_display', self._set_called) |
3042 | + ui = self.gui_class(window_id=xid, **self.kwargs) |
3043 | + self.addCleanup(ui.destroy) |
3044 | + |
3045 | + self.assertEqual(self._called, ((xid,), {})) |
3046 | + |
3047 | + |
3048 | +class EnterDetailsTestCase(UbuntuSSOClientTestCase): |
3049 | + """Test suite for the user registration (enter details page).""" |
3050 | + |
3051 | + def test_initial_text_for_header_label(self): |
3052 | + """The header must have the correct text at startup.""" |
3053 | + msg = 'Text for the header must be %r (got %r instead).' |
3054 | + expected = gui.JOIN_HEADER_LABEL % {'app_name': APP_NAME} |
3055 | + actual = self.ui.header_label.get_text().decode('utf8') |
3056 | + # text content is correct |
3057 | + self.assertEqual(expected, actual, msg % (expected, actual)) |
3058 | + |
3059 | + def test_entries_are_packed_to_ui(self): |
3060 | + """Every entry is properly packed in the ui.""" |
3061 | + for kind in ('email', 'password'): |
3062 | + container_name = '%ss_hbox' % kind |
3063 | + entries = ('%s%s' % (kind, i) for i in xrange(1, 3)) |
3064 | + self.assert_entries_are_packed_to_ui(container_name, entries) |
3065 | + |
3066 | + self.assert_entries_are_packed_to_ui('enter_details_vbox', ('name',)) |
3067 | + self.assert_entries_are_packed_to_ui('captcha_solution_vbox', |
3068 | + ('captcha_solution',)) |
3069 | + self.assert_entries_are_packed_to_ui('verify_email_details_vbox', |
3070 | + ('email_token',)) |
3071 | + |
3072 | + def test_initial_texts_for_checkbuttons(self): |
3073 | + """Check buttons have the correct text at startup.""" |
3074 | + msg = 'Text for %r must be %r (got %r instead).' |
3075 | + expected = gui.YES_TO_UPDATES % {'app_name': APP_NAME} |
3076 | + actual = self.ui.yes_to_updates_checkbutton.get_label().decode('utf8') |
3077 | + self.assertEqual(expected, actual, msg % ('yes_to_updates_checkbutton', |
3078 | + expected, actual)) |
3079 | + expected = gui.YES_TO_TC % {'app_name': APP_NAME} |
3080 | + actual = self.ui.yes_to_tc_checkbutton.get_label().decode('utf8') |
3081 | + self.assertEqual(expected, actual, |
3082 | + msg % ('yes_to_tc_checkbutton', expected, actual)) |
3083 | + |
3084 | + def test_updates_checkbutton_is_checked_at_startup(self): |
3085 | + """The 'yes to updates' checkbutton is checked by default.""" |
3086 | + msg = '%r is checked by default.' |
3087 | + name = 'yes_to_updates_checkbutton' |
3088 | + widget = getattr(self.ui, name) |
3089 | + self.assertTrue(widget.get_active(), msg % name) |
3090 | + |
3091 | + def test_tc_checkbutton_is_not_checked_at_startup(self): |
3092 | + """The 'yes to T&C' checkbutton is not checked by default.""" |
3093 | + msg = '%r is checked by default.' |
3094 | + name = 'yes_to_tc_checkbutton' |
3095 | + widget = getattr(self.ui, name) |
3096 | + self.assertFalse(widget.get_active(), msg % name) |
3097 | + |
3098 | + def test_vboxes_visible_properties(self): |
3099 | + """Only 'enter_details' vbox is visible at start up.""" |
3100 | + self.assert_pages_visibility(enter_details=True) |
3101 | + |
3102 | + def test_join_ok_button_clicked(self): |
3103 | + """Clicking 'join_ok_button' sends info to backend using 'register'.""" |
3104 | + self.click_join_with_valid_data() |
3105 | + |
3106 | + # assert register_user was called |
3107 | + self.assert_backend_called('register_user', |
3108 | + APP_NAME, EMAIL, PASSWORD, NAME, CAPTCHA_ID, CAPTCHA_SOLUTION) |
3109 | + |
3110 | + def test_join_ok_button_clicked_morphs_to_processing_page(self): |
3111 | + """Clicking 'join_ok_button' presents the processing vbox.""" |
3112 | + self.click_join_with_valid_data() |
3113 | + self.assert_pages_visibility(processing=True) |
3114 | + |
3115 | + def test_processing_vbox_displays_an_active_spinner(self): |
3116 | + """When processing the registration, an active spinner is shown.""" |
3117 | + self.click_join_with_valid_data() |
3118 | + |
3119 | + self.assertTrue(self.ui.processing_vbox.get_property('visible'), |
3120 | + 'the processing box should be visible.') |
3121 | + |
3122 | + box = self.ui.processing_vbox.get_children()[0].get_children()[0] |
3123 | + self.assertEqual(2, len(box.get_children()), |
3124 | + 'processing_vbox must have two children.') |
3125 | + |
3126 | + spinner, label = box.get_children() |
3127 | + self.assertIsInstance(spinner, Gtk.Spinner) |
3128 | + self.assertIsInstance(label, Gtk.Label) |
3129 | + |
3130 | + self.assertTrue(spinner.get_property('visible'), |
3131 | + 'the processing spinner should be visible.') |
3132 | + self.assertTrue(spinner.get_property('active'), |
3133 | + 'the processing spinner should be active.') |
3134 | + self.assertTrue(label.get_property('visible'), |
3135 | + 'the processing label should be visible.') |
3136 | + self.assertEqual(label.get_text().decode('utf8'), |
3137 | + gui.ONE_MOMENT_PLEASE, |
3138 | + 'the processing label text must be correct.') |
3139 | + |
3140 | + def test_captcha_image_is_not_visible_at_startup(self): |
3141 | + """Captcha image is not shown at startup.""" |
3142 | + self.assertFalse(self.ui.captcha_image.get_property('visible'), |
3143 | + 'the captcha_image should not be visible.') |
3144 | + |
3145 | + def test_captcha_filename_is_different_each_time(self): |
3146 | + """The captcha image is different each time.""" |
3147 | + ui = self.gui_class(**self.kwargs) |
3148 | + self.addCleanup(ui.destroy) |
3149 | + |
3150 | + self.assertNotEqual(self.ui._captcha_filename, ui._captcha_filename) |
3151 | + |
3152 | + def test_captcha_image_is_removed_when_exiting(self): |
3153 | + """The captcha image is removed at shutdown time.""" |
3154 | + open(self.ui._captcha_filename, 'w').close() |
3155 | + assert os.path.exists(self.ui._captcha_filename) |
3156 | + self.ui.on_close_clicked() |
3157 | + |
3158 | + self.assertFalse(os.path.exists(self.ui._captcha_filename), |
3159 | + 'captcha image must be removed when exiting.') |
3160 | + |
3161 | + def test_captcha_image_is_a_spinner_at_first(self): |
3162 | + """Captcha image shows a spinner until the image is downloaded.""" |
3163 | + self.assertTrue(self.ui.captcha_loading.get_property('visible'), |
3164 | + 'the captcha_loading box should be visible.') |
3165 | + |
3166 | + box = self.ui.captcha_loading.get_children()[0].get_children()[0] |
3167 | + self.assertEqual(2, len(box.get_children()), |
3168 | + 'captcha_loading must have two children.') |
3169 | + |
3170 | + spinner, label = box.get_children() |
3171 | + self.assertIsInstance(spinner, Gtk.Spinner) |
3172 | + self.assertIsInstance(label, Gtk.Label) |
3173 | + |
3174 | + self.assertTrue(spinner.get_property('visible'), |
3175 | + 'the captcha_loading spinner should be visible.') |
3176 | + self.assertTrue(spinner.get_property('active'), |
3177 | + 'the captcha_loading spinner should be active.') |
3178 | + self.assertTrue(label.get_property('visible'), |
3179 | + 'the captcha_loading label should be visible.') |
3180 | + self.assertEqual(label.get_text().decode('utf8'), gui.LOADING, |
3181 | + 'the captcha_loading label text must be correct.') |
3182 | + |
3183 | + def test_join_ok_button_is_disabled_until_captcha_is_available(self): |
3184 | + """The join_ok_button is not sensitive until captcha is available.""" |
3185 | + self.assertFalse(self.ui.join_ok_button.is_sensitive()) |
3186 | + |
3187 | + def test_join_ok_button_is_enabled_when_captcha_is_available(self): |
3188 | + """The join_ok_button is sensitive when captcha is available.""" |
3189 | + self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID) |
3190 | + self.assertTrue(self.ui.join_ok_button.is_sensitive()) |
3191 | + |
3192 | + def test_captcha_loading_is_hid_when_captcha_is_available(self): |
3193 | + """The captcha_loading is hid when captcha is available.""" |
3194 | + self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID) |
3195 | + self.assertFalse(self.ui.captcha_loading.get_property('visible'), |
3196 | + 'captcha_loading is not visible.') |
3197 | + |
3198 | + def test_captcha_id_is_stored_when_captcha_is_available(self): |
3199 | + """The captcha_id is stored when captcha is available.""" |
3200 | + self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID) |
3201 | + self.assertEqual(CAPTCHA_ID, self.ui._captcha_id) |
3202 | + |
3203 | + def test_captcha_image_is_requested_as_startup(self): |
3204 | + """The captcha image is requested at startup.""" |
3205 | + # assert generate_captcha was called |
3206 | + self.assert_backend_called('generate_captcha', |
3207 | + APP_NAME, self.ui._captcha_filename) |
3208 | + |
3209 | + def test_captcha_is_shown_when_available(self): |
3210 | + """The captcha image is shown when available.""" |
3211 | + self.patch(self.ui.captcha_image, 'set_from_file', self._set_called) |
3212 | + self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID) |
3213 | + self.assertTrue(self.ui.captcha_image.get_property('visible')) |
3214 | + self.assertEqual(self._called, ((self.ui._captcha_filename,), {})) |
3215 | + |
3216 | + def test_on_captcha_generated_logs_captcha_id_when_none(self): |
3217 | + """If the captcha id is None, a warning is logged.""" |
3218 | + self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=None) |
3219 | + self.assertTrue(self.memento.check(logging.WARNING, repr(APP_NAME))) |
3220 | + self.assertTrue(self.memento.check(logging.WARNING, |
3221 | + 'captcha_id is None')) |
3222 | + |
3223 | + def test_captcha_reload_button_visible(self): |
3224 | + """The captcha reload button is initially visible.""" |
3225 | + self.assertTrue(self.ui.captcha_reload_button.get_visible(), |
3226 | + "The captcha button is not visible") |
3227 | + |
3228 | + def test_captcha_reload_button_reloads_captcha(self): |
3229 | + """The captcha reload button loads a new captcha.""" |
3230 | + self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID) |
3231 | + self.patch(self.ui, '_generate_captcha', self._set_called) |
3232 | + self.ui.captcha_reload_button.clicked() |
3233 | + self.assertEqual(self._called, ((), {})) |
3234 | + |
3235 | + def test_captcha_reload_button_has_tooltip(self): |
3236 | + """The captcha reload button has a tooltip.""" |
3237 | + self.assertEqual(self.ui.captcha_reload_button.get_tooltip_text(), |
3238 | + gui.CAPTCHA_RELOAD_TOOLTIP) |
3239 | + |
3240 | + def test_login_button_has_correct_wording(self): |
3241 | + """The sign in button has the proper wording.""" |
3242 | + actual = self.ui.login_button.get_label().decode('utf8') |
3243 | + self.assertEqual(gui.LOGIN_BUTTON_LABEL, actual) |
3244 | + |
3245 | + def test_join_ok_button_does_nothing_if_clicked_but_disabled(self): |
3246 | + """The join form can only be submitted if the button is sensitive.""" |
3247 | + self.patch(self.ui.email1_entry, 'get_text', self._set_called) |
3248 | + |
3249 | + self.ui.join_ok_button.set_sensitive(False) |
3250 | + self.ui.join_ok_button.clicked() |
3251 | + self.assertFalse(self._called) |
3252 | + |
3253 | + self.ui.join_ok_button.set_sensitive(True) |
3254 | + self.ui.join_ok_button.clicked() |
3255 | + self.assertTrue(self._called) |
3256 | + |
3257 | + def test_user_and_pass_are_cached(self): |
3258 | + """Username and password are temporarly cached for further use.""" |
3259 | + self.click_join_with_valid_data() |
3260 | + self.assertEqual(self.ui.user_email, EMAIL) |
3261 | + self.assertEqual(self.ui.user_password, PASSWORD) |
3262 | + |
3263 | + def test_on_captcha_generation_error(self): |
3264 | + """on_captcha_generation_error shows an error and reloads captcha.""" |
3265 | + self.patch(self.ui, '_generate_captcha', self._set_called) |
3266 | + self.ui.on_captcha_generation_error(APP_NAME, error=self.error) |
3267 | + self.assert_correct_label_warning(self.ui.warning_label, |
3268 | + gui.CAPTCHA_LOAD_ERROR) |
3269 | + self.assertEqual(self._called, ((), {})) |
3270 | + |
3271 | + def test_captcha_success_after_error(self): |
3272 | + """When captcha was retrieved after error, the warning is removed.""" |
3273 | + self.ui.on_captcha_generation_error(APP_NAME, error=self.error) |
3274 | + self.ui.on_captcha_generated(app_name=APP_NAME, captcha_id=CAPTCHA_ID) |
3275 | + self.assertEqual(self.ui.warning_label.get_text().decode('utf8'), '') |
3276 | + |
3277 | + def test_has_tc_link(self): |
3278 | + """The T&C button and checkbox are shown if the link is provided""" |
3279 | + self.assertEqual(self.ui.tc_button.get_visible(), True) |
3280 | + self.assertEqual(self.ui.yes_to_tc_checkbutton.get_visible(), True) |
3281 | + |
3282 | + |
3283 | +class NoTermsAndConditionsTestCase(EnterDetailsTestCase): |
3284 | + """Test suite for the user registration (with no t&c link).""" |
3285 | + |
3286 | + kwargs = dict(app_name=APP_NAME, tc_url='', help_text=HELP_TEXT) |
3287 | + |
3288 | + def test_has_tc_link(self): |
3289 | + """The T&C button and checkbox are not shown if no link is provided""" |
3290 | + self.assertEqual(self.ui.tc_vbox.get_visible(), False) |
3291 | + |
3292 | + |
3293 | +class TermsAndConditionsBrowserTestCase(UbuntuSSOClientTestCase): |
3294 | + """Test suite for the terms & conditions browser.""" |
3295 | + |
3296 | + def setUp(self): |
3297 | + super(TermsAndConditionsBrowserTestCase, self).setUp() |
3298 | + self.patch(WebKit, 'WebView', FakedEmbeddedBrowser) |
3299 | + self.patch(self.ui, '_webkit_init_ssl', self._set_called) |
3300 | + |
3301 | + self.ui.tc_button.clicked() |
3302 | + self.addCleanup(self.ui.tc_browser_vbox.hide) |
3303 | + |
3304 | + children = self.ui.tc_browser_window.get_children() |
3305 | + assert len(children) == 1 |
3306 | + self.browser = children[0] |
3307 | + |
3308 | + def test_ssl_validation(self): |
3309 | + """The browser is set to validate SSL.""" |
3310 | + self.assertEqual(self._called, ((), {}), |
3311 | + '_webkit_init_ssl should be called when creating a ' |
3312 | + 'webkit browser.') |
3313 | + |
3314 | + def test_tc_browser_is_created_when_tc_page_is_shown(self): |
3315 | + """The browser is created when the TC button is clicked.""" |
3316 | + self.ui.on_tc_browser_notify_load_status(self.browser) |
3317 | + |
3318 | + children = self.ui.tc_browser_window.get_children() |
3319 | + self.assertEqual(1, len(children)) |
3320 | + |
3321 | + def test_is_visible(self): |
3322 | + """The browser is visible.""" |
3323 | + self.assertIsInstance(self.browser, FakedEmbeddedBrowser) |
3324 | + self.assertTrue(self.browser.get_property('visible')) |
3325 | + |
3326 | + def test_settings(self): |
3327 | + """The browser settings are correct.""" |
3328 | + settings = self.browser.get_settings() |
3329 | + self.assertFalse(settings.get_property('enable-plugins')) |
3330 | + self.assertFalse(settings.get_property('enable-default-context-menu')) |
3331 | + |
3332 | + def test_tc_browser_is_destroyed_when_tc_page_is_hid(self): |
3333 | + """The browser is destroyed when the TC page is hid.""" |
3334 | + self.ui.on_tc_browser_notify_load_status(self.browser) |
3335 | + self.patch(self.browser, 'destroy', self._set_called) |
3336 | + self.ui.tc_browser_vbox.hide() |
3337 | + self.assertEqual(self._called, ((), {})) |
3338 | + |
3339 | + def test_tc_browser_is_removed_when_tc_page_is_hid(self): |
3340 | + """The browser is removed when the TC page is hid.""" |
3341 | + self.ui.on_tc_browser_notify_load_status(self.browser) |
3342 | + |
3343 | + self.ui.tc_browser_vbox.hide() |
3344 | + |
3345 | + children = self.ui.tc_browser_window.get_children() |
3346 | + self.assertEqual(0, len(children)) |
3347 | + |
3348 | + def test_tc_button_clicked_morphs_into_processing_page(self): |
3349 | + """Clicking the T&C button morphs into processing page.""" |
3350 | + self.assert_pages_visibility(processing=True) |
3351 | + |
3352 | + def test_tc_back_clicked_returns_to_previous_page(self): |
3353 | + """Terms & Conditions back button return to previous page.""" |
3354 | + self.ui.on_tc_browser_notify_load_status(self.browser) |
3355 | + self.ui.tc_back_button.clicked() |
3356 | + self.assert_pages_visibility(enter_details=True) |
3357 | + |
3358 | + def test_tc_button_has_the_proper_wording(self): |
3359 | + """Terms & Conditions has the proper wording.""" |
3360 | + self.assertEqual(self.ui.tc_button.get_label().decode('utf8'), |
3361 | + gui.TC_BUTTON) |
3362 | + |
3363 | + def test_tc_has_no_help_text(self): |
3364 | + """The help text is removed.""" |
3365 | + self.ui.on_tc_browser_notify_load_status(self.browser) |
3366 | + self.assertEqual('', self.ui.help_label.get_text().decode('utf8')) |
3367 | + |
3368 | + def test_tc_browser_opens_the_proper_url(self): |
3369 | + """Terms & Conditions browser shows the proper uri.""" |
3370 | + self.assertEqual(self.browser.get_property('uri'), TC_URL) |
3371 | + |
3372 | + @skip('Connecting to notify::load-status makes U1 terms navigation fail.') |
3373 | + def test_notify_load_status_connected(self): |
3374 | + """The 'notify::load-status' signal is connected.""" |
3375 | + expected = [self.ui.on_tc_browser_notify_load_status] |
3376 | + self.assertEqual(self.browser._signals['notify::load-status'], |
3377 | + expected) |
3378 | + |
3379 | + def test_notify_load_finished_connected(self): |
3380 | + """The 'load-finished' signal is connected.""" |
3381 | + expected = [self.ui.on_tc_browser_notify_load_status] |
3382 | + self.assertEqual(self.browser._signals['notify::load-status'], |
3383 | + expected) |
3384 | + |
3385 | + def test_tc_loaded_morphs_into_tc_browser_vbox(self): |
3386 | + """When the Terms & Conditions is loaded, show the browser window.""" |
3387 | + self.ui.on_tc_browser_notify_load_status(self.browser) |
3388 | + self.assert_pages_visibility(tc_browser=True) |
3389 | + |
3390 | + def test_navigation_requested_connected(self): |
3391 | + """The 'navigation-policy-decision-requested' signal is connected.""" |
3392 | + actual = self.browser._signals['navigation-policy-decision-requested'] |
3393 | + expected = [self.ui.on_tc_browser_navigation_requested] |
3394 | + self.assertEqual(actual, expected) |
3395 | + |
3396 | + def test_navigation_requested_succeeds_for_no_clicking(self): |
3397 | + """The navigation request succeeds when user hasn't clicked a link.""" |
3398 | + action = WebKit.WebNavigationAction() |
3399 | + action.set_reason(WebKit.WebNavigationReason.OTHER) |
3400 | + |
3401 | + decision = WebKit.WebPolicyDecision() |
3402 | + decision.use = self._set_called |
3403 | + |
3404 | + kwargs = dict(browser=self.browser, frame=None, request=None, |
3405 | + action=action, decision=decision) |
3406 | + self.ui.on_tc_browser_navigation_requested(**kwargs) |
3407 | + self.assertEqual(self._called, ((), {})) |
3408 | + |
3409 | + def test_navigation_requested_ignores_clicked_links(self): |
3410 | + """The navigation request is ignored if a link was clicked.""" |
3411 | + action = WebKit.WebNavigationAction() |
3412 | + action.set_reason(WebKit.WebNavigationReason.LINK_CLICKED) |
3413 | + |
3414 | + decision = WebKit.WebPolicyDecision() |
3415 | + decision.ignore = self._set_called |
3416 | + |
3417 | + self.patch(gui.webbrowser, 'open', lambda *args, **kwargs: None) |
3418 | + |
3419 | + kwargs = dict(browser=self.browser, frame=None, request=None, |
3420 | + action=action, decision=decision) |
3421 | + self.ui.on_tc_browser_navigation_requested(**kwargs) |
3422 | + self.assertEqual(self._called, ((), {})) |
3423 | + |
3424 | + def test_navigation_requested_ignores_for_none(self): |
3425 | + """The navigation request is ignoref the request if params are None.""" |
3426 | + kwargs = dict(browser=None, frame=None, request=None, |
3427 | + action=None, decision=None) |
3428 | + self.ui.on_tc_browser_navigation_requested(**kwargs) |
3429 | + |
3430 | + def test_navigation_requested_opens_links_when_clicked(self): |
3431 | + """The navigation request is opened on user's default browser |
3432 | + |
3433 | + (If the user opened a link by clicking into it). |
3434 | + |
3435 | + """ |
3436 | + url = 'http://something.com/yadda' |
3437 | + action = WebKit.WebNavigationAction() |
3438 | + action.set_reason(WebKit.WebNavigationReason.LINK_CLICKED) |
3439 | + action.set_original_uri(url) |
3440 | + |
3441 | + decision = WebKit.WebPolicyDecision() |
3442 | + decision.ignore = gui.NO_OP |
3443 | + |
3444 | + self.patch(gui.webbrowser, 'open', self._set_called) |
3445 | + |
3446 | + kwargs = dict(browser=self.browser, frame=None, request=None, |
3447 | + action=action, decision=decision) |
3448 | + self.ui.on_tc_browser_navigation_requested(**kwargs) |
3449 | + self.assertEqual(self._called, ((url,), {})) |
3450 | + |
3451 | + def test_on_tc_button_clicked_no_child(self): |
3452 | + """Test the tc loading with no child.""" |
3453 | + called = [] |
3454 | + |
3455 | + def fake_add_browser(): |
3456 | + """Fake add browser.""" |
3457 | + called.append('fake_add_browser') |
3458 | + |
3459 | + self.patch(self.ui, '_add_webkit_browser', fake_add_browser) |
3460 | + self.patch(self.ui.tc_browser_window, 'get_child', lambda: None) |
3461 | + |
3462 | + self.ui.on_tc_button_clicked() |
3463 | + self.assertIn('fake_add_browser', called) |
3464 | + |
3465 | + def test_on_tc_button_clicked_child(self): |
3466 | + """Test the tc loading with child.""" |
3467 | + called = [] |
3468 | + |
3469 | + def fake_add_browser(i_self): |
3470 | + """Fake add browser.""" |
3471 | + called.append('fake_add_browser') |
3472 | + |
3473 | + self.patch(self.ui, '_add_webkit_browser', fake_add_browser) |
3474 | + |
3475 | + browser = WebKit.WebView() |
3476 | + self.ui.tc_browser_window.add(browser) |
3477 | + self.ui.on_tc_button_clicked() |
3478 | + self.assertNotIn('fake_add_browser', called) |
3479 | + |
3480 | + |
3481 | +class RegistrationErrorTestCase(UbuntuSSOClientTestCase): |
3482 | + """Test suite for the user registration error handling.""" |
3483 | + |
3484 | + def setUp(self): |
3485 | + """Init.""" |
3486 | + super(RegistrationErrorTestCase, self).setUp() |
3487 | + self.click_join_with_valid_data() |
3488 | + |
3489 | + def test_previous_page_is_shown(self): |
3490 | + """On UserRegistrationError the previous page is shown.""" |
3491 | + self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error) |
3492 | + self.assert_pages_visibility(enter_details=True) |
3493 | + |
3494 | + def test_captcha_is_reloaded(self): |
3495 | + """On UserRegistrationError the captcha is reloaded.""" |
3496 | + self.patch(self.ui, '_generate_captcha', self._set_called) |
3497 | + self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error) |
3498 | + self.assertEqual(self._called, ((), {})) |
3499 | + |
3500 | + def test_warning_label_is_shown(self): |
3501 | + """On UserRegistrationError the warning label is shown.""" |
3502 | + self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error) |
3503 | + self.assert_correct_label_warning(self.ui.warning_label, |
3504 | + UNKNOWN_ERROR) |
3505 | + |
3506 | + def test_specific_errors_from_backend_are_shown(self): |
3507 | + """Specific errors from backend are used.""" |
3508 | + error = {'errtype': 'RegistrationError', |
3509 | + 'message': 'We\'re so doomed.', |
3510 | + 'email': 'Enter a valid e-mail address.', |
3511 | + 'password': 'I don\'t like your password.', |
3512 | + '__all__': 'Wrong captcha solution.'} |
3513 | + |
3514 | + self.ui.on_user_registration_error(app_name=APP_NAME, error=error) |
3515 | + |
3516 | + expected = '\n'.join((error['__all__'], error['message'])) |
3517 | + self.assert_correct_label_warning(self.ui.warning_label, expected) |
3518 | + self.assert_correct_entry_warning(self.ui.email1_entry, |
3519 | + error['email']) |
3520 | + self.assert_correct_entry_warning(self.ui.email2_entry, |
3521 | + error['email']) |
3522 | + self.assert_correct_entry_warning(self.ui.password1_entry, |
3523 | + error['password']) |
3524 | + self.assert_correct_entry_warning(self.ui.password2_entry, |
3525 | + error['password']) |
3526 | + |
3527 | + |
3528 | +class VerifyEmailTestCase(UbuntuSSOClientTestCase): |
3529 | + """Test suite for the user registration (verify email page).""" |
3530 | + |
3531 | + method = 'validate_email' |
3532 | + method_args = (APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN) |
3533 | + |
3534 | + def setUp(self): |
3535 | + """Init.""" |
3536 | + super(VerifyEmailTestCase, self).setUp() |
3537 | + self.ui.on_user_registered(app_name=APP_NAME, email=EMAIL) |
3538 | + |
3539 | + def test_registration_successful_shows_verify_email_vbox(self): |
3540 | + """Receiving 'registration_successful' shows the verify email vbox.""" |
3541 | + self.ui.on_user_registered(app_name=APP_NAME, email=EMAIL) |
3542 | + self.assert_pages_visibility(verify_email=True) |
3543 | + |
3544 | + def test_help_label_display_correct_wording(self): |
3545 | + """The help_label display VERIFY_EMAIL_LABEL.""" |
3546 | + msg = 'help_label must read %r (got %r instead).' |
3547 | + actual = self.ui.help_label.get_label().decode('utf8') |
3548 | + expected = gui.VERIFY_EMAIL_LABEL % {'app_name': APP_NAME, |
3549 | + 'email': EMAIL} |
3550 | + self.assertEqual(expected, actual, msg % (expected, actual)) |
3551 | + |
3552 | + def test_on_verify_token_button_clicked_calls_backend(self): |
3553 | + """Verify token button triggers call to backend.""" |
3554 | + self.click_verify_email_with_valid_data() |
3555 | + self.assert_backend_called(self.method, *self.method_args) |
3556 | + |
3557 | + def test_on_verify_token_button_clicked(self): |
3558 | + """Verify token uses cached user_email and user_password.""" |
3559 | + self.ui.user_email = 'test@me.com' |
3560 | + self.ui.user_password = 'yadda-yedda' |
3561 | + method_args = list(self.method_args) |
3562 | + method_args[1] = self.ui.user_email |
3563 | + method_args[2] = self.ui.user_password |
3564 | + |
3565 | + # resolve email token properly |
3566 | + self.ui.email_token_entry.set_text(EMAIL_TOKEN) |
3567 | + |
3568 | + self.ui.on_verify_token_button_clicked() |
3569 | + self.assert_backend_called(self.method, *tuple(method_args)) |
3570 | + |
3571 | + def test_on_verify_token_button_shows_processing_page(self): |
3572 | + """Verify token button triggers call to backend.""" |
3573 | + self.click_verify_email_with_valid_data() |
3574 | + self.assert_pages_visibility(processing=True) |
3575 | + |
3576 | + def test_no_warning_messages_if_valid_data(self): |
3577 | + """No warning messages are shown if the data is valid.""" |
3578 | + # this will certainly NOT generate warnings |
3579 | + self.click_verify_email_with_valid_data() |
3580 | + self.assert_warnings_visibility() |
3581 | + |
3582 | + def test_on_email_validated_shows_finish_page(self): |
3583 | + """On email validated the finish page is shown.""" |
3584 | + self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL) |
3585 | + self.assert_pages_visibility(finish=True) |
3586 | + |
3587 | + def test_on_email_validated_does_not_clear_the_help_text(self): |
3588 | + """On email validated the help text is not removed.""" |
3589 | + self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL) |
3590 | + self.assertEqual(self.ui.verify_email_vbox.help_text, |
3591 | + self.ui.help_label.get_label().decode('utf8')) |
3592 | + |
3593 | + def test_on_email_validation_error_verify_email_is_shown(self): |
3594 | + """On email validation error, the verify_email page is shown.""" |
3595 | + self.ui.on_email_validation_error(app_name=APP_NAME, error=self.error) |
3596 | + self.assert_pages_visibility(verify_email=True) |
3597 | + self.assert_correct_label_warning(self.ui.warning_label, |
3598 | + UNKNOWN_ERROR) |
3599 | + |
3600 | + def test_specific_errors_from_backend_are_shown(self): |
3601 | + """Specific errors from backend are used.""" |
3602 | + error = {'errtype': 'EmailValidationError', |
3603 | + 'message': 'We\'re so doomed.', |
3604 | + 'email_token': 'Enter a valid e-mail address.', |
3605 | + '__all__': 'We all are gonna die.'} |
3606 | + |
3607 | + self.ui.on_email_validation_error(app_name=APP_NAME, error=error) |
3608 | + |
3609 | + expected = '\n'.join((error['__all__'], error['message'])) |
3610 | + self.assert_correct_label_warning(self.ui.warning_label, expected) |
3611 | + self.assert_correct_entry_warning(self.ui.email_token_entry, |
3612 | + error['email_token']) |
3613 | + |
3614 | + def test_success_label_is_correct(self): |
3615 | + """The success message is correct.""" |
3616 | + self.assertEqual(gui.SUCCESS % {'app_name': APP_NAME}, |
3617 | + self.ui.success_vbox.label.get_text().decode('utf8')) |
3618 | + markup = self.ui.success_vbox.label.get_label().decode('utf8') |
3619 | + self.assertTrue('<span size="x-large">' in markup) |
3620 | + self.assertTrue(self.ui.app_name in markup) |
3621 | + |
3622 | + def test_error_label_is_correct(self): |
3623 | + """The error message is correct.""" |
3624 | + self.assertEqual(gui.ERROR, |
3625 | + self.ui.error_vbox.label.get_text().decode('utf8')) |
3626 | + markup = self.ui.error_vbox.label.get_label().decode('utf8') |
3627 | + self.assertTrue('<span size="x-large">' in markup) |
3628 | + |
3629 | + def test_on_finish_close_button_clicked_closes_window(self): |
3630 | + """When done the window is closed.""" |
3631 | + self.ui.finish_close_button.clicked() |
3632 | + self.assertFalse(self.ui.window.get_property('visible')) |
3633 | + |
3634 | + def test_verify_token_button_does_nothing_if_clicked_but_disabled(self): |
3635 | + """The email token can only be submitted if the button is sensitive.""" |
3636 | + self.patch(self.ui.email_token_entry, 'get_text', self._set_called) |
3637 | + |
3638 | + self.ui.verify_token_button.set_sensitive(False) |
3639 | + self.ui.verify_token_button.clicked() |
3640 | + self.assertFalse(self._called) |
3641 | + |
3642 | + self.ui.verify_token_button.set_sensitive(True) |
3643 | + self.ui.verify_token_button.clicked() |
3644 | + self.assertTrue(self._called) |
3645 | + |
3646 | + def test_after_email_validated_finish_success(self): |
3647 | + """After email_validated is called, finish_success is called.""" |
3648 | + self.patch(self.ui, 'finish_success', self._set_called) |
3649 | + |
3650 | + self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL) |
3651 | + |
3652 | + self.assertEqual(self._called, ((), {})) |
3653 | + |
3654 | + |
3655 | +class VerifyEmailWithPingTestCase(VerifyEmailTestCase): |
3656 | + """Test suite for the user registration (verify email page).""" |
3657 | + |
3658 | + kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT, |
3659 | + ping_url=PING_URL) |
3660 | + method = 'validate_email_and_ping' |
3661 | + method_args = (APP_NAME, EMAIL, PASSWORD, EMAIL_TOKEN, PING_URL) |
3662 | + |
3663 | + |
3664 | +class VerifyEmailValidationTestCase(UbuntuSSOClientTestCase): |
3665 | + """Test suite for the user registration validation (verify email page).""" |
3666 | + |
3667 | + def setUp(self): |
3668 | + """Init.""" |
3669 | + super(VerifyEmailValidationTestCase, self).setUp() |
3670 | + self.ui.on_user_registered(app_name=APP_NAME, email=EMAIL) |
3671 | + |
3672 | + def test_warning_is_shown_if_empty_email_token(self): |
3673 | + """A warning message is shown if email token is empty.""" |
3674 | + self.ui.email_token_entry.set_text('') |
3675 | + |
3676 | + self.ui.verify_token_button.clicked() |
3677 | + |
3678 | + self.assert_correct_entry_warning(self.ui.email_token_entry, |
3679 | + gui.FIELD_REQUIRED) |
3680 | + self.assertNotIn('validate_email', self.ui.backend._called) |
3681 | + |
3682 | + def test_no_warning_messages_if_valid_data(self): |
3683 | + """No warning messages are shown if the data is valid.""" |
3684 | + # this will certainly NOT generate warnings |
3685 | + self.click_verify_email_with_valid_data() |
3686 | + |
3687 | + self.assert_warnings_visibility() |
3688 | + |
3689 | + def test_no_warning_messages_if_valid_data_after_invalid_data(self): |
3690 | + """No warnings if the data is valid (with prior invalid data).""" |
3691 | + # this will certainly generate warnings |
3692 | + self.ui.verify_token_button.clicked() |
3693 | + |
3694 | + # this will certainly NOT generate warnings |
3695 | + self.click_verify_email_with_valid_data() |
3696 | + |
3697 | + self.assert_warnings_visibility() |
3698 | + |
3699 | + |
3700 | +class VerifyEmailLoginOnlyTestCase(VerifyEmailTestCase): |
3701 | + """Test suite for the user login (verify email page).""" |
3702 | + |
3703 | + kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT, |
3704 | + login_only=True) |
3705 | + |
3706 | + |
3707 | +class VerifyEmailValidationLoginOnlyTestCase(VerifyEmailValidationTestCase): |
3708 | + """Test suite for the user login validation (verify email page).""" |
3709 | + |
3710 | + kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT, |
3711 | + login_only=True) |
3712 | + |
3713 | + |
3714 | +class RegistrationValidationTestCase(UbuntuSSOClientTestCase): |
3715 | + """Test suite for the user registration validations.""" |
3716 | + |
3717 | + def setUp(self): |
3718 | + """Init.""" |
3719 | + super(RegistrationValidationTestCase, self).setUp() |
3720 | + self.ui.join_ok_button.set_sensitive(True) |
3721 | + |
3722 | + def test_warning_is_shown_if_name_empty(self): |
3723 | + """A warning message is shown if name is empty.""" |
3724 | + self.ui.name_entry.set_text('') |
3725 | + |
3726 | + self.ui.join_ok_button.clicked() |
3727 | + |
3728 | + self.assert_correct_entry_warning(self.ui.name_entry, |
3729 | + gui.FIELD_REQUIRED) |
3730 | + self.assertNotIn('register_user', self.ui.backend._called) |
3731 | + |
3732 | + def test_warning_is_shown_if_empty_email(self): |
3733 | + """A warning message is shown if emails are empty.""" |
3734 | + self.ui.email1_entry.set_text('') |
3735 | + self.ui.email2_entry.set_text('') |
3736 | + |
3737 | + self.ui.join_ok_button.clicked() |
3738 | + |
3739 | + self.assert_correct_entry_warning(self.ui.email1_entry, |
3740 | + gui.FIELD_REQUIRED) |
3741 | + self.assert_correct_entry_warning(self.ui.email2_entry, |
3742 | + gui.FIELD_REQUIRED) |
3743 | + self.assertNotIn('register_user', self.ui.backend._called) |
3744 | + |
3745 | + def test_warning_is_shown_if_email_mismatch(self): |
3746 | + """A warning message is shown if emails doesn't match.""" |
3747 | + self.ui.email1_entry.set_text(EMAIL) |
3748 | + self.ui.email2_entry.set_text(EMAIL * 2) |
3749 | + |
3750 | + self.ui.join_ok_button.clicked() |
3751 | + |
3752 | + self.assert_correct_entry_warning(self.ui.email1_entry, |
3753 | + gui.EMAIL_MISMATCH) |
3754 | + self.assert_correct_entry_warning(self.ui.email2_entry, |
3755 | + gui.EMAIL_MISMATCH) |
3756 | + self.assertNotIn('register_user', self.ui.backend._called) |
3757 | + |
3758 | + def test_warning_is_shown_if_invalid_email(self): |
3759 | + """A warning message is shown if email is invalid.""" |
3760 | + self.ui.email1_entry.set_text('q') |
3761 | + self.ui.email2_entry.set_text('q') |
3762 | + |
3763 | + self.ui.join_ok_button.clicked() |
3764 | + |
3765 | + self.assert_correct_entry_warning(self.ui.email1_entry, |
3766 | + gui.EMAIL_INVALID) |
3767 | + self.assert_correct_entry_warning(self.ui.email2_entry, |
3768 | + gui.EMAIL_INVALID) |
3769 | + self.assertNotIn('register_user', self.ui.backend._called) |
3770 | + |
3771 | + def test_password_help_is_always_shown(self): |
3772 | + """Password help text is correctly displayed.""" |
3773 | + self.assertTrue(self.ui.password_help_label.get_property('visible'), |
3774 | + 'password help text is visible.') |
3775 | + self.assertEqual(self.ui.password_help_label.get_text().decode('utf8'), |
3776 | + gui.PASSWORD_HELP) |
3777 | + self.assertNotIn('register_user', self.ui.backend._called) |
3778 | + |
3779 | + def test_warning_is_shown_if_password_mismatch(self): |
3780 | + """A warning message is shown if password doesn't match.""" |
3781 | + self.ui.password1_entry.set_text(PASSWORD) |
3782 | + self.ui.password2_entry.set_text(PASSWORD * 2) |
3783 | + |
3784 | + self.ui.join_ok_button.clicked() |
3785 | + |
3786 | + self.assert_correct_entry_warning(self.ui.password1_entry, |
3787 | + gui.PASSWORD_MISMATCH) |
3788 | + self.assert_correct_entry_warning(self.ui.password2_entry, |
3789 | + gui.PASSWORD_MISMATCH) |
3790 | + self.assertNotIn('register_user', self.ui.backend._called) |
3791 | + |
3792 | + def test_warning_is_shown_if_password_too_weak(self): |
3793 | + """A warning message is shown if password is too weak.""" |
3794 | + # password will match but will be too weak |
3795 | + for pwd in ('', 'h3lloWo', PASSWORD.lower(), 'helloWorld'): |
3796 | + self.ui.password1_entry.set_text(pwd) |
3797 | + self.ui.password2_entry.set_text(pwd) |
3798 | + |
3799 | + self.ui.join_ok_button.clicked() |
3800 | + |
3801 | + self.assert_correct_entry_warning(self.ui.password1_entry, |
3802 | + gui.PASSWORD_TOO_WEAK) |
3803 | + self.assert_correct_entry_warning(self.ui.password2_entry, |
3804 | + gui.PASSWORD_TOO_WEAK) |
3805 | + self.assertNotIn('register_user', self.ui.backend._called) |
3806 | + |
3807 | + def test_warning_is_shown_if_tc_not_accepted(self): |
3808 | + """A warning message is shown if TC are not accepted.""" |
3809 | + # don't agree to TC |
3810 | + self.ui.yes_to_tc_checkbutton.set_active(False) |
3811 | + |
3812 | + self.ui.join_ok_button.clicked() |
3813 | + |
3814 | + self.assert_correct_label_warning(self.ui.tc_warning_label, |
3815 | + gui.TC_NOT_ACCEPTED % {'app_name': APP_NAME}) |
3816 | + self.assertNotIn('register_user', self.ui.backend._called) |
3817 | + |
3818 | + def test_warning_is_shown_if_not_captcha_solution(self): |
3819 | + """A warning message is shown if TC are not accepted.""" |
3820 | + # captcha solution will be empty |
3821 | + self.ui.captcha_solution_entry.set_text('') |
3822 | + |
3823 | + self.ui.join_ok_button.clicked() |
3824 | + |
3825 | + self.assert_correct_entry_warning(self.ui.captcha_solution_entry, |
3826 | + gui.FIELD_REQUIRED) |
3827 | + self.assertNotIn('register_user', self.ui.backend._called) |
3828 | + |
3829 | + def test_no_warning_messages_if_valid_data(self): |
3830 | + """No warning messages are shown if the data is valid.""" |
3831 | + # this will certainly NOT generate warnings |
3832 | + self.click_join_with_valid_data() |
3833 | + |
3834 | + self.assert_warnings_visibility() |
3835 | + |
3836 | + def test_no_warning_messages_if_valid_data_after_invalid_data(self): |
3837 | + """No warnings if the data is valid (with prior invalid data).""" |
3838 | + # this will certainly generate warnings |
3839 | + self.ui.join_ok_button.clicked() |
3840 | + |
3841 | + # this will certainly NOT generate warnings |
3842 | + self.click_join_with_valid_data() |
3843 | + |
3844 | + self.assert_warnings_visibility() |
3845 | + |
3846 | + |
3847 | +class LoginTestCase(UbuntuSSOClientTestCase): |
3848 | + """Test suite for the user login pages.""" |
3849 | + |
3850 | + method = 'login' |
3851 | + method_args = (APP_NAME, EMAIL, PASSWORD) |
3852 | + |
3853 | + def setUp(self): |
3854 | + """Init.""" |
3855 | + super(LoginTestCase, self).setUp() |
3856 | + self.ui.login_button.clicked() |
3857 | + |
3858 | + def test_login_button_clicked_morphs_to_login_page(self): |
3859 | + """Clicking sig_in_button morphs window into login page.""" |
3860 | + self.assert_pages_visibility(login=True) |
3861 | + |
3862 | + def test_initial_text_for_header_label(self): |
3863 | + """The header must have the correct text when logging in.""" |
3864 | + msg = 'Text for the header must be %r (got %r instead).' |
3865 | + expected = gui.LOGIN_HEADER_LABEL % {'app_name': APP_NAME} |
3866 | + actual = self.ui.header_label.get_text().decode('utf8') |
3867 | + self.assertEqual(expected, actual, msg % (expected, actual)) |
3868 | + |
3869 | + def test_initial_text_for_help_label(self): |
3870 | + """The help must have the correct text at startup.""" |
3871 | + msg = 'Text for the help must be %r (got %r instead).' |
3872 | + expected = gui.CONNECT_HELP_LABEL % {'app_name': APP_NAME} |
3873 | + actual = self.ui.help_label.get_text().decode('utf8') |
3874 | + self.assertEqual(expected, actual, msg % (expected, actual)) |
3875 | + |
3876 | + def test_entries_are_packed_to_ui_for_login(self): |
3877 | + """Every entry is properly packed in the ui for the login page.""" |
3878 | + entries = ('login_email', 'login_password') |
3879 | + self.assert_entries_are_packed_to_ui('login_details_vbox', entries) |
3880 | + |
3881 | + def test_entries_are_packed_to_ui_for_set_new_password(self): |
3882 | + """Every entry is packed in the ui for the reset password page.""" |
3883 | + entries = ('reset_code', 'reset_password1', 'reset_password2') |
3884 | + self.assert_entries_are_packed_to_ui('set_new_password_details_vbox', |
3885 | + entries) |
3886 | + |
3887 | + def test_entries_are_packed_to_ui_for_request_password_token(self): |
3888 | + """Every entry is packed in the ui for the reset email page.""" |
3889 | + container_name = 'request_password_token_details_vbox' |
3890 | + entries = ('reset_email',) |
3891 | + self.assert_entries_are_packed_to_ui(container_name, entries) |
3892 | + |
3893 | + def test_on_login_back_button_clicked(self): |
3894 | + """Clicking login_back_button show registration page.""" |
3895 | + self.ui.login_back_button.clicked() |
3896 | + self.assert_pages_visibility(enter_details=True) |
3897 | + |
3898 | + def test_on_login_connect_button_clicked(self): |
3899 | + """Clicking login_ok_button calls backend.login.""" |
3900 | + self.click_connect_with_valid_data() |
3901 | + self.assert_backend_called(self.method, *self.method_args) |
3902 | + |
3903 | + def test_on_login_connect_button_clicked_morphs_to_processing_page(self): |
3904 | + """Clicking login_ok_button morphs to the processing page.""" |
3905 | + self.click_connect_with_valid_data() |
3906 | + self.assert_pages_visibility(processing=True) |
3907 | + |
3908 | + def test_on_logged_in_morphs_to_finish_page(self): |
3909 | + """When user logged in the finish page is shown.""" |
3910 | + self.click_connect_with_valid_data() |
3911 | + self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL) |
3912 | + self.assert_pages_visibility(finish=True) |
3913 | + |
3914 | + def test_on_login_error_morphs_to_login_page(self): |
3915 | + """On user login error, the previous page is shown.""" |
3916 | + self.click_connect_with_valid_data() |
3917 | + self.ui.on_login_error(app_name=APP_NAME, error=self.error) |
3918 | + self.assert_pages_visibility(login=True) |
3919 | + |
3920 | + def test_on_user_not_validated_morphs_to_verify_page(self): |
3921 | + """On user not validated, the verify page is shown.""" |
3922 | + self.click_connect_with_valid_data() |
3923 | + self.ui.on_user_not_validated(app_name=APP_NAME, email=EMAIL) |
3924 | + self.assert_pages_visibility(verify_email=True) |
3925 | + |
3926 | + def test_on_login_error_a_warning_is_shown(self): |
3927 | + """On user login error, a warning is shown with proper wording.""" |
3928 | + self.click_connect_with_valid_data() |
3929 | + self.ui.on_login_error(app_name=APP_NAME, error=self.error) |
3930 | + self.assert_correct_label_warning(self.ui.warning_label, |
3931 | + UNKNOWN_ERROR) |
3932 | + |
3933 | + def test_specific_errors_from_backend_are_shown(self): |
3934 | + """Specific errors from backend are used.""" |
3935 | + error = {'errtype': 'AuthenticationError', |
3936 | + 'message': 'We\'re so doomed.', |
3937 | + '__all__': 'We all are gonna die.'} |
3938 | + |
3939 | + self.ui.on_login_error(app_name=APP_NAME, error=error) |
3940 | + |
3941 | + expected = '\n'.join((error['__all__'], error['message'])) |
3942 | + self.assert_correct_label_warning(self.ui.warning_label, expected) |
3943 | + |
3944 | + def test_back_to_registration_hides_warning(self): |
3945 | + """After user login error, warning is hidden when clicking 'Back'.""" |
3946 | + self.click_connect_with_valid_data() |
3947 | + self.ui.on_login_error(app_name=APP_NAME, error=self.error) |
3948 | + self.ui.login_back_button.clicked() |
3949 | + self.assert_warnings_visibility() |
3950 | + |
3951 | + def test_login_ok_button_does_nothing_if_clicked_but_disabled(self): |
3952 | + """The join form can only be submitted if the button is sensitive.""" |
3953 | + self.patch(self.ui.login_email_entry, 'get_text', self._set_called) |
3954 | + |
3955 | + self.ui.login_ok_button.set_sensitive(False) |
3956 | + self.ui.login_ok_button.clicked() |
3957 | + self.assertFalse(self._called) |
3958 | + |
3959 | + self.ui.login_ok_button.set_sensitive(True) |
3960 | + self.ui.login_ok_button.clicked() |
3961 | + self.assertTrue(self._called) |
3962 | + |
3963 | + def test_user_and_pass_are_cached(self): |
3964 | + """Username and password are temporarly cached for further use.""" |
3965 | + self.click_connect_with_valid_data() |
3966 | + self.assertEqual(self.ui.user_email, EMAIL) |
3967 | + self.assertEqual(self.ui.user_password, PASSWORD) |
3968 | + |
3969 | + def test_after_login_success_finish_success(self): |
3970 | + """After logged_in is called, finish_success is called.""" |
3971 | + self.patch(self.ui, 'finish_success', self._set_called) |
3972 | + |
3973 | + self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL) |
3974 | + |
3975 | + self.assertEqual(self._called, ((), {})) |
3976 | + |
3977 | + |
3978 | +class LoginWithPingTestCase(LoginTestCase): |
3979 | + """Test suite for the login when the ping_url is set.""" |
3980 | + |
3981 | + kwargs = dict(app_name=APP_NAME, tc_url=TC_URL, help_text=HELP_TEXT, |
3982 | + ping_url=PING_URL) |
3983 | + method = 'login_and_ping' |
3984 | + method_args = (APP_NAME, EMAIL, PASSWORD, PING_URL) |
3985 | + |
3986 | + |
3987 | +class LoginValidationTestCase(UbuntuSSOClientTestCase): |
3988 | + """Test suite for the user login validation.""" |
3989 | + |
3990 | + def setUp(self): |
3991 | + """Init.""" |
3992 | + super(LoginValidationTestCase, self).setUp() |
3993 | + self.ui.login_button.clicked() |
3994 | + |
3995 | + def test_warning_is_shown_if_empty_email(self): |
3996 | + """A warning message is shown if email is empty.""" |
3997 | + self.ui.login_email_entry.set_text('') |
3998 | + |
3999 | + self.ui.login_ok_button.clicked() |
4000 | + |
4001 | + self.assert_correct_entry_warning(self.ui.login_email_entry, |
4002 | + gui.FIELD_REQUIRED) |
4003 | + self.assertNotIn('login', self.ui.backend._called) |
4004 | + |
4005 | + def test_warning_is_shown_if_invalid_email(self): |
4006 | + """A warning message is shown if email is invalid.""" |
4007 | + self.ui.login_email_entry.set_text('q') |
4008 | + |
4009 | + self.ui.login_ok_button.clicked() |
4010 | + |
4011 | + self.assert_correct_entry_warning(self.ui.login_email_entry, |
4012 | + gui.EMAIL_INVALID) |
4013 | + self.assertNotIn('login', self.ui.backend._called) |
4014 | + |
4015 | + def test_warning_is_shown_if_empty_password(self): |
4016 | + """A warning message is shown if password is empty.""" |
4017 | + self.ui.login_password_entry.set_text('') |
4018 | + |
4019 | + self.ui.login_ok_button.clicked() |
4020 | + |
4021 | + self.assert_correct_entry_warning(self.ui.login_password_entry, |
4022 | + gui.FIELD_REQUIRED) |
4023 | + self.assertNotIn('login', self.ui.backend._called) |
4024 | + |
4025 | + def test_no_warning_messages_if_valid_data(self): |
4026 | + """No warning messages are shown if the data is valid.""" |
4027 | + # this will certainly NOT generate warnings |
4028 | + self.click_connect_with_valid_data() |
4029 | + |
4030 | + self.assert_warnings_visibility() |
4031 | + |
4032 | + def test_no_warning_messages_if_valid_data_after_invalid_data(self): |
4033 | + """No warnings if the data is valid (with prior invalid data).""" |
4034 | + # this will certainly generate warnings |
4035 | + self.ui.login_ok_button.clicked() |
4036 | + |
4037 | + # this will certainly NOT generate warnings |
4038 | + self.click_connect_with_valid_data() |
4039 | + |
4040 | + self.assert_warnings_visibility() |
4041 | + |
4042 | + |
4043 | +class ResetPasswordTestCase(UbuntuSSOClientTestCase): |
4044 | + """Test suite for the reset password functionality.""" |
4045 | + |
4046 | + def setUp(self): |
4047 | + """Init.""" |
4048 | + super(ResetPasswordTestCase, self).setUp() |
4049 | + self.ui.login_button.clicked() |
4050 | + self.ui.forgotten_password_button.clicked() |
4051 | + |
4052 | + def test_forgotten_password_button_has_the_proper_wording(self): |
4053 | + """The forgotten_password_button has the proper wording.""" |
4054 | + actual = self.ui.forgotten_password_button.get_label() |
4055 | + self.assertEqual(actual.decode('utf8'), gui.FORGOTTEN_PASSWORD_BUTTON) |
4056 | + |
4057 | + def test_on_forgotten_password_button_clicked_help_text(self): |
4058 | + """Clicking forgotten_password_button the help is properly changed.""" |
4059 | + wanted = gui.REQUEST_PASSWORD_TOKEN_LABEL % {'app_name': APP_NAME} |
4060 | + self.assertEqual(self.ui.help_label.get_text().decode('utf8'), wanted) |
4061 | + |
4062 | + def test_on_forgotten_password_button_clicked_header_label(self): |
4063 | + """Clicking forgotten_password_button the title is properly changed.""" |
4064 | + self.assertEqual(self.ui.header_label.get_text().decode('utf8'), |
4065 | + gui.RESET_PASSWORD) |
4066 | + |
4067 | + def test_on_forgotten_password_button_clicked_ok_button(self): |
4068 | + """Clicking forgotten_password_button the ok button reads 'Next'.""" |
4069 | + actual = self.ui.request_password_token_ok_button.get_label() |
4070 | + self.assertEqual(actual.decode('utf8'), gui.NEXT) |
4071 | + |
4072 | + def test_on_forgotten_password_button_clicked_morphs_window(self): |
4073 | + """Clicking forgotten_password_button the proper page is shown.""" |
4074 | + self.assert_pages_visibility(request_password_token=True) |
4075 | + |
4076 | + def test_on_request_password_token_back_button_clicked(self): |
4077 | + """Clicking request_password_token_back_button show login screen.""" |
4078 | + self.ui.request_password_token_back_button.clicked() |
4079 | + self.assert_pages_visibility(login=True) |
4080 | + |
4081 | + def test_request_password_token_ok_button_disabled_until_email_added(self): |
4082 | + """The button is disabled until email added.""" |
4083 | + is_sensitive = self.ui.request_password_token_ok_button.get_sensitive |
4084 | + self.assertFalse(is_sensitive()) |
4085 | + |
4086 | + self.ui.reset_email_entry.set_text('a') |
4087 | + self.assertTrue(is_sensitive()) |
4088 | + |
4089 | + self.ui.reset_email_entry.set_text('') |
4090 | + self.assertFalse(is_sensitive()) |
4091 | + |
4092 | + self.ui.reset_email_entry.set_text(' ') |
4093 | + self.assertFalse(is_sensitive()) |
4094 | + |
4095 | + def test_on_request_password_token_ok_button_clicked_morphs_window(self): |
4096 | + """Clicking request_password_token_ok_button morphs processing page.""" |
4097 | + self.click_request_password_token_with_valid_data() |
4098 | + self.assert_pages_visibility(processing=True) |
4099 | + |
4100 | + def test_on_request_password_token_ok_button_clicked_calls_backend(self): |
4101 | + """Clicking request_password_token_ok_button the backend is called.""" |
4102 | + self.click_request_password_token_with_valid_data() |
4103 | + self.assert_backend_called('request_password_reset_token', |
4104 | + APP_NAME, EMAIL) |
4105 | + |
4106 | + def test_on_password_reset_token_sent_morphs_window(self): |
4107 | + """When the reset token was sent, the reset password page is shown.""" |
4108 | + self.click_request_password_token_with_valid_data() |
4109 | + self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL) |
4110 | + self.assert_pages_visibility(set_new_password=True) |
4111 | + |
4112 | + def test_on_password_reset_token_sent_help_text(self): |
4113 | + """Clicking request_password_token_ok_button changes the help text.""" |
4114 | + self.click_request_password_token_with_valid_data() |
4115 | + self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL) |
4116 | + |
4117 | + self.assertEqual(self.ui.help_label.get_text().decode('utf8'), |
4118 | + gui.SET_NEW_PASSWORD_LABEL % {'email': EMAIL}) |
4119 | + |
4120 | + def test_on_password_reset_token_sent_ok_button(self): |
4121 | + """After request_password_token_ok_button the ok button is updated.""" |
4122 | + self.click_request_password_token_with_valid_data() |
4123 | + self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL) |
4124 | + |
4125 | + actual = self.ui.set_new_password_ok_button.get_label() |
4126 | + self.assertEqual(actual.decode('utf8'), gui.RESET_PASSWORD) |
4127 | + |
4128 | + def test_on_password_reset_error_shows_login_page(self): |
4129 | + """When reset token wasn't successfuly sent the login page is shown.""" |
4130 | + self.ui.on_password_reset_error(app_name=APP_NAME, error=self.error) |
4131 | + self.assert_correct_label_warning(self.ui.warning_label, |
4132 | + UNKNOWN_ERROR) |
4133 | + self.assert_pages_visibility(login=True) |
4134 | + |
4135 | + def test_specific_errors_from_backend_are_shown(self): |
4136 | + """Specific errors from backend are used.""" |
4137 | + error = {'errtype': 'ResetPasswordTokenError', |
4138 | + 'message': 'We\'re so doomed.', |
4139 | + '__all__': 'We all are gonna die.'} |
4140 | + |
4141 | + self.ui.on_password_reset_error(app_name=APP_NAME, error=error) |
4142 | + |
4143 | + expected = '\n'.join((error['__all__'], error['message'])) |
4144 | + self.assert_correct_label_warning(self.ui.warning_label, expected) |
4145 | + |
4146 | + def test_ok_button_does_nothing_if_clicked_but_disabled(self): |
4147 | + """The password token can be requested if the button is sensitive.""" |
4148 | + self.patch(self.ui.reset_email_entry, 'get_text', self._set_called) |
4149 | + |
4150 | + self.ui.request_password_token_ok_button.set_sensitive(False) |
4151 | + self.ui.request_password_token_ok_button.clicked() |
4152 | + self.assertFalse(self._called) |
4153 | + |
4154 | + self.ui.request_password_token_ok_button.set_sensitive(True) |
4155 | + self.ui.request_password_token_ok_button.clicked() |
4156 | + self.assertTrue(self._called) |
4157 | + |
4158 | + |
4159 | +class ResetPasswordValidationTestCase(UbuntuSSOClientTestCase): |
4160 | + """Test suite for the password reset validations.""" |
4161 | + |
4162 | + def test_warning_is_shown_if_empty_email(self): |
4163 | + """A warning message is shown if emails are empty.""" |
4164 | + self.ui.reset_email_entry.set_text(' ') |
4165 | + |
4166 | + self.ui.request_password_token_ok_button.set_sensitive(True) |
4167 | + self.ui.request_password_token_ok_button.clicked() |
4168 | + |
4169 | + self.assert_correct_entry_warning(self.ui.reset_email_entry, |
4170 | + gui.FIELD_REQUIRED) |
4171 | + self.assertNotIn('request_password_reset_token', |
4172 | + self.ui.backend._called) |
4173 | + |
4174 | + def test_warning_is_shown_if_invalid_email(self): |
4175 | + """A warning message is shown if email is invalid.""" |
4176 | + self.ui.reset_email_entry.set_text('q') |
4177 | + |
4178 | + self.ui.request_password_token_ok_button.clicked() |
4179 | + |
4180 | + self.assert_correct_entry_warning(self.ui.reset_email_entry, |
4181 | + gui.EMAIL_INVALID) |
4182 | + self.assertNotIn('request_password_reset_token', |
4183 | + self.ui.backend._called) |
4184 | + |
4185 | + def test_no_warning_messages_if_valid_data(self): |
4186 | + """No warning messages are shown if the data is valid.""" |
4187 | + # this will certainly NOT generate warnings |
4188 | + self.click_request_password_token_with_valid_data() |
4189 | + |
4190 | + self.assert_warnings_visibility() |
4191 | + |
4192 | + def test_no_warning_messages_if_valid_data_after_invalid_data(self): |
4193 | + """No warnings if the data is valid (with prior invalid data).""" |
4194 | + # this will certainly generate warnings |
4195 | + self.ui.request_password_token_ok_button.clicked() |
4196 | + |
4197 | + # this will certainly NOT generate warnings |
4198 | + self.click_request_password_token_with_valid_data() |
4199 | + |
4200 | + self.assert_warnings_visibility() |
4201 | + |
4202 | + |
4203 | +class SetNewPasswordTestCase(UbuntuSSOClientTestCase): |
4204 | + """Test suite for setting a new password functionality.""" |
4205 | + |
4206 | + def setUp(self): |
4207 | + """Init.""" |
4208 | + super(SetNewPasswordTestCase, self).setUp() |
4209 | + self.click_request_password_token_with_valid_data() |
4210 | + self.ui.on_password_reset_token_sent(app_name=APP_NAME, email=EMAIL) |
4211 | + |
4212 | + def test_on_set_new_password_ok_button_disabled(self): |
4213 | + """The set_new_password_ok_button is disabled until values added.""" |
4214 | + self.click_request_password_token_with_valid_data() |
4215 | + self.assertFalse(self.ui.set_new_password_ok_button.get_sensitive()) |
4216 | + |
4217 | + msg = 'set_new_password_ok_button must be sensitive (%s) for %r.' |
4218 | + entries = (self.ui.reset_code_entry, |
4219 | + self.ui.reset_password1_entry, |
4220 | + self.ui.reset_password2_entry) |
4221 | + for values in itertools.product(('', ' ', 'a'), repeat=3): |
4222 | + expected = True |
4223 | + for entry, val in zip(entries, values): |
4224 | + entry.set_text(val) |
4225 | + expected &= bool(val and not val.isspace()) |
4226 | + |
4227 | + actual = self.ui.set_new_password_ok_button.get_sensitive() |
4228 | + self.assertEqual(expected, actual, msg % (expected, values)) |
4229 | + |
4230 | + def test_on_set_new_password_ok_button_clicked_morphs_window(self): |
4231 | + """Clicking set_new_password_ok_button the processing page is shown.""" |
4232 | + self.click_set_new_password_with_valid_data() |
4233 | + self.assert_pages_visibility(processing=True) |
4234 | + |
4235 | + def test_on_set_new_password_ok_button_clicked_calls_backend(self): |
4236 | + """Clicking set_new_password_ok_button the backend is called.""" |
4237 | + self.click_set_new_password_with_valid_data() |
4238 | + self.assert_backend_called('set_new_password', |
4239 | + APP_NAME, EMAIL, RESET_PASSWORD_TOKEN, PASSWORD) |
4240 | + |
4241 | + def test_on_password_changed_shows_login_page(self): |
4242 | + """When password was successfuly changed the login page is shown.""" |
4243 | + self.ui.on_password_changed(app_name=APP_NAME, email=EMAIL) |
4244 | + self.assert_correct_label_warning(self.ui.warning_label, |
4245 | + gui.PASSWORD_CHANGED) |
4246 | + self.assert_pages_visibility(login=True) |
4247 | + |
4248 | + def test_on_password_change_error_shows_login_page(self): |
4249 | + """When password wasn't changed the reset password page is shown.""" |
4250 | + self.ui.on_password_change_error(app_name=APP_NAME, error=self.error) |
4251 | + self.assert_correct_label_warning(self.ui.warning_label, |
4252 | + UNKNOWN_ERROR) |
4253 | + self.assert_pages_visibility(request_password_token=True) |
4254 | + |
4255 | + def test_specific_errors_from_backend_are_shown(self): |
4256 | + """Specific errors from backend are used.""" |
4257 | + error = {'errtype': 'NewPasswordError', |
4258 | + 'message': 'We\'re so doomed.', |
4259 | + '__all__': 'We all are gonna die.'} |
4260 | + |
4261 | + self.ui.on_password_change_error(app_name=APP_NAME, error=error) |
4262 | + |
4263 | + expected = '\n'.join((error['__all__'], error['message'])) |
4264 | + self.assert_correct_label_warning(self.ui.warning_label, expected) |
4265 | + |
4266 | + def test_ok_button_does_nothing_if_clicked_but_disabled(self): |
4267 | + """The new passwrd can only be set if the button is sensitive.""" |
4268 | + self.patch(self.ui.reset_code_entry, 'get_text', self._set_called) |
4269 | + |
4270 | + self.ui.set_new_password_ok_button.set_sensitive(False) |
4271 | + self.ui.set_new_password_ok_button.clicked() |
4272 | + self.assertFalse(self._called) |
4273 | + |
4274 | + self.ui.set_new_password_ok_button.set_sensitive(True) |
4275 | + self.ui.set_new_password_ok_button.clicked() |
4276 | + self.assertTrue(self._called) |
4277 | + |
4278 | + |
4279 | +class SetNewPasswordValidationTestCase(UbuntuSSOClientTestCase): |
4280 | + """Test suite for validations for setting a new password.""" |
4281 | + |
4282 | + def test_warning_is_shown_if_reset_code_empty(self): |
4283 | + """A warning message is shown if reset_code is empty.""" |
4284 | + self.ui.reset_code_entry.set_text('') |
4285 | + |
4286 | + self.ui.set_new_password_ok_button.set_sensitive(True) |
4287 | + self.ui.set_new_password_ok_button.clicked() |
4288 | + |
4289 | + self.assert_correct_entry_warning(self.ui.reset_code_entry, |
4290 | + gui.FIELD_REQUIRED) |
4291 | + self.assertNotIn('set_new_password', self.ui.backend._called) |
4292 | + |
4293 | + def test_password_help_is_always_shown(self): |
4294 | + """Password help text is correctly displayed.""" |
4295 | + visible = self.ui.reset_password_help_label.get_property('visible') |
4296 | + self.assertTrue(visible, 'password help text is visible.') |
4297 | + actual = self.ui.reset_password_help_label.get_text() |
4298 | + self.assertEqual(actual.decode('utf8'), gui.PASSWORD_HELP) |
4299 | + self.assertNotIn('set_new_password', self.ui.backend._called) |
4300 | + |
4301 | + def test_warning_is_shown_if_password_mismatch(self): |
4302 | + """A warning message is shown if password doesn't match.""" |
4303 | + self.ui.reset_password1_entry.set_text(PASSWORD) |
4304 | + self.ui.reset_password2_entry.set_text(PASSWORD * 2) |
4305 | + |
4306 | + self.ui.set_new_password_ok_button.set_sensitive(True) |
4307 | + self.ui.set_new_password_ok_button.clicked() |
4308 | + |
4309 | + self.assert_correct_entry_warning(self.ui.reset_password1_entry, |
4310 | + gui.PASSWORD_MISMATCH) |
4311 | + self.assert_correct_entry_warning(self.ui.reset_password2_entry, |
4312 | + gui.PASSWORD_MISMATCH) |
4313 | + self.assertNotIn('set_new_password', self.ui.backend._called) |
4314 | + |
4315 | + def test_warning_is_shown_if_password_too_weak(self): |
4316 | + """A warning message is shown if password is too weak.""" |
4317 | + # password will match but will be too weak |
4318 | + for pwd in ('', 'h3lloWo', PASSWORD.lower(), 'helloWorld'): |
4319 | + self.ui.reset_password1_entry.set_text(pwd) |
4320 | + self.ui.reset_password2_entry.set_text(pwd) |
4321 | + |
4322 | + self.ui.set_new_password_ok_button.set_sensitive(True) |
4323 | + self.ui.set_new_password_ok_button.clicked() |
4324 | + |
4325 | + self.assert_correct_entry_warning(self.ui.reset_password1_entry, |
4326 | + gui.PASSWORD_TOO_WEAK) |
4327 | + self.assert_correct_entry_warning(self.ui.reset_password2_entry, |
4328 | + gui.PASSWORD_TOO_WEAK) |
4329 | + self.assertNotIn('set_new_password', self.ui.backend._called) |
4330 | + |
4331 | + def test_no_warning_messages_if_valid_data(self): |
4332 | + """No warning messages are shown if the data is valid.""" |
4333 | + # this will certainly NOT generate warnings |
4334 | + self.click_set_new_password_with_valid_data() |
4335 | + |
4336 | + self.assert_warnings_visibility() |
4337 | + |
4338 | + def test_no_warning_messages_if_valid_data_after_invalid_data(self): |
4339 | + """No warnings if the data is valid (with prior invalid data).""" |
4340 | + # this will certainly generate warnings |
4341 | + self.ui.set_new_password_ok_button.clicked() |
4342 | + |
4343 | + # this will certainly NOT generate warnings |
4344 | + self.click_set_new_password_with_valid_data() |
4345 | + |
4346 | + self.assert_warnings_visibility() |
4347 | + |
4348 | + |
4349 | +class SignalsTestCase(UbuntuSSOClientTestCase): |
4350 | + """Test suite for the backend signals.""" |
4351 | + |
4352 | + def test_all_the_signals_are_listed(self): |
4353 | + """All the backend signals are listed to be binded.""" |
4354 | + for sig in ('CaptchaGenerated', 'CaptchaGenerationError', |
4355 | + 'UserRegistered', 'UserRegistrationError', |
4356 | + 'LoggedIn', 'LoginError', 'UserNotValidated', |
4357 | + 'EmailValidated', 'EmailValidationError', |
4358 | + 'PasswordResetTokenSent', 'PasswordResetError', |
4359 | + 'PasswordChanged', 'PasswordChangeError'): |
4360 | + self.assertIn(sig, self.ui._signals) |
4361 | + |
4362 | + def test_signal_receivers_are_connected(self): |
4363 | + """Callbacks are connected to signals of interest.""" |
4364 | + msg1 = 'callback %r for signal %r must be added to the backend.' |
4365 | + msg2 = 'callback %r for signal %r must be added to the ui log.' |
4366 | + for signal, method in self.ui._signals.items(): |
4367 | + actual = self.ui.backend.callbacks.get(signal) |
4368 | + self.assertEqual([method], actual, msg1 % (method, signal)) |
4369 | + actual = self.ui._signals_receivers.get(signal) |
4370 | + self.assertEqual(method, actual, msg2 % (method, signal)) |
4371 | + |
4372 | + def test_callbacks_only_log_when_app_name_doesnt_match(self): |
4373 | + """Callbacks do nothing but logging when app_name doesn't match.""" |
4374 | + mismatch_app_name = self.ui.app_name * 2 |
4375 | + for method in self.ui._signals.values(): |
4376 | + msgs = ('ignoring', method.__name__, repr(mismatch_app_name)) |
4377 | + method(mismatch_app_name, 'dummy') |
4378 | + self.assertTrue(self.memento.check(logging.INFO, *msgs)) |
4379 | + self.memento.reset() |
4380 | + |
4381 | + def test_on_captcha_generated_is_not_called(self): |
4382 | + """on_captcha_generated is not called if incorrect app_name.""" |
4383 | + self.patch(self.ui, 'on_captcha_generated', self._set_called) |
4384 | + mismatch_app_name = self.ui.app_name * 2 |
4385 | + self.ui._signals['CaptchaGenerated'](mismatch_app_name, 'dummy') |
4386 | + self.assertFalse(self._called) |
4387 | + |
4388 | + def test_on_captcha_generation_error_is_not_called(self): |
4389 | + """on_captcha_generation_error is not called if incorrect app_name.""" |
4390 | + self.patch(self.ui, 'on_captcha_generation_error', self._set_called) |
4391 | + mismatch_app_name = self.ui.app_name * 2 |
4392 | + self.ui._signals['CaptchaGenerationError'](mismatch_app_name, 'dummy') |
4393 | + self.assertFalse(self._called) |
4394 | + |
4395 | + def test_on_user_registered_is_not_called(self): |
4396 | + """on_user_registered is not called if incorrect app_name.""" |
4397 | + self.patch(self.ui, 'on_user_registered', self._set_called) |
4398 | + mismatch_app_name = self.ui.app_name * 2 |
4399 | + self.ui._signals['UserRegistered'](mismatch_app_name, 'dummy') |
4400 | + self.assertFalse(self._called) |
4401 | + |
4402 | + def test_on_user_registration_error_is_not_called(self): |
4403 | + """on_user_registration_error is not called if incorrect app_name.""" |
4404 | + self.patch(self.ui, 'on_user_registration_error', self._set_called) |
4405 | + mismatch_app_name = self.ui.app_name * 2 |
4406 | + self.ui._signals['UserRegistrationError'](mismatch_app_name, 'dummy') |
4407 | + self.assertFalse(self._called) |
4408 | + |
4409 | + def test_on_email_validated_is_not_called(self): |
4410 | + """on_email_validated is not called if incorrect app_name.""" |
4411 | + self.patch(self.ui, 'on_email_validated', self._set_called) |
4412 | + mismatch_app_name = self.ui.app_name * 2 |
4413 | + self.ui._signals['EmailValidated'](mismatch_app_name, 'dummy') |
4414 | + self.assertFalse(self._called) |
4415 | + |
4416 | + def test_on_email_validation_error_is_not_called(self): |
4417 | + """on_email_validation_error is not called if incorrect app_name.""" |
4418 | + self.patch(self.ui, 'on_email_validation_error', self._set_called) |
4419 | + mismatch_app_name = self.ui.app_name * 2 |
4420 | + self.ui._signals['EmailValidationError'](mismatch_app_name, 'dummy') |
4421 | + self.assertFalse(self._called) |
4422 | + |
4423 | + def test_on_logged_in_is_not_called(self): |
4424 | + """on_logged_in is not called if incorrect app_name.""" |
4425 | + self.patch(self.ui, 'on_logged_in', self._set_called) |
4426 | + mismatch_app_name = self.ui.app_name * 2 |
4427 | + self.ui._signals['LoggedIn'](mismatch_app_name, 'dummy') |
4428 | + self.assertFalse(self._called) |
4429 | + |
4430 | + def test_on_login_error_is_not_called(self): |
4431 | + """on_login_error is not called if incorrect app_name.""" |
4432 | + self.patch(self.ui, 'on_login_error', self._set_called) |
4433 | + mismatch_app_name = self.ui.app_name * 2 |
4434 | + self.ui._signals['LoginError'](mismatch_app_name, 'dummy') |
4435 | + self.assertFalse(self._called) |
4436 | + |
4437 | + def test_on_user_not_validated_is_not_called(self): |
4438 | + """on_user_not_validated is not called if incorrect app_name.""" |
4439 | + self.patch(self.ui, 'on_user_not_validated', self._set_called) |
4440 | + mismatch_app_name = self.ui.app_name * 2 |
4441 | + self.ui._signals['UserNotValidated'](mismatch_app_name, 'dummy') |
4442 | + self.assertFalse(self._called) |
4443 | + |
4444 | + def test_on_password_reset_token_sent_is_not_called(self): |
4445 | + """on_password_reset_token_sent is not called if incorrect app_name.""" |
4446 | + self.patch(self.ui, 'on_password_reset_token_sent', self._set_called) |
4447 | + mismatch_app_name = self.ui.app_name * 2 |
4448 | + self.ui._signals['PasswordResetTokenSent'](mismatch_app_name, 'dummy') |
4449 | + self.assertFalse(self._called) |
4450 | + |
4451 | + def test_on_password_reset_error_is_not_called(self): |
4452 | + """on_password_reset_error is not called if incorrect app_name.""" |
4453 | + self.patch(self.ui, 'on_password_reset_error', self._set_called) |
4454 | + mismatch_app_name = self.ui.app_name * 2 |
4455 | + self.ui._signals['PasswordResetError'](mismatch_app_name, 'dummy') |
4456 | + self.assertFalse(self._called) |
4457 | + |
4458 | + def test_on_password_changed_is_not_called(self): |
4459 | + """on_password_changed is not called if incorrect app_name.""" |
4460 | + self.patch(self.ui, 'on_password_changed', self._set_called) |
4461 | + mismatch_app_name = self.ui.app_name * 2 |
4462 | + self.ui._signals['PasswordChanged'](mismatch_app_name, 'dummy') |
4463 | + self.assertFalse(self._called) |
4464 | + |
4465 | + def test_on_password_change_error_is_not_called(self): |
4466 | + """on_password_change_error is not called if incorrect app_name.""" |
4467 | + self.patch(self.ui, 'on_password_change_error', self._set_called) |
4468 | + mismatch_app_name = self.ui.app_name * 2 |
4469 | + self.ui._signals['PasswordChangeError'](mismatch_app_name, 'dummy') |
4470 | + self.assertFalse(self._called) |
4471 | + |
4472 | + |
4473 | +class LoginOnlyTestCase(UbuntuSSOClientTestCase): |
4474 | + """Test suite for the login only GUI.""" |
4475 | + |
4476 | + kwargs = dict(app_name=APP_NAME, tc_url=None, help_text=HELP_TEXT, |
4477 | + login_only=True) |
4478 | + |
4479 | + def test_login_is_first_page(self): |
4480 | + """When starting, the login page is the first one.""" |
4481 | + self.assert_pages_visibility(login=True) |
4482 | + |
4483 | + def test_no_back_button(self): |
4484 | + """There is no back button in the login screen.""" |
4485 | + self.assertFalse(self.ui.login_back_button.get_property('visible')) |
4486 | + |
4487 | + def test_login_ok_button_has_the_focus(self): |
4488 | + """The login_ok_button has the focus.""" |
4489 | + self.assertTrue(self.ui.login_ok_button.is_focus()) |
4490 | + |
4491 | + def test_help_text_is_used(self): |
4492 | + """The passed help_text is used.""" |
4493 | + self.assertEqual(self.ui.help_label.get_text().decode('utf8'), |
4494 | + HELP_TEXT) |
4495 | + |
4496 | + |
4497 | +class ReturnCodeTestCase(UbuntuSSOClientTestCase): |
4498 | + """Test the return codes.""" |
4499 | + |
4500 | + def setUp(self): |
4501 | + super(ReturnCodeTestCase, self).setUp() |
4502 | + self.patch(gui.sys, 'exit', self._set_called) |
4503 | + |
4504 | + def test_closing_main_window(self): |
4505 | + """When closing the main window, USER_CANCELLATION is called.""" |
4506 | + self.ui.window.emit('delete-event', Gdk.Event()) |
4507 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
4508 | + |
4509 | + def test_every_cancel_calls_proper_callback(self): |
4510 | + """When any cancel button is clicked, USER_CANCELLATION is called.""" |
4511 | + self.patch(self.ui.backend, 'disconnect_from_signal', lambda *a: None) |
4512 | + msg = 'USER_CANCELLATION should be returned when %r is clicked.' |
4513 | + buttons = filter(lambda name: 'cancel_button' in name, self.ui.widgets) |
4514 | + for name in buttons: |
4515 | + widget = getattr(self.ui, name) |
4516 | + widget.clicked() |
4517 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {}), |
4518 | + msg % name) |
4519 | + self._called = False |
4520 | + |
4521 | + def test_on_user_registration_error_proper_callback_is_called(self): |
4522 | + """On UserRegistrationError, USER_CANCELLATION is called.""" |
4523 | + self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error) |
4524 | + self.ui.on_close_clicked() |
4525 | + |
4526 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
4527 | + |
4528 | + def test_on_email_validated_proper_callback_is_called(self): |
4529 | + """On EmailValidated, REGISTRATION_SUCCESS is called.""" |
4530 | + self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL) |
4531 | + self.ui.on_close_clicked() |
4532 | + |
4533 | + self.assertEqual(self._called, ((gui.USER_SUCCESS,), {})) |
4534 | + |
4535 | + def test_on_email_validation_error_proper_callback_is_called(self): |
4536 | + """On EmailValidationError, USER_CANCELLATION is called.""" |
4537 | + self.ui.on_email_validation_error(app_name=APP_NAME, error=self.error) |
4538 | + self.ui.on_close_clicked() |
4539 | + |
4540 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
4541 | + |
4542 | + def test_on_logged_in_proper_callback_is_called(self): |
4543 | + """On LoggedIn, LOGIN_SUCCESS is called.""" |
4544 | + self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL) |
4545 | + self.ui.on_close_clicked() |
4546 | + |
4547 | + self.assertEqual(self._called, ((gui.USER_SUCCESS,), {})) |
4548 | + |
4549 | + def test_on_login_error_proper_callback_is_called(self): |
4550 | + """On LoginError, USER_CANCELLATION is called.""" |
4551 | + self.click_connect_with_valid_data() |
4552 | + self.ui.on_login_error(app_name=APP_NAME, error=self.error) |
4553 | + self.ui.on_close_clicked() |
4554 | + |
4555 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
4556 | + |
4557 | + def test_registration_success_even_if_prior_registration_error(self): |
4558 | + """Only one callback is called with the final outcome. |
4559 | + |
4560 | + When the user successfully registers, REGISTRATION_SUCCESS is |
4561 | + called even if there were errors before. |
4562 | + |
4563 | + """ |
4564 | + self.click_join_with_valid_data() |
4565 | + self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error) |
4566 | + self.click_join_with_valid_data() |
4567 | + self.ui.on_email_validated(app_name=APP_NAME, email=EMAIL) |
4568 | + self.ui.on_close_clicked() |
4569 | + |
4570 | + self.assertEqual(self._called, ((gui.USER_SUCCESS,), {})) |
4571 | + |
4572 | + def test_login_success_even_if_prior_login_error(self): |
4573 | + """Only one callback is called with the final outcome. |
4574 | + |
4575 | + When the user successfully logins, LOGIN_SUCCESS is called even if |
4576 | + there were errors before. |
4577 | + |
4578 | + """ |
4579 | + self.click_connect_with_valid_data() |
4580 | + self.ui.on_login_error(app_name=APP_NAME, error=self.error) |
4581 | + self.click_connect_with_valid_data() |
4582 | + self.ui.on_logged_in(app_name=APP_NAME, email=EMAIL) |
4583 | + self.ui.on_close_clicked() |
4584 | + |
4585 | + self.assertEqual(self._called, ((gui.USER_SUCCESS,), {})) |
4586 | + |
4587 | + def test_user_cancelation_even_if_prior_registration_error(self): |
4588 | + """Only one callback is called with the final outcome. |
4589 | + |
4590 | + When the user closes the window, USER_CANCELLATION is called even if |
4591 | + there were registration errors before. |
4592 | + |
4593 | + """ |
4594 | + self.click_join_with_valid_data() |
4595 | + self.ui.on_user_registration_error(app_name=APP_NAME, error=self.error) |
4596 | + self.ui.join_cancel_button.clicked() |
4597 | + |
4598 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
4599 | + |
4600 | + def test_user_cancelation_even_if_prior_login_error(self): |
4601 | + """Only one callback is called with the final outcome. |
4602 | + |
4603 | + When the user closes the window, USER_CANCELLATION is called even if |
4604 | + there were login errors before. |
4605 | + |
4606 | + """ |
4607 | + self.click_connect_with_valid_data() |
4608 | + self.ui.on_login_error(app_name=APP_NAME, error=self.error) |
4609 | + self.ui.login_cancel_button.clicked() |
4610 | + |
4611 | + self.assertEqual(self._called, ((gui.USER_CANCELLATION,), {})) |
4612 | + |
4613 | + |
4614 | +class DefaultButtonsTestCase(UbuntuSSOClientTestCase): |
4615 | + """Each UI page has a default button when visible.""" |
4616 | + |
4617 | + def setUp(self): |
4618 | + """Init.""" |
4619 | + super(DefaultButtonsTestCase, self).setUp() |
4620 | + self.mapping = ( |
4621 | + ('enter_details_vbox', 'join_ok_button'), |
4622 | + ('tc_browser_vbox', 'tc_back_button'), |
4623 | + ('verify_email_vbox', 'verify_token_button'), |
4624 | + ('login_vbox', 'login_ok_button'), |
4625 | + ('request_password_token_vbox', |
4626 | + 'request_password_token_ok_button'), |
4627 | + ('set_new_password_vbox', 'set_new_password_ok_button'), |
4628 | + ('success_vbox', 'finish_close_button'), |
4629 | + ('error_vbox', 'finish_close_button'), |
4630 | + ('processing_vbox', None)) |
4631 | + |
4632 | + def test_pages_have_default_widget_set(self): |
4633 | + """Each page has a proper button as default widget.""" |
4634 | + msg = 'Page %r must have %r as default_widget (got %r instead).' |
4635 | + for pname, bname in self.mapping: |
4636 | + page = getattr(self.ui, pname) |
4637 | + button = bname and getattr(self.ui, bname) |
4638 | + self.assertTrue(page.default_widget is button, |
4639 | + msg % (pname, bname, page.default_widget)) |
4640 | + |
4641 | + def test_default_widget_can_default(self): |
4642 | + """Each default button can default.""" |
4643 | + msg = 'Button %r must have can-default enabled.' |
4644 | + for _, bname in self.mapping: |
4645 | + if bname is not None: |
4646 | + button = getattr(self.ui, bname) |
4647 | + self.assertTrue(button.get_property('can-default'), |
4648 | + msg % bname) |
4649 | + |
4650 | + def test_set_current_page_grabs_focus_for_default_button(self): |
4651 | + """Setting the current page marks the default widget as default.""" |
4652 | + msg = '%r "has_default" must be True when %r if the current page.' |
4653 | + for pname, bname in self.mapping: |
4654 | + if bname is not None: |
4655 | + page = getattr(self.ui, pname) |
4656 | + self.patch(page.default_widget, 'grab_default', |
4657 | + self._set_called) |
4658 | + self.ui._set_current_page(page) |
4659 | + self.assertEqual(self._called, ((), {}), msg % (bname, pname)) |
4660 | + self._called = False |
4661 | + |
4662 | + |
4663 | +class RunTestCase(BasicTestCase): |
4664 | + |
4665 | + def test_run(self): |
4666 | + """Calling run.gui() a UI instance is created.""" |
4667 | + called = [] |
4668 | + self.patch(gui, 'UbuntuSSOClientGUI', |
4669 | + lambda **kw: called.append(('GUI', kw))) |
4670 | + self.patch(gui.Gtk, 'main', |
4671 | + lambda: called.append('main')) |
4672 | + |
4673 | + kwargs = dict(foo='foo', bar='bar', baz='yadda', yadda=0) |
4674 | + gui.run(**kwargs) |
4675 | + |
4676 | + kwargs['close_callback'] = gui.Gtk.main_quit |
4677 | + self.assertEqual(called, [('GUI', kwargs), 'main']) |
940 -#!/bin/sh
941 +#! /bin/bash
963 -#!/usr/bin/env python
964 +#! /usr/bin/env python
Why the adding of a space between ! and / here?
1127 +#!/usr/bin/env python
And not here?
Also, can you please make them be #!/usr/bin/python instead of using env? Per bug #984089, this is needed so programs can be used in virtualenv, and so that installing with python3 will work (though this code isn't ported to python3 yet obviously).
Also, as mentioned in IRC, I'm not sure the test suite actually needs trial or anything from twisted, though that may be fixed later, when porting to Python 3.