Merge lp:~nataliabidart/software-center/winged-migration into lp:software-center

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
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.

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 :

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.

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'])