Merge lp:~nataliabidart/ubuntu/maverick/ubuntu-sso-client/ubuntu-sso-client-1.0.9 into lp:ubuntu/maverick/ubuntu-sso-client

Proposed by Natalia Bidart on 2011-04-12
Status: Superseded
Proposed branch: lp:~nataliabidart/ubuntu/maverick/ubuntu-sso-client/ubuntu-sso-client-1.0.9
Merge into: lp:ubuntu/maverick/ubuntu-sso-client
Diff against target: 1307 lines (+490/-147)
12 files modified
PKG-INFO (+1/-1)
bin/ubuntu-sso-login (+11/-17)
data/ui.glade (+40/-32)
debian/changelog (+61/-0)
debian/control (+2/-1)
debian/watch (+1/-1)
run-tests (+1/-1)
setup.py (+3/-2)
ubuntu_sso/gui.py (+33/-22)
ubuntu_sso/main.py (+108/-39)
ubuntu_sso/tests/test_gui.py (+65/-20)
ubuntu_sso/tests/test_main.py (+164/-11)
To merge this branch: bzr merge lp:~nataliabidart/ubuntu/maverick/ubuntu-sso-client/ubuntu-sso-client-1.0.9
Reviewer Review Type Date Requested Status
Ubuntu Sponsors Team 2011-04-12 Pending
Review via email: mp+57379@code.launchpad.net

Description of the change

  * New upstream release:
    [ Natalia B. Bidart <email address hidden> ]
      - Register now uses the 'displayname' field to pass it on to SSO as display name (LP: #709494).

To post a comment you must log in.

Unmerged revisions

22. By Natalia Bidart on 2011-04-12

Attaching bug Bug #709494.

21. By Natalia Bidart on 2011-03-29

* New upstream release:
  [ Natalia B. Bidart <email address hidden> ]
    - Register now uses the 'displayname' field to pass it on to SSO as
    display name (LP: #709494).

20. By Natalia Bidart on 2010-12-16

* New upstream release.

  [ Natalia B. Bidart <email address hidden> ]
     * Avoid generating an extra token when attempting to validate a user
     account (LP: #687523).

19. By Sebastien Bacher on 2010-11-05

releasing version 1.0.7-0ubuntu1

18. By Natalia Bidart on 2010-10-12

* New upstream release:

[ Alejandro J. Cura <email address hidden> ]
  * Replace twisted gtk reactor with the standard gtk mainloop. (LP: #655327).

[ Alejandro J. Cura <email address hidden> ]
  * Call the dbus mainloop thread init (fixes LP: #656545).

* Adding .bzr-builddeb/default.conf as per Michael Vog (mvo) request.

* Adding dpkg (>= 1.15.7.2) as Pre-Depends (fixes LP: #658768).

* Adding gnome-keyring as dep since python-gnomekeyring doesn't install it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'PKG-INFO'
--- PKG-INFO 2010-10-01 14:57:22 +0000
+++ PKG-INFO 2011-04-12 19:01:16 +0000
@@ -1,6 +1,6 @@
1Metadata-Version: 1.11Metadata-Version: 1.1
2Name: ubuntu-sso-client2Name: ubuntu-sso-client
3Version: 1.0.33Version: 1.0.9
4Summary: Ubuntu Single Sign-On client4Summary: Ubuntu Single Sign-On client
5Home-page: https://launchpad.net/ubuntu-sso-client5Home-page: https://launchpad.net/ubuntu-sso-client
6Author: Natalia Bidart6Author: Natalia Bidart
77
=== modified file 'bin/ubuntu-sso-login'
--- bin/ubuntu-sso-login 2010-10-01 14:57:22 +0000
+++ bin/ubuntu-sso-login 2011-04-12 19:01:16 +0000
@@ -42,6 +42,7 @@
42import signal42import signal
43import sys43import sys
4444
45import dbus.mainloop.glib
45import dbus.service46import dbus.service
46import gtk47import gtk
4748
@@ -57,21 +58,21 @@
5758
5859
59logger = setup_logging("ubuntu-sso-login")60logger = setup_logging("ubuntu-sso-login")
61dbus.mainloop.glib.threads_init()
60gtk.gdk.threads_init()62gtk.gdk.threads_init()
61DBusGMainLoop(set_as_default=True)63DBusGMainLoop(set_as_default=True)
6264
6365
64def sighup_handler(*a, **kw):66def sighup_handler(*a, **kw):
65 """Stop the service.67 """Stop the service."""
6668 # This handler may be called in any thread, so is not thread safe.
67 This is not thread safe, see the link below for info:69 # See the link below for info:
68 www.listware.net/201004/gtk-devel-list/115067-unix-signals-in-glib.html70 # www.listware.net/201004/gtk-devel-list/115067-unix-signals-in-glib.html
69 """71 #
70 from twisted.internet import reactor72 # gtk.main_quit and the logger methods are safe to be called from any thread.
71 # Module 'twisted.internet.reactor' has no 'stop' member73 # Just don't call other random stuff here.
72 # pylint: disable=E1101
73 logger.info("Stoping Ubuntu SSO login manager since SIGHUP was received.")74 logger.info("Stoping Ubuntu SSO login manager since SIGHUP was received.")
74 reactor.stop()75 gtk.main_quit()
7576
7677
77if __name__ == "__main__":78if __name__ == "__main__":
@@ -83,10 +84,6 @@
83 logger.error("Ubuntu SSO login manager already running, quitting.")84 logger.error("Ubuntu SSO login manager already running, quitting.")
84 sys.exit(0)85 sys.exit(0)
8586
86 logger.debug("Installing the Twisted gtk2reactor.")
87 from twisted.internet import gtk2reactor
88 gtk2reactor.install()
89
90 logger.debug("Hooking up SIGHUP with handler %r.", sighup_handler)87 logger.debug("Hooking up SIGHUP with handler %r.", sighup_handler)
91 signal.signal(signal.SIGHUP, sighup_handler)88 signal.signal(signal.SIGHUP, sighup_handler)
9289
@@ -97,7 +94,4 @@
97 bus=dbus.SessionBus()),94 bus=dbus.SessionBus()),
98 object_path=DBUS_CRED_PATH)95 object_path=DBUS_CRED_PATH)
9996
100 from twisted.internet import reactor97 gtk.main()
101 # Module 'twisted.internet.reactor' has no 'run' member
102 # pylint: disable=E1101
103 reactor.run()
10498
=== modified file 'data/ui.glade'
--- data/ui.glade 2010-08-27 22:16:40 +0000
+++ data/ui.glade 2011-04-12 19:01:16 +0000
@@ -264,36 +264,62 @@
264 </packing>264 </packing>
265 </child>265 </child>
266 <child>266 <child>
267 <object class="GtkHButtonBox" id="hbuttonbox1">267 <object class="GtkHBox" id="hbox2">
268 <property name="visible">True</property>268 <property name="visible">True</property>
269 <property name="spacing">5</property>
270 <property name="layout_style">end</property>
271 <child>269 <child>
272 <object class="GtkButton" id="join_cancel_button">270 <object class="GtkLinkButton" id="login_button">
273 <property name="label">gtk-cancel</property>271 <property name="label">login button</property>
274 <property name="visible">True</property>272 <property name="visible">True</property>
275 <property name="can_focus">True</property>273 <property name="can_focus">True</property>
276 <property name="receives_default">True</property>274 <property name="receives_default">True</property>
277 <property name="use_stock">True</property>275 <property name="relief">none</property>
276 <signal name="clicked" handler="on_sign_in_button_clicked" swapped="no"/>
278 </object>277 </object>
279 <packing>278 <packing>
280 <property name="expand">False</property>279 <property name="expand">False</property>
281 <property name="fill">False</property>280 <property name="fill">True</property>
282 <property name="position">0</property>281 <property name="position">0</property>
283 </packing>282 </packing>
284 </child>283 </child>
285 <child>284 <child>
286 <object class="GtkButton" id="join_ok_button">285 <object class="GtkHButtonBox" id="hbuttonbox1">
287 <property name="label">gtk-go-forward</property>
288 <property name="visible">True</property>286 <property name="visible">True</property>
289 <property name="can_focus">True</property>287 <property name="can_focus">False</property>
290 <property name="receives_default">True</property>288 <property name="spacing">5</property>
291 <property name="use_stock">True</property>289 <property name="layout_style">end</property>
292 <signal name="clicked" handler="on_join_ok_button_clicked"/>290 <child>
291 <object class="GtkButton" id="join_cancel_button">
292 <property name="label">gtk-cancel</property>
293 <property name="visible">True</property>
294 <property name="can_focus">True</property>
295 <property name="receives_default">True</property>
296 <property name="use_stock">True</property>
297 </object>
298 <packing>
299 <property name="expand">False</property>
300 <property name="fill">False</property>
301 <property name="position">0</property>
302 </packing>
303 </child>
304 <child>
305 <object class="GtkButton" id="join_ok_button">
306 <property name="label">gtk-go-forward</property>
307 <property name="visible">True</property>
308 <property name="can_focus">True</property>
309 <property name="receives_default">True</property>
310 <property name="use_stock">True</property>
311 <signal name="clicked" handler="on_join_ok_button_clicked" swapped="no"/>
312 </object>
313 <packing>
314 <property name="expand">False</property>
315 <property name="fill">False</property>
316 <property name="position">1</property>
317 </packing>
318 </child>
293 </object>319 </object>
294 <packing>320 <packing>
295 <property name="expand">False</property>321 <property name="expand">False</property>
296 <property name="fill">False</property>322 <property name="pack_type">end</property>
297 <property name="position">1</property>323 <property name="position">1</property>
298 </packing>324 </packing>
299 </child>325 </child>
@@ -303,24 +329,6 @@
303 <property name="position">12</property>329 <property name="position">12</property>
304 </packing>330 </packing>
305 </child>331 </child>
306 <child>
307 <object class="GtkLinkButton" id="login_button">
308 <property name="label" translatable="yes">login button</property>
309 <property name="visible">True</property>
310 <property name="can_focus">True</property>
311 <property name="receives_default">True</property>
312 <property name="has_tooltip">True</property>
313 <property name="relief">none</property>
314 <signal name="clicked" handler="on_sign_in_button_clicked"/>
315 </object>
316 <packing>
317 <property name="expand">False</property>
318 <property name="position">13</property>
319 </packing>
320 </child>
321 <child>
322 <placeholder/>
323 </child>
324 </object>332 </object>
325 <object class="GtkVBox" id="processing_vbox">333 <object class="GtkVBox" id="processing_vbox">
326 <property name="visible">True</property>334 <property name="visible">True</property>
327335
=== modified file 'debian/changelog'
--- debian/changelog 2010-10-01 15:35:43 +0000
+++ debian/changelog 2011-04-12 19:01:16 +0000
@@ -1,3 +1,64 @@
1ubuntu-sso-client (1.0.9-0ubuntu1) UNRELEASED; urgency=low
2
3 * New upstream release:
4
5 [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
6 - Register now uses the 'displayname' field to pass it on to SSO as
7 display name (LP: #709494).
8
9 -- Natalia Bidart (nessita) <nataliabidart@gmail.com> Tue, 29 Mar 2011 16:26:42 -0300
10
11ubuntu-sso-client (1.0.8-0ubuntu1) maverick-proposed; urgency=low
12
13 * New upstream release.
14
15 [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
16 * Avoid generating an extra token when attempting to validate a user
17 account (LP: #687523).
18
19 -- Natalia Bidart (nessita) <nataliabidart@gmail.com> Thu, 16 Dec 2010 13:19:01 -0300
20
21ubuntu-sso-client (1.0.7-0ubuntu1) maverick-proposed; urgency=low
22
23 * New upstream release (1.0.6, 1.0.7):
24
25 [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
26 * Added a new DBus signal UserNotValidated to indicate when a user is
27 registered but not validated (LP: #667899).
28 * Added new workflow so email validation is requested if necessary.
29 * The verify email page should be always built, not only on registration.
30
31 [ Alejandro J. Cura <alecu@canonical.com> ]
32 * Store credentials on the keyring *only* from the main thread (LP:
33 #656545).
34
35 * New upstream release (1.0.5):
36
37 [ Natalia B. Bidart <natalia.bidart@canonical.com> ]
38
39 * Credentials are removed if the pinging to the server fails or any
40 other exception occurs (LP: #660516).
41
42 -- Natalia Bidart (nessita) <nataliabidart@gmail.com> Thu, 04 Nov 2010 09:21:00 -0300
43
44ubuntu-sso-client (1.0.4-0ubuntu1) maverick-proposed; urgency=low
45
46 * New upstream release:
47
48 [ Alejandro J. Cura <alecu@canonical.com> ]
49 * Replace twisted gtk reactor with the standard gtk mainloop. (LP: #655327).
50
51 [ Alejandro J. Cura <alecu@canonical.com> ]
52 * Call the dbus mainloop thread init (fixes LP: #656545).
53
54 * Adding .bzr-builddeb/default.conf as per Michael Vog (mvo) request.
55
56 * Adding dpkg (>= 1.15.7.2) as Pre-Depends (fixes LP: #658768).
57
58 * Adding gnome-keyring as dep since python-gnomekeyring doesn't install it.
59
60 -- Natalia Bidart (nessita) <nataliabidart@gmail.com> Tue, 12 Oct 2010 10:07:55 -0300
61
1ubuntu-sso-client (1.0.3-0ubuntu1) maverick; urgency=low62ubuntu-sso-client (1.0.3-0ubuntu1) maverick; urgency=low
263
3 * New upstream release:64 * New upstream release:
465
=== modified file 'debian/control'
--- debian/control 2010-09-13 15:02:42 +0000
+++ debian/control 2011-04-12 19:01:16 +0000
@@ -13,9 +13,10 @@
13Package: ubuntu-sso-client13Package: ubuntu-sso-client
14Architecture: all14Architecture: all
15XB-Python-Version: ${python:Versions}15XB-Python-Version: ${python:Versions}
16Pre-Depends: dpkg (>=1.15.7.2),
16Depends: ${misc:Depends},17Depends: ${misc:Depends},
17 ${python:Depends},18 ${python:Depends},
18 dpkg (>=1.15.7.2),19 gnome-keyring,
19 python-dbus,20 python-dbus,
20 python-gnomekeyring,21 python-gnomekeyring,
21 python-gtk2,22 python-gtk2,
2223
=== modified file 'debian/watch'
--- debian/watch 2010-09-22 18:53:18 +0000
+++ debian/watch 2011-04-12 19:01:16 +0000
@@ -1,3 +1,3 @@
1version=31version=3
2http://launchpad.net/ubuntu-sso-client/+download?start=10 .*/ubuntu-sso-client-([0-9.]+)\.tar\.gz2http://launchpad.net/ubuntu-sso-client/+download?start=20 .*/ubuntu-sso-client-([0-9.]+)\.tar\.gz
33
44
=== modified file 'run-tests'
--- run-tests 2010-09-08 19:25:02 +0000
+++ run-tests 2011-04-12 19:01:16 +0000
@@ -16,7 +16,7 @@
16# with this program. If not, see <http://www.gnu.org/licenses/>.16# with this program. If not, see <http://www.gnu.org/licenses/>.
1717
18`which xvfb-run` ./contrib/test "$@"18`which xvfb-run` ./contrib/test "$@"
19pylint contrib ubuntu_sso19pylint ubuntu_sso
20if [ -x `which pep8` ]; then20if [ -x `which pep8` ]; then
21 pep8 --repeat bin/ contrib/ ubuntu_sso/21 pep8 --repeat bin/ contrib/ ubuntu_sso/
22else22else
2323
=== modified file 'setup.py'
--- setup.py 2010-10-01 14:57:22 +0000
+++ setup.py 2011-04-12 19:01:16 +0000
@@ -36,7 +36,8 @@
36from distutils.command import clean, build36from distutils.command import clean, build
3737
38# Defining variables for various rules here, similar to a Makefile.am38# Defining variables for various rules here, similar to a Makefile.am
39CLEANFILES = ['data/com.ubuntu.sso.service']39CLEANFILES = ['data/com.ubuntu.sso.service', 'po/ubuntu-sso-client.pot',
40 'MANIFEST']
4041
4142
42# XXX: This needs some serious cleanup43# XXX: This needs some serious cleanup
@@ -86,7 +87,7 @@
8687
87DistUtilsExtra.auto.setup(88DistUtilsExtra.auto.setup(
88 name='ubuntu-sso-client',89 name='ubuntu-sso-client',
89 version='1.0.3',90 version='1.0.9',
90 license='GPL v3',91 license='GPL v3',
91 author='Natalia Bidart',92 author='Natalia Bidart',
92 author_email='natalia.bidart@canonical.com',93 author_email='natalia.bidart@canonical.com',
9394
=== modified file 'ubuntu_sso/gui.py'
--- ubuntu_sso/gui.py 2010-10-01 14:57:22 +0000
+++ ubuntu_sso/gui.py 2011-04-12 19:01:16 +0000
@@ -28,7 +28,7 @@
28import dbus28import dbus
29import gettext29import gettext
30import gobject30import gobject
31import gtk31import gtk # pylint: disable=W0403
32import webkit32import webkit
33import xdg33import xdg
3434
@@ -125,7 +125,7 @@
125 self.is_password = is_password125 self.is_password = is_password
126 self.warning = None126 self.warning = None
127127
128 super(LabeledEntry, self).__init__(*args, **kwargs)128 gtk.Entry.__init__(self, *args, **kwargs)
129129
130 self.set_width_chars(DEFAULT_WIDTH)130 self.set_width_chars(DEFAULT_WIDTH)
131 self._set_label(self, None)131 self._set_label(self, None)
@@ -161,7 +161,7 @@
161161
162 def get_text(self):162 def get_text(self):
163 """Get text only if it's not the label nor empty."""163 """Get text only if it's not the label nor empty."""
164 result = super(LabeledEntry, self).get_text()164 result = gtk.Entry.get_text(self)
165 if result == self.label or result.isspace():165 if result == self.label or result.isspace():
166 result = ''166 result = ''
167 return result167 return result
@@ -252,6 +252,8 @@
252 self.tc_uri = tc_uri252 self.tc_uri = tc_uri
253 self.help_text = help_text253 self.help_text = help_text
254 self.close_callback = close_callback254 self.close_callback = close_callback
255 self.user_email = None
256 self.user_password = None
255257
256 ui_filename = get_data_file('ui.glade')258 ui_filename = get_data_file('ui.glade')
257 builder = gtk.Builder()259 builder = gtk.Builder()
@@ -319,13 +321,13 @@
319 self._append_page(self._build_login_page())321 self._append_page(self._build_login_page())
320 self._append_page(self._build_request_password_token_page())322 self._append_page(self._build_request_password_token_page())
321 self._append_page(self._build_set_new_password_page())323 self._append_page(self._build_set_new_password_page())
324 self._append_page(self._build_verify_email_page())
322325
323 window_size = None326 window_size = None
324 if not login_only:327 if not login_only:
325 window_size = (550, 500)328 window_size = (550, 500)
326 self._append_page(self._build_enter_details_page())329 self._append_page(self._build_enter_details_page())
327 self._append_page(self._build_tc_page())330 self._append_page(self._build_tc_page())
328 self._append_page(self._build_verify_email_page())
329 self.login_button.grab_focus()331 self.login_button.grab_focus()
330 self._set_current_page(self.enter_details_vbox)332 self._set_current_page(self.enter_details_vbox)
331 else:333 else:
@@ -357,6 +359,8 @@
357 self._filter_by_app_name(self.on_logged_in),359 self._filter_by_app_name(self.on_logged_in),
358 'LoginError':360 'LoginError':
359 self._filter_by_app_name(self.on_login_error),361 self._filter_by_app_name(self.on_login_error),
362 'UserNotValidated':
363 self._filter_by_app_name(self.on_user_not_validated),
360 'PasswordResetTokenSent':364 'PasswordResetTokenSent':
361 self._filter_by_app_name(self.on_password_reset_token_sent),365 self._filter_by_app_name(self.on_password_reset_token_sent),
362 'PasswordResetError':366 'PasswordResetError':
@@ -383,8 +387,6 @@
383 msg = 'UbuntuSSOClientGUI: failed set_transient_for win id %r'387 msg = 'UbuntuSSOClientGUI: failed set_transient_for win id %r'
384 logger.exception(msg, window_id)388 logger.exception(msg, window_id)
385389
386 # Hidding unused widgets to save some space (LP #627440).
387 self.name_entry.hide()
388 self.yes_to_updates_checkbutton.hide()390 self.yes_to_updates_checkbutton.hide()
389391
390 self.window.show()392 self.window.show()
@@ -417,8 +419,8 @@
417419
418 match = self.bus.add_signal_receiver(method, signal_name=signal,420 match = self.bus.add_signal_receiver(method, signal_name=signal,
419 dbus_interface=iface)421 dbus_interface=iface)
420 logger.info('Connecting signal %r with method %r at iface %r.' \422 logger.debug('Connecting signal %r with method %r at iface %r.' \
421 'Match: %r', signal, method, iface, match)423 'Match: %r', signal, method, iface, match)
422 self._signals_receivers[(iface, signal)] = method424 self._signals_receivers[(iface, signal)] = method
423425
424 def _debug(self, *args, **kwargs):426 def _debug(self, *args, **kwargs):
@@ -725,8 +727,8 @@
725 remove = self.bus.remove_signal_receiver727 remove = self.bus.remove_signal_receiver
726 for (iface, signal) in self._signals_receivers.keys():728 for (iface, signal) in self._signals_receivers.keys():
727 method = self._signals_receivers.pop((iface, signal))729 method = self._signals_receivers.pop((iface, signal))
728 logger.info('Removing signal %r with method %r at iface %r.',730 logger.debug('Removing signal %r with method %r at iface %r.',
729 signal, method, iface)731 signal, method, iface)
730 remove(method, signal_name=signal, dbus_interface=iface)732 remove(method, signal_name=signal, dbus_interface=iface)
731733
732 # hide the main window734 # hide the main window
@@ -765,11 +767,10 @@
765767
766 error = False768 error = False
767769
768 # Hidding unused widgets to save some space (LP #627440).770 name = self.name_entry.get_text()
769 #name = self.name_entry.get_text()771 if not name:
770 #if not name:772 self.name_entry.set_warning(self.FIELD_REQUIRED)
771 # self.name_entry.set_warning(self.FIELD_REQUIRED)773 error = True
772 # error = True
773774
774 # check email775 # check email
775 email1 = self.email1_entry.get_text()776 email1 = self.email1_entry.get_text()
@@ -804,12 +805,15 @@
804 return805 return
805806
806 self._set_current_page(self.processing_vbox)807 self._set_current_page(self.processing_vbox)
808 self.user_email = email1
809 self.user_password = password1
807810
808 logger.info('Calling register_user with email %r, password <hidden>,' \811 logger.info('Calling register_with_name with email %r, password, '
809 ' captcha_id %r and captcha_solution %r.', email1,812 '<hidden> name %r, captcha_id %r and captcha_solution %r.',
810 self._captcha_id, captcha_solution)813 email1, name, self._captcha_id, captcha_solution)
811 f = self.backend.register_user814 f = self.backend.register_with_name
812 f(self.app_name, email1, password1, self._captcha_id, captcha_solution,815 f(self.app_name, email1, password1, name,
816 self._captcha_id, captcha_solution,
813 reply_handler=NO_OP, error_handler=NO_OP)817 reply_handler=NO_OP, error_handler=NO_OP)
814818
815 def on_tc_button_clicked(self, *args, **kwargs):819 def on_tc_button_clicked(self, *args, **kwargs):
@@ -832,8 +836,8 @@
832 self.email_token_entry.set_warning(self.FIELD_REQUIRED)836 self.email_token_entry.set_warning(self.FIELD_REQUIRED)
833 return837 return
834838
835 email = self.email1_entry.get_text()839 email = self.user_email
836 password = self.password1_entry.get_text()840 password = self.user_password
837 f = self.backend.validate_email841 f = self.backend.validate_email
838 logger.info('Calling validate_email with email %r, password <hidden>' \842 logger.info('Calling validate_email with email %r, password <hidden>' \
839 ', app_name %r and email_token %r.', email, self.app_name,843 ', app_name %r and email_token %r.', email, self.app_name,
@@ -871,6 +875,8 @@
871 reply_handler=NO_OP, error_handler=NO_OP)875 reply_handler=NO_OP, error_handler=NO_OP)
872876
873 self._set_current_page(self.processing_vbox)877 self._set_current_page(self.processing_vbox)
878 self.user_email = email
879 self.user_password = password
874880
875 def on_login_back_button_clicked(self, *args, **kwargs):881 def on_login_back_button_clicked(self, *args, **kwargs):
876 """User wants to go to the previous page."""882 """User wants to go to the previous page."""
@@ -1061,6 +1067,11 @@
1061 msg))1067 msg))
10621068
1063 @log_call1069 @log_call
1070 def on_user_not_validated(self, app_name, email, *args, **kwargs):
1071 """User was not validated."""
1072 self.on_user_registered(app_name, email)
1073
1074 @log_call
1064 def on_password_reset_token_sent(self, app_name, email, *args, **kwargs):1075 def on_password_reset_token_sent(self, app_name, email, *args, **kwargs):
1065 """Password reset token was successfully sent."""1076 """Password reset token was successfully sent."""
1066 msg = self.SET_NEW_PASSWORD_LABEL % {'email': email}1077 msg = self.SET_NEW_PASSWORD_LABEL % {'email': email}
10671078
=== modified file 'ubuntu_sso/main.py'
--- ubuntu_sso/main.py 2010-09-14 19:28:09 +0000
+++ ubuntu_sso/main.py 2011-04-12 19:01:16 +0000
@@ -55,6 +55,7 @@
55logger = setup_logging("ubuntu_sso.main")55logger = setup_logging("ubuntu_sso.main")
56PING_URL = "https://one.ubuntu.com/oauth/sso-finished-so-get-tokens/"56PING_URL = "https://one.ubuntu.com/oauth/sso-finished-so-get-tokens/"
57SERVICE_URL = "https://login.ubuntu.com/api/1.0"57SERVICE_URL = "https://login.ubuntu.com/api/1.0"
58NO_OP = lambda *args, **kwargs: None
5859
5960
60class NoDefaultConfigError(Exception):61class NoDefaultConfigError(Exception):
@@ -96,10 +97,16 @@
96 """The new password could not be set."""97 """The new password could not be set."""
9798
9899
99def keyring_store_credentials(app_name, credentials):100def keyring_store_credentials(app_name, credentials, callback, *cb_args):
100 """Store the credentials in the keyring."""101 """Store the credentials in the keyring."""
101 logger.info('keyring_store_credentials: app_name "%s".', app_name)102
102 Keyring(app_name).set_ubuntusso_attr(credentials)103 def _inner():
104 """Store the credentials, and trigger the callback."""
105 logger.info('keyring_store_credentials: app_name "%s".', app_name)
106 Keyring(app_name).set_ubuntusso_attr(credentials)
107 callback(*cb_args)
108
109 gobject.idle_add(_inner)
103110
104111
105def keyring_get_credentials(app_name):112def keyring_get_credentials(app_name):
@@ -164,24 +171,27 @@
164171
165 return captcha['captcha_id']172 return captcha['captcha_id']
166173
167 def register_user(self, email, password, captcha_id, captcha_solution):174 def register_with_name(self, email, password, displayname,
175 captcha_id, captcha_solution):
168 """Register a new user with 'email' and 'password'."""176 """Register a new user with 'email' and 'password'."""
169 logger.debug('register_user: email: %r password: <hidden>, '177 logger.debug('register_with_name: email: %r password: <hidden>, '
170 'captcha_id: %r, captcha_solution: %r',178 'displayname: %r, captcha_id: %r, captcha_solution: %r',
171 email, captcha_id, captcha_solution)179 email, displayname, captcha_id, captcha_solution)
172 sso_service = self.sso_service_class(None, self.service_url)180 sso_service = self.sso_service_class(None, self.service_url)
173 if not self._valid_email(email):181 if not self._valid_email(email):
174 logger.error('register_user: InvalidEmailError for email: %r',182 logger.error('register_with_name: InvalidEmailError for email: %r',
175 email)183 email)
176 raise InvalidEmailError()184 raise InvalidEmailError()
177 if not self._valid_password(password):185 if not self._valid_password(password):
178 logger.error('register_user: InvalidPasswordError')186 logger.error('register_with_name: InvalidPasswordError')
179 raise InvalidPasswordError()187 raise InvalidPasswordError()
180188
181 result = sso_service.registrations.register(189 result = sso_service.registrations.register(
182 email=email, password=password, captcha_id=captcha_id,190 email=email, password=password,
191 displayname=displayname,
192 captcha_id=captcha_id,
183 captcha_solution=captcha_solution)193 captcha_solution=captcha_solution)
184 logger.info('register_user: email: %r result: %r', email, result)194 logger.info('register_with_name: email: %r result: %r', email, result)
185195
186 if result['status'] == 'error':196 if result['status'] == 'error':
187 errorsdict = self._format_webservice_errors(result['errors'])197 errorsdict = self._format_webservice_errors(result['errors'])
@@ -191,6 +201,17 @@
191 else:201 else:
192 return email202 return email
193203
204 def register_user(self, email, password,
205 captcha_id, captcha_solution):
206 """Register a new user with 'email' and 'password'."""
207 logger.debug('register_user: email: %r password: <hidden>, '
208 'captcha_id: %r, captcha_solution: %r',
209 email, captcha_id, captcha_solution)
210 res = self.register_with_name(email, password, displayname='',
211 captcha_id=captcha_id,
212 captcha_solution=captcha_solution)
213 return res
214
194 def login(self, email, password, token_name):215 def login(self, email, password, token_name):
195 """Login a user with 'email' and 'password'."""216 """Login a user with 'email' and 'password'."""
196 logger.debug('login: email: %r password: <hidden>, token_name: %r',217 logger.debug('login: email: %r password: <hidden>, token_name: %r',
@@ -209,6 +230,25 @@
209 'token_name: %r', credentials['consumer_key'], token_name)230 'token_name: %r', credentials['consumer_key'], token_name)
210 return credentials231 return credentials
211232
233 def is_validated(self, token, sso_service=None):
234 """Return if user with 'email' and 'password' is validated."""
235 logger.debug('is_validated: requesting accounts.me() info.')
236 if sso_service is None:
237 oauth_token = oauth.OAuthToken(token['token'],
238 token['token_secret'])
239 authorizer = OAuthAuthorizer(token['consumer_key'],
240 token['consumer_secret'],
241 oauth_token)
242 sso_service = self.sso_service_class(authorizer, self.service_url)
243
244 me_info = sso_service.accounts.me()
245 key = 'preferred_email'
246 result = key in me_info and me_info[key] != None
247
248 logger.info('is_validated: consumer_key: %r, result: %r.',
249 token['consumer_key'], result)
250 return result
251
212 def validate_email(self, email, password, email_token, token_name):252 def validate_email(self, email, password, email_token, token_name):
213 """Validate an email token for user with 'email' and 'password'."""253 """Validate an email token for user with 'email' and 'password'."""
214 logger.debug('validate_email: email: %r password: <hidden>, '254 logger.debug('validate_email: email: %r password: <hidden>, '
@@ -311,12 +351,8 @@
311 dbus.service.Object.__init__(self, object_path="/sso",351 dbus.service.Object.__init__(self, object_path="/sso",
312 bus_name=bus_name)352 bus_name=bus_name)
313 self.sso_login_processor_class = sso_login_processor_class353 self.sso_login_processor_class = sso_login_processor_class
314 self.sso_service_class = sso_service_class354 self.processor = self.sso_login_processor_class(
315355 sso_service_class=sso_service_class)
316 def processor(self):
317 """Create a login processor with the given class and service class."""
318 return self.sso_login_processor_class(
319 sso_service_class=self.sso_service_class)
320356
321 # generate_capcha signals357 # generate_capcha signals
322 @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")358 @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
@@ -337,7 +373,7 @@
337 """Call the matching method in the processor."""373 """Call the matching method in the processor."""
338 def f():374 def f():
339 """Inner function that will be run in a thread."""375 """Inner function that will be run in a thread."""
340 return self.processor().generate_captcha(filename)376 return self.processor.generate_captcha(filename)
341 blocking(f, app_name, self.CaptchaGenerated,377 blocking(f, app_name, self.CaptchaGenerated,
342 self.CaptchaGenerationError)378 self.CaptchaGenerationError)
343379
@@ -361,8 +397,19 @@
361 """Call the matching method in the processor."""397 """Call the matching method in the processor."""
362 def f():398 def f():
363 """Inner function that will be run in a thread."""399 """Inner function that will be run in a thread."""
364 return self.processor().register_user(email, password,400 return self.processor.register_user(email, password,
365 captcha_id, captcha_solution)401 captcha_id, captcha_solution)
402 blocking(f, app_name, self.UserRegistered, self.UserRegistrationError)
403
404 @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
405 in_signature='ssssss')
406 def register_with_name(self, app_name, email, password, name,
407 captcha_id, captcha_solution):
408 """Call the matching method in the processor."""
409 def f():
410 """Inner function that will be run in a thread."""
411 return self.processor.register_with_name(email, password, name,
412 captcha_id, captcha_solution)
366 blocking(f, app_name, self.UserRegistered, self.UserRegistrationError)413 blocking(f, app_name, self.UserRegistered, self.UserRegistrationError)
367414
368 # login signals415 # login signals
@@ -378,6 +425,12 @@
378 logger.debug('SSOLogin: emitting LoginError with '425 logger.debug('SSOLogin: emitting LoginError with '
379 'app_name "%s" and error %r', app_name, error)426 'app_name "%s" and error %r', app_name, error)
380427
428 @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
429 def UserNotValidated(self, app_name, result):
430 """Signal thrown when the user is not validated."""
431 logger.debug('SSOLogin: emitting UserNotValidated with app_name "%s" '
432 'and result %r', app_name, result)
433
381 @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,434 @dbus.service.method(dbus_interface=DBUS_IFACE_USER_NAME,
382 in_signature='sss')435 in_signature='sss')
383 def login(self, app_name, email, password):436 def login(self, app_name, email, password):
@@ -387,13 +440,21 @@
387 token_name = get_token_name(app_name)440 token_name = get_token_name(app_name)
388 logger.debug('login: token_name %r, email %r, password <hidden>.',441 logger.debug('login: token_name %r, email %r, password <hidden>.',
389 token_name, email)442 token_name, email)
390 credentials = self.processor().login(email, password, token_name)443 credentials = self.processor.login(email, password, token_name)
391 logger.debug('login returned not None credentials? %r.',444 logger.debug('login returned not None credentials? %r.',
392 credentials is not None)445 credentials is not None)
393 assert credentials is not None446 return credentials
394 keyring_store_credentials(app_name, credentials)447
395 return email448 def success_cb(app_name, credentials):
396 blocking(f, app_name, self.LoggedIn, self.LoginError)449 """Login finished successfull."""
450 is_validated = self.processor.is_validated(credentials)
451 logger.debug('user is validated? %r.', is_validated)
452 if is_validated:
453 keyring_store_credentials(app_name, credentials,
454 self.LoggedIn, app_name, email)
455 else:
456 self.UserNotValidated(app_name, email)
457 blocking(f, app_name, success_cb, self.LoginError)
397458
398 # validate_email signals459 # validate_email signals
399 @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")460 @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
@@ -415,11 +476,16 @@
415 def f():476 def f():
416 """Inner function that will be run in a thread."""477 """Inner function that will be run in a thread."""
417 token_name = get_token_name(app_name)478 token_name = get_token_name(app_name)
418 credentials = self.processor().validate_email(email, password,479 credentials = self.processor.validate_email(email, password,
419 email_token, token_name)480 email_token, token_name)
420 keyring_store_credentials(app_name, credentials)481
421 return email482 def _email_stored():
422 blocking(f, app_name, self.EmailValidated, self.EmailValidationError)483 """The email was stored, so call the signal."""
484 self.EmailValidated(app_name, email)
485
486 keyring_store_credentials(app_name, credentials, _email_stored)
487
488 blocking(f, app_name, NO_OP, self.EmailValidationError)
423489
424 # request_password_reset_token signals490 # request_password_reset_token signals
425 @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")491 @dbus.service.signal(DBUS_IFACE_USER_NAME, signature="ss")
@@ -440,7 +506,7 @@
440 """Call the matching method in the processor."""506 """Call the matching method in the processor."""
441 def f():507 def f():
442 """Inner function that will be run in a thread."""508 """Inner function that will be run in a thread."""
443 return self.processor().request_password_reset_token(email)509 return self.processor.request_password_reset_token(email)
444 blocking(f, app_name, self.PasswordResetTokenSent,510 blocking(f, app_name, self.PasswordResetTokenSent,
445 self.PasswordResetError)511 self.PasswordResetError)
446512
@@ -463,8 +529,8 @@
463 """Call the matching method in the processor."""529 """Call the matching method in the processor."""
464 def f():530 def f():
465 """Inner function that will be run in a thread."""531 """Inner function that will be run in a thread."""
466 return self.processor().set_new_password(email, token,532 return self.processor.set_new_password(email, token,
467 new_password)533 new_password)
468 blocking(f, app_name, self.PasswordChanged, self.PasswordChangeError)534 blocking(f, app_name, self.PasswordChanged, self.PasswordChangeError)
469535
470536
@@ -481,19 +547,19 @@
481 @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="s")547 @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="s")
482 def AuthorizationDenied(self, app_name):548 def AuthorizationDenied(self, app_name):
483 """Signal thrown when the user denies the authorization."""549 """Signal thrown when the user denies the authorization."""
484 logger.info('SSOLogin: emitting AuthorizationDenied with app_name '550 logger.info('SSOCredentials: emitting AuthorizationDenied with '
485 '"%s"', app_name)551 'app_name "%s"', app_name)
486552
487 @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="sa{ss}")553 @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="sa{ss}")
488 def CredentialsFound(self, app_name, credentials):554 def CredentialsFound(self, app_name, credentials):
489 """Signal thrown when the credentials are found."""555 """Signal thrown when the credentials are found."""
490 logger.info('SSOLogin: emitting CredentialsFound with app_name "%s"',556 logger.info('SSOCredentials: emitting CredentialsFound with '
491 app_name)557 'app_name "%s"', app_name)
492558
493 @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="sss")559 @dbus.service.signal(DBUS_IFACE_CRED_NAME, signature="sss")
494 def CredentialsError(self, app_name, error_message, detailed_error):560 def CredentialsError(self, app_name, error_message, detailed_error):
495 """Signal thrown when there is a problem finding the credentials."""561 """Signal thrown when there is a problem finding the credentials."""
496 logger.debug('SSOCredentials: emitting CredentialsError with app_name '562 logger.error('SSOCredentials: emitting CredentialsError with app_name '
497 '"%s" and error_message %r', app_name, error_message)563 '"%s" and error_message %r', app_name, error_message)
498564
499 @dbus.service.method(dbus_interface=DBUS_IFACE_CRED_NAME,565 @dbus.service.method(dbus_interface=DBUS_IFACE_CRED_NAME,
@@ -510,7 +576,8 @@
510576
511 def _login_success_cb(self, dialog, app_name, email):577 def _login_success_cb(self, dialog, app_name, email):
512 """Handles the response from the UI dialog."""578 """Handles the response from the UI dialog."""
513 logger.info('Login successful for app %r, email %r', app_name, email)579 logger.info('Login successful for app %r, email %r. Still pending to '
580 'ping server and send result signal.', app_name, email)
514 try:581 try:
515 creds = keyring_get_credentials(app_name)582 creds = keyring_get_credentials(app_name)
516 self._ping_url(app_name, email, creds)583 self._ping_url(app_name, email, creds)
@@ -518,6 +585,7 @@
518 except: # pylint: disable=W0702585 except: # pylint: disable=W0702
519 msg = "Problem getting the credentials from the keyring."586 msg = "Problem getting the credentials from the keyring."
520 logger.exception(msg)587 logger.exception(msg)
588 self.clear_token(app_name)
521 self.CredentialsError(app_name, msg, traceback.format_exc())589 self.CredentialsError(app_name, msg, traceback.format_exc())
522590
523 def _login_error_cb(self, dialog, app_name, error):591 def _login_error_cb(self, dialog, app_name, error):
@@ -639,6 +707,7 @@
639707
640 'app_name' is the name of the application.708 'app_name' is the name of the application.
641 """709 """
710 logger.info('Clearing credentials for app %r.', app_name)
642 try:711 try:
643 creds = Keyring(app_name)712 creds = Keyring(app_name)
644 creds.delete_ubuntusso_attr()713 creds.delete_ubuntusso_attr()
645714
=== modified file 'ubuntu_sso/tests/test_gui.py'
--- ubuntu_sso/tests/test_gui.py 2010-09-08 19:25:02 +0000
+++ ubuntu_sso/tests/test_gui.py 2011-04-12 19:01:16 +0000
@@ -63,7 +63,7 @@
63 self._args = args63 self._args = args
64 self._kwargs = kwargs64 self._kwargs = kwargs
65 self._called = {}65 self._called = {}
66 for i in ('generate_captcha', 'login', 'register_user',66 for i in ('generate_captcha', 'login', 'register_with_name',
67 'validate_email', 'request_password_reset_token',67 'validate_email', 'request_password_reset_token',
68 'set_new_password'):68 'set_new_password'):
69 setattr(self, i, self._record_call(i))69 setattr(self, i, self._record_call(i))
@@ -674,11 +674,11 @@
674 """Clicking 'join_ok_button' sends info to backend using 'register'."""674 """Clicking 'join_ok_button' sends info to backend using 'register'."""
675 self.click_join_with_valid_data()675 self.click_join_with_valid_data()
676676
677 # assert register_user was called677 # assert register_with_name was called
678 expected = 'register_user'678 expected = 'register_with_name'
679 self.assertIn(expected, self.ui.backend._called)679 self.assertIn(expected, self.ui.backend._called)
680 self.assertEqual(self.ui.backend._called[expected],680 self.assertEqual(self.ui.backend._called[expected],
681 ((APP_NAME, EMAIL, PASSWORD, CAPTCHA_ID,681 ((APP_NAME, EMAIL, PASSWORD, NAME, CAPTCHA_ID,
682 CAPTCHA_SOLUTION),682 CAPTCHA_SOLUTION),
683 dict(reply_handler=gui.NO_OP,683 dict(reply_handler=gui.NO_OP,
684 error_handler=gui.NO_OP)))684 error_handler=gui.NO_OP)))
@@ -856,6 +856,12 @@
856 self.ui.join_ok_button.clicked()856 self.ui.join_ok_button.clicked()
857 self.assertTrue(self._called)857 self.assertTrue(self._called)
858858
859 def test_user_and_pass_are_cached(self):
860 """Username and password are temporarly cached for further use."""
861 self.click_join_with_valid_data()
862 self.assertEqual(self.ui.user_email, EMAIL)
863 self.assertEqual(self.ui.user_password, PASSWORD)
864
859865
860class NoTermsAndConditionsTestCase(UbuntuSSOClientTestCase):866class NoTermsAndConditionsTestCase(UbuntuSSOClientTestCase):
861 """Test suite for the user registration (with no t&c link)."""867 """Test suite for the user registration (with no t&c link)."""
@@ -1005,6 +1011,17 @@
1005 dict(reply_handler=gui.NO_OP,1011 dict(reply_handler=gui.NO_OP,
1006 error_handler=gui.NO_OP)))1012 error_handler=gui.NO_OP)))
10071013
1014 def test_on_verify_token_button_clicked(self):
1015 """Verify token uses cached user_email and user_password."""
1016 self.ui.user_email = 'test@me.com'
1017 self.ui.user_password = 'yadda-yedda'
1018 self.ui.on_verify_token_button_clicked()
1019 self.assertEqual(self.ui.backend._called['validate_email'],
1020 ((APP_NAME, self.ui.user_email,
1021 self.ui.user_password, EMAIL_TOKEN),
1022 dict(reply_handler=gui.NO_OP,
1023 error_handler=gui.NO_OP)))
1024
1008 def test_on_verify_token_button_shows_processing_page(self):1025 def test_on_verify_token_button_shows_processing_page(self):
1009 """Verify token button triggers call to backend."""1026 """Verify token button triggers call to backend."""
1010 self.click_verify_email_with_valid_data()1027 self.click_verify_email_with_valid_data()
@@ -1079,7 +1096,7 @@
10791096
10801097
1081class VerifyEmailValidationTestCase(UbuntuSSOClientTestCase):1098class VerifyEmailValidationTestCase(UbuntuSSOClientTestCase):
1082 """Test suite for the user registration (verify email page)."""1099 """Test suite for the user registration validation (verify email page)."""
10831100
1084 def setUp(self):1101 def setUp(self):
1085 """Init."""1102 """Init."""
@@ -1114,6 +1131,20 @@
1114 self.assert_warnings_visibility()1131 self.assert_warnings_visibility()
11151132
11161133
1134class VerifyEmailLoginOnlyTestCase(VerifyEmailTestCase):
1135 """Test suite for the user login (verify email page)."""
1136
1137 kwargs = dict(app_name=APP_NAME, tc_uri=TC_URI, help_text=HELP_TEXT,
1138 login_only=True)
1139
1140
1141class VerifyEmailValidationLoginOnlyTestCase(VerifyEmailValidationTestCase):
1142 """Test suite for the user login validation (verify email page)."""
1143
1144 kwargs = dict(app_name=APP_NAME, tc_uri=TC_URI, help_text=HELP_TEXT,
1145 login_only=True)
1146
1147
1117class RegistrationValidationTestCase(UbuntuSSOClientTestCase):1148class RegistrationValidationTestCase(UbuntuSSOClientTestCase):
1118 """Test suite for the user registration validations."""1149 """Test suite for the user registration validations."""
11191150
@@ -1130,12 +1161,7 @@
11301161
1131 self.assert_correct_entry_warning(self.ui.name_entry,1162 self.assert_correct_entry_warning(self.ui.name_entry,
1132 self.ui.FIELD_REQUIRED)1163 self.ui.FIELD_REQUIRED)
1133 self.assertNotIn('register_user', self.ui.backend._called)1164 self.assertNotIn('register_with_name', self.ui.backend._called)
1134
1135 # Unused variable 'skip'
1136 # pylint: disable=W0612
1137 test_warning_is_shown_if_name_empty.skip = \
1138 'Unused for now, will be hidden to save space (LP: #627440).'
11391165
1140 def test_warning_is_shown_if_empty_email(self):1166 def test_warning_is_shown_if_empty_email(self):
1141 """A warning message is shown if emails are empty."""1167 """A warning message is shown if emails are empty."""
@@ -1148,7 +1174,7 @@
1148 self.ui.FIELD_REQUIRED)1174 self.ui.FIELD_REQUIRED)
1149 self.assert_correct_entry_warning(self.ui.email2_entry,1175 self.assert_correct_entry_warning(self.ui.email2_entry,
1150 self.ui.FIELD_REQUIRED)1176 self.ui.FIELD_REQUIRED)
1151 self.assertNotIn('register_user', self.ui.backend._called)1177 self.assertNotIn('register_with_name', self.ui.backend._called)
11521178
1153 def test_warning_is_shown_if_email_mismatch(self):1179 def test_warning_is_shown_if_email_mismatch(self):
1154 """A warning message is shown if emails doesn't match."""1180 """A warning message is shown if emails doesn't match."""
@@ -1161,7 +1187,7 @@
1161 self.ui.EMAIL_MISMATCH)1187 self.ui.EMAIL_MISMATCH)
1162 self.assert_correct_entry_warning(self.ui.email2_entry,1188 self.assert_correct_entry_warning(self.ui.email2_entry,
1163 self.ui.EMAIL_MISMATCH)1189 self.ui.EMAIL_MISMATCH)
1164 self.assertNotIn('register_user', self.ui.backend._called)1190 self.assertNotIn('register_with_name', self.ui.backend._called)
11651191
1166 def test_warning_is_shown_if_invalid_email(self):1192 def test_warning_is_shown_if_invalid_email(self):
1167 """A warning message is shown if email is invalid."""1193 """A warning message is shown if email is invalid."""
@@ -1174,7 +1200,7 @@
1174 self.ui.EMAIL_INVALID)1200 self.ui.EMAIL_INVALID)
1175 self.assert_correct_entry_warning(self.ui.email2_entry,1201 self.assert_correct_entry_warning(self.ui.email2_entry,
1176 self.ui.EMAIL_INVALID)1202 self.ui.EMAIL_INVALID)
1177 self.assertNotIn('register_user', self.ui.backend._called)1203 self.assertNotIn('register_with_name', self.ui.backend._called)
11781204
1179 def test_password_help_is_always_shown(self):1205 def test_password_help_is_always_shown(self):
1180 """Password help text is correctly displayed."""1206 """Password help text is correctly displayed."""
@@ -1182,7 +1208,7 @@
1182 'password help text is visible.')1208 'password help text is visible.')
1183 self.assertEqual(self.ui.password_help_label.get_text(),1209 self.assertEqual(self.ui.password_help_label.get_text(),
1184 self.ui.PASSWORD_HELP)1210 self.ui.PASSWORD_HELP)
1185 self.assertNotIn('register_user', self.ui.backend._called)1211 self.assertNotIn('register_with_name', self.ui.backend._called)
11861212
1187 def test_warning_is_shown_if_password_mismatch(self):1213 def test_warning_is_shown_if_password_mismatch(self):
1188 """A warning message is shown if password doesn't match."""1214 """A warning message is shown if password doesn't match."""
@@ -1195,7 +1221,7 @@
1195 self.ui.PASSWORD_MISMATCH)1221 self.ui.PASSWORD_MISMATCH)
1196 self.assert_correct_entry_warning(self.ui.password2_entry,1222 self.assert_correct_entry_warning(self.ui.password2_entry,
1197 self.ui.PASSWORD_MISMATCH)1223 self.ui.PASSWORD_MISMATCH)
1198 self.assertNotIn('register_user', self.ui.backend._called)1224 self.assertNotIn('register_with_name', self.ui.backend._called)
11991225
1200 def test_warning_is_shown_if_password_too_weak(self):1226 def test_warning_is_shown_if_password_too_weak(self):
1201 """A warning message is shown if password is too weak."""1227 """A warning message is shown if password is too weak."""
@@ -1210,7 +1236,7 @@
1210 self.ui.PASSWORD_TOO_WEAK)1236 self.ui.PASSWORD_TOO_WEAK)
1211 self.assert_correct_entry_warning(self.ui.password2_entry,1237 self.assert_correct_entry_warning(self.ui.password2_entry,
1212 self.ui.PASSWORD_TOO_WEAK)1238 self.ui.PASSWORD_TOO_WEAK)
1213 self.assertNotIn('register_user', self.ui.backend._called)1239 self.assertNotIn('register_with_name', self.ui.backend._called)
12141240
1215 def test_warning_is_shown_if_tc_not_accepted(self):1241 def test_warning_is_shown_if_tc_not_accepted(self):
1216 """A warning message is shown if TC are not accepted."""1242 """A warning message is shown if TC are not accepted."""
@@ -1221,7 +1247,7 @@
12211247
1222 self.assert_correct_label_warning(self.ui.tc_warning_label,1248 self.assert_correct_label_warning(self.ui.tc_warning_label,
1223 self.ui.TC_NOT_ACCEPTED)1249 self.ui.TC_NOT_ACCEPTED)
1224 self.assertNotIn('register_user', self.ui.backend._called)1250 self.assertNotIn('register_with_name', self.ui.backend._called)
12251251
1226 def test_warning_is_shown_if_not_captcha_solution(self):1252 def test_warning_is_shown_if_not_captcha_solution(self):
1227 """A warning message is shown if TC are not accepted."""1253 """A warning message is shown if TC are not accepted."""
@@ -1232,7 +1258,7 @@
12321258
1233 self.assert_correct_entry_warning(self.ui.captcha_solution_entry,1259 self.assert_correct_entry_warning(self.ui.captcha_solution_entry,
1234 self.ui.FIELD_REQUIRED)1260 self.ui.FIELD_REQUIRED)
1235 self.assertNotIn('register_user', self.ui.backend._called)1261 self.assertNotIn('register_with_name', self.ui.backend._called)
12361262
1237 def test_no_warning_messages_if_valid_data(self):1263 def test_no_warning_messages_if_valid_data(self):
1238 """No warning messages are shown if the data is valid."""1264 """No warning messages are shown if the data is valid."""
@@ -1328,6 +1354,12 @@
1328 self.ui.on_login_error(app_name=APP_NAME, error=self.error)1354 self.ui.on_login_error(app_name=APP_NAME, error=self.error)
1329 self.assert_pages_visibility(login=True)1355 self.assert_pages_visibility(login=True)
13301356
1357 def test_on_user_not_validated_morphs_to_verify_page(self):
1358 """On user not validated, the verify page is shown."""
1359 self.click_connect_with_valid_data()
1360 self.ui.on_user_not_validated(app_name=APP_NAME, email=EMAIL)
1361 self.assert_pages_visibility(verify_email=True)
1362
1331 def test_on_login_error_a_warning_is_shown(self):1363 def test_on_login_error_a_warning_is_shown(self):
1332 """On user login error, a warning is shown with proper wording."""1364 """On user login error, a warning is shown with proper wording."""
1333 self.click_connect_with_valid_data()1365 self.click_connect_with_valid_data()
@@ -1377,6 +1409,12 @@
1377 self.ui.login_ok_button.clicked()1409 self.ui.login_ok_button.clicked()
1378 self.assertTrue(self._called)1410 self.assertTrue(self._called)
13791411
1412 def test_user_and_pass_are_cached(self):
1413 """Username and password are temporarly cached for further use."""
1414 self.click_connect_with_valid_data()
1415 self.assertEqual(self.ui.user_email, EMAIL)
1416 self.assertEqual(self.ui.user_password, PASSWORD)
1417
13801418
1381class LoginValidationTestCase(UbuntuSSOClientTestCase):1419class LoginValidationTestCase(UbuntuSSOClientTestCase):
1382 """Test suite for the user login validation."""1420 """Test suite for the user login validation."""
@@ -1782,7 +1820,7 @@
1782 """All the backend signals are listed to be binded."""1820 """All the backend signals are listed to be binded."""
1783 for sig in ('CaptchaGenerated', 'CaptchaGenerationError',1821 for sig in ('CaptchaGenerated', 'CaptchaGenerationError',
1784 'UserRegistered', 'UserRegistrationError',1822 'UserRegistered', 'UserRegistrationError',
1785 'LoggedIn', 'LoginError',1823 'LoggedIn', 'LoginError', 'UserNotValidated',
1786 'EmailValidated', 'EmailValidationError',1824 'EmailValidated', 'EmailValidationError',
1787 'PasswordResetTokenSent', 'PasswordResetError',1825 'PasswordResetTokenSent', 'PasswordResetError',
1788 'PasswordChanged', 'PasswordChangeError'):1826 'PasswordChanged', 'PasswordChangeError'):
@@ -1864,6 +1902,13 @@
1864 self.ui._signals['LoginError'](mismatch_app_name, 'dummy')1902 self.ui._signals['LoginError'](mismatch_app_name, 'dummy')
1865 self.assertFalse(self._called)1903 self.assertFalse(self._called)
18661904
1905 def test_on_user_not_validated_is_not_called(self):
1906 """on_user_not_validated is not called if incorrect app_name."""
1907 self.patch(self.ui, 'on_user_not_validated', self._set_called)
1908 mismatch_app_name = self.ui.app_name * 2
1909 self.ui._signals['UserNotValidated'](mismatch_app_name, 'dummy')
1910 self.assertFalse(self._called)
1911
1867 def test_on_password_reset_token_sent_is_not_called(self):1912 def test_on_password_reset_token_sent_is_not_called(self):
1868 """on_password_reset_token_sent is not called if incorrect app_name."""1913 """on_password_reset_token_sent is not called if incorrect app_name."""
1869 self.patch(self.ui, 'on_password_reset_token_sent', self._set_called)1914 self.patch(self.ui, 'on_password_reset_token_sent', self._set_called)
18701915
=== modified file 'ubuntu_sso/tests/test_main.py'
--- ubuntu_sso/tests/test_main.py 2010-09-08 19:25:02 +0000
+++ ubuntu_sso/tests/test_main.py 2011-04-12 19:01:16 +0000
@@ -1,3 +1,5 @@
1# -*- coding: utf-8 -*-
2#
1# test_main - tests for ubuntu_sso.main3# test_main - tests for ubuntu_sso.main
2#4#
3# Author: Natalia Bidart <natalia.bidart@canonical.com>5# Author: Natalia Bidart <natalia.bidart@canonical.com>
@@ -63,6 +65,7 @@
63EMAIL_ALREADY_REGISTERED = 'a@example.com'65EMAIL_ALREADY_REGISTERED = 'a@example.com'
64EMAIL_TOKEN = 'B2Pgtf'66EMAIL_TOKEN = 'B2Pgtf'
65HELP = 'help text'67HELP = 'help text'
68NAME = 'Juanito PĂ©rez'
66PASSWORD = 'be4tiFul'69PASSWORD = 'be4tiFul'
67RESET_PASSWORD_TOKEN = '8G5Wtq'70RESET_PASSWORD_TOKEN = '8G5Wtq'
68TOKEN = {u'consumer_key': u'xQ7xDAz',71TOKEN = {u'consumer_key': u'xQ7xDAz',
@@ -107,7 +110,8 @@
107class FakedRegistrations(object):110class FakedRegistrations(object):
108 """Fake the registrations service."""111 """Fake the registrations service."""
109112
110 def register(self, email, password, captcha_id, captcha_solution):113 def register(self, email, password, displayname,
114 captcha_id, captcha_solution):
111 """Fake registration. Return a fix result."""115 """Fake registration. Return a fix result."""
112 if email == EMAIL_ALREADY_REGISTERED:116 if email == EMAIL_ALREADY_REGISTERED:
113 return {'status': 'error',117 return {'status': 'error',
@@ -152,6 +156,9 @@
152class FakedAccounts(object):156class FakedAccounts(object):
153 """Fake the accounts service."""157 """Fake the accounts service."""
154158
159 def __init__(self):
160 self.preferred_email = EMAIL
161
155 def validate_email(self, email_token):162 def validate_email(self, email_token):
156 """Fake the email validation. Return a fix result."""163 """Fake the email validation. Return a fix result."""
157 if email_token is None:164 if email_token is None:
@@ -164,6 +171,17 @@
164 else:171 else:
165 return STATUS_EMAIL_OK172 return STATUS_EMAIL_OK
166173
174 # pylint: disable=E0202, C0103
175
176 def me(self):
177 """Fake the 'me' information."""
178 return {u'username': u'Wh46bKY',
179 u'preferred_email': self.preferred_email,
180 u'displayname': u'',
181 u'unverified_emails': [u'aaaaaa@example.com'],
182 u'verified_emails': [],
183 u'openid_identifier': u'Wh46bKY'}
184
167185
168class FakedSSOServer(object):186class FakedSSOServer(object):
169 """Fake an SSO server."""187 """Fake an SSO server."""
@@ -279,6 +297,29 @@
279 result = self.processor.login(**self.login_kwargs)297 result = self.processor.login(**self.login_kwargs)
280 self.assertEqual(TOKEN, result, 'authentication was successful.')298 self.assertEqual(TOKEN, result, 'authentication was successful.')
281299
300 # is_validated
301
302 def test_is_validated(self):
303 """If preferred email is not None, user is validated."""
304 result = self.processor.is_validated(token=TOKEN)
305 self.assertTrue(result, 'user must be validated.')
306
307 def test_is_not_validated(self):
308 """If preferred email is None, user is not validated."""
309 service = FakedSSOServer(None, None)
310 service.accounts.preferred_email = None
311 result = self.processor.is_validated(sso_service=service,
312 token=TOKEN)
313 self.assertFalse(result, 'user must not be validated.')
314
315 def test_is_not_validated_empty_result(self):
316 """If preferred email is None, user is not validated."""
317 service = FakedSSOServer(None, None)
318 service.accounts.me = lambda: {}
319 result = self.processor.is_validated(sso_service=service,
320 token=TOKEN)
321 self.assertFalse(result, 'user must not be validated.')
322
282 # validate_email323 # validate_email
283324
284 def test_validate_email_if_status_ok(self):325 def test_validate_email_if_status_ok(self):
@@ -362,6 +403,16 @@
362 self.assertIn('Received invalid reply: %s' % STATUS_UNKNOWN, exc)403 self.assertIn('Received invalid reply: %s' % STATUS_UNKNOWN, exc)
363404
364405
406class SSORegistrationWithNameTestCase(SSOLoginProcessorTestCase):
407 """Test suite for the SSO login processor for register_with_name."""
408
409 def setUp(self):
410 """Init."""
411 super(SSORegistrationWithNameTestCase, self).setUp()
412 self.register_kwargs['displayname'] = NAME
413 self.processor.register_user = self.processor.register_with_name
414
415
365class BlockingSampleException(Exception):416class BlockingSampleException(Exception):
366 """The exception that will be thrown by the fake blocking."""417 """The exception that will be thrown by the fake blocking."""
367418
@@ -380,12 +431,13 @@
380 mockbus._register_object_path(ARGS)431 mockbus._register_object_path(ARGS)
381 self.mockprocessorclass = None432 self.mockprocessorclass = None
382433
383 def ksc(k, val):434 def ksc(k, val, callback, *cb_args):
384 """Assert over token and app_name."""435 """Assert over token and app_name."""
385 self.assertEqual(k, APP_NAME)436 self.assertEqual(k, APP_NAME)
386 self.assertEqual(val, TOKEN)437 self.assertEqual(val, TOKEN)
387 self.keyring_was_set = True438 self.keyring_was_set = True
388 self.keyring_values = k, val439 self.keyring_values = k, val
440 callback(*cb_args)
389441
390 self.patch(ubuntu_sso.main, "keyring_store_credentials", ksc)442 self.patch(ubuntu_sso.main, "keyring_store_credentials", ksc)
391 self.keyring_was_set = False443 self.keyring_was_set = False
@@ -472,8 +524,8 @@
472 """Test that the register_user method works ok."""524 """Test that the register_user method works ok."""
473 d = Deferred()525 d = Deferred()
474 expected_result = "expected result"526 expected_result = "expected result"
475 self.create_mock_processor().register_user(EMAIL, PASSWORD, CAPTCHA_ID,527 self.create_mock_processor().register_user(EMAIL, PASSWORD,
476 CAPTCHA_SOLUTION)528 CAPTCHA_ID, CAPTCHA_SOLUTION)
477 self.mocker.result(expected_result)529 self.mocker.result(expected_result)
478 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)530 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
479 self.mocker.replay()531 self.mocker.replay()
@@ -496,8 +548,8 @@
496 """Test that the register_user method fails as expected."""548 """Test that the register_user method fails as expected."""
497 d = Deferred()549 d = Deferred()
498 expected_result = "expected result"550 expected_result = "expected result"
499 self.create_mock_processor().register_user(EMAIL, PASSWORD, CAPTCHA_ID,551 self.create_mock_processor().register_user(EMAIL, PASSWORD,
500 CAPTCHA_SOLUTION)552 CAPTCHA_ID, CAPTCHA_SOLUTION)
501 self.mocker.result(expected_result)553 self.mocker.result(expected_result)
502 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)554 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
503 self.mocker.replay()555 self.mocker.replay()
@@ -516,11 +568,62 @@
516 CAPTCHA_SOLUTION)568 CAPTCHA_SOLUTION)
517 return d569 return d
518570
571 def test_register_with_name(self):
572 """Test that the register_with_name method works ok."""
573 d = Deferred()
574 expected_result = "expected result"
575 self.create_mock_processor().register_with_name(EMAIL, PASSWORD, NAME,
576 CAPTCHA_ID, CAPTCHA_SOLUTION)
577 self.mocker.result(expected_result)
578 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
579 self.mocker.replay()
580
581 def verify(app_name, result):
582 """The actual test."""
583 self.assertEqual(result, expected_result)
584 self.assertEqual(app_name, APP_NAME)
585 d.callback(result)
586
587 client = SSOLogin(self.mockbusname,
588 sso_login_processor_class=self.mockprocessorclass)
589 self.patch(client, "UserRegistered", verify)
590 self.patch(client, "UserRegistrationError", d.errback)
591 client.register_with_name(APP_NAME, EMAIL, PASSWORD, NAME, CAPTCHA_ID,
592 CAPTCHA_SOLUTION)
593 return d
594
595 def test_register_with_name_error(self):
596 """Test that the register_with_name method fails as expected."""
597 d = Deferred()
598 expected_result = "expected result"
599 self.create_mock_processor().register_with_name(EMAIL, PASSWORD, NAME,
600 CAPTCHA_ID, CAPTCHA_SOLUTION)
601 self.mocker.result(expected_result)
602 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
603 self.mocker.replay()
604
605 def verify(app_name, errdict):
606 """The actual test."""
607 self.assertEqual(errdict["errtype"], "BlockingSampleException")
608 self.assertEqual(app_name, APP_NAME)
609 d.callback("Ok")
610
611 client = SSOLogin(self.mockbusname,
612 sso_login_processor_class=self.mockprocessorclass)
613 self.patch(client, "UserRegistered", d.errback)
614 self.patch(client, "UserRegistrationError", verify)
615 client.register_with_name(APP_NAME, EMAIL, PASSWORD, NAME, CAPTCHA_ID,
616 CAPTCHA_SOLUTION)
617 return d
618
519 def test_login(self):619 def test_login(self):
520 """Test that the login method works ok."""620 """Test that the login method works ok."""
521 d = Deferred()621 d = Deferred()
522 self.create_mock_processor().login(EMAIL, PASSWORD, TOKEN_NAME)622 processor = self.create_mock_processor()
623 processor.login(EMAIL, PASSWORD, TOKEN_NAME)
523 self.mocker.result(TOKEN)624 self.mocker.result(TOKEN)
625 processor.is_validated(TOKEN)
626 self.mocker.result(True)
524 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)627 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
525 self.mocker.replay()628 self.mocker.replay()
526629
@@ -535,13 +638,40 @@
535 sso_login_processor_class=self.mockprocessorclass)638 sso_login_processor_class=self.mockprocessorclass)
536 self.patch(client, "LoggedIn", verify)639 self.patch(client, "LoggedIn", verify)
537 self.patch(client, "LoginError", d.errback)640 self.patch(client, "LoginError", d.errback)
641 self.patch(client, "UserNotValidated", d.errback)
642 client.login(APP_NAME, EMAIL, PASSWORD)
643 return d
644
645 def test_login_user_not_validated(self):
646 """Test that the login sends EmailNotValidated signal."""
647 d = Deferred()
648 processor = self.create_mock_processor()
649 processor.login(EMAIL, PASSWORD, TOKEN_NAME)
650 self.mocker.result(TOKEN)
651 processor.is_validated(TOKEN)
652 self.mocker.result(False)
653 self.patch(ubuntu_sso.main, "blocking", self.fake_ok_blocking)
654 self.mocker.replay()
655
656 def verify(app_name, email):
657 """The actual test."""
658 self.assertEqual(app_name, APP_NAME)
659 self.assertEqual(email, EMAIL)
660 self.assertFalse(self.keyring_was_set, "Keyring should not be set")
661 d.callback("Ok")
662
663 client = SSOLogin(self.mockbusname,
664 sso_login_processor_class=self.mockprocessorclass)
665 self.patch(client, "LoggedIn", d.errback)
666 self.patch(client, "LoginError", d.errback)
667 self.patch(client, "UserNotValidated", verify)
538 client.login(APP_NAME, EMAIL, PASSWORD)668 client.login(APP_NAME, EMAIL, PASSWORD)
539 return d669 return d
540670
541 def test_login_error(self):671 def test_login_error(self):
542 """Test that the login method fails as expected."""672 """Test that the login method fails as expected."""
543 d = Deferred()673 d = Deferred()
544 self.mockprocessorclass = self.mocker.mock()674 self.create_mock_processor()
545 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)675 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
546676
547 def fake_gtn(*args):677 def fake_gtn(*args):
@@ -562,6 +692,7 @@
562 sso_login_processor_class=self.mockprocessorclass)692 sso_login_processor_class=self.mockprocessorclass)
563 self.patch(client, "LoggedIn", d.errback)693 self.patch(client, "LoggedIn", d.errback)
564 self.patch(client, "LoginError", verify)694 self.patch(client, "LoginError", verify)
695 self.patch(client, "UserNotValidated", d.errback)
565 client.login(APP_NAME, EMAIL, PASSWORD)696 client.login(APP_NAME, EMAIL, PASSWORD)
566 return d697 return d
567698
@@ -591,7 +722,7 @@
591 def test_validate_email_error(self):722 def test_validate_email_error(self):
592 """Test that the validate_email method fails as expected."""723 """Test that the validate_email method fails as expected."""
593 d = Deferred()724 d = Deferred()
594 self.mockprocessorclass = self.mocker.mock()725 self.create_mock_processor()
595 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)726 self.patch(ubuntu_sso.main, "blocking", self.fake_err_blocking)
596727
597 def fake_gtn(*args):728 def fake_gtn(*args):
@@ -802,7 +933,7 @@
802 self.assertEqual(result["errtype"], e.__class__.__name__)933 self.assertEqual(result["errtype"], e.__class__.__name__)
803934
804935
805class KeyringCredentialsTestCase(MockerTestCase):936class KeyringCredentialsTestCase(TestCase, MockerTestCase):
806 """Check the functions that access the keyring."""937 """Check the functions that access the keyring."""
807938
808 # Invalid name (should match ([a-z_][a-z0-9_]*|[A-Z_][A-Z0-9_]*)$)939 # Invalid name (should match ([a-z_][a-z0-9_]*|[A-Z_][A-Z0-9_]*)$)
@@ -810,15 +941,19 @@
810941
811 def test_keyring_store_cred(self):942 def test_keyring_store_cred(self):
812 """Verify the method that stores credentials."""943 """Verify the method that stores credentials."""
944 idle_add = lambda f, *args, **kwargs: f(*args, **kwargs)
945 self.patch(gobject, "idle_add", idle_add)
813 token_value = TOKEN946 token_value = TOKEN
814 mockKeyringClass = self.mocker.replace("ubuntu_sso.keyring.Keyring")947 mockKeyringClass = self.mocker.replace("ubuntu_sso.keyring.Keyring")
815 mockKeyringClass(APP_NAME)948 mockKeyringClass(APP_NAME)
816 mockKeyring = self.mocker.mock()949 mockKeyring = self.mocker.mock()
950 callback = self.mocker.mock()
817 self.mocker.result(mockKeyring)951 self.mocker.result(mockKeyring)
818 mockKeyring.set_ubuntusso_attr(token_value)952 mockKeyring.set_ubuntusso_attr(token_value)
953 callback(1, 2, 3)
819 self.mocker.replay()954 self.mocker.replay()
820955
821 keyring_store_credentials(APP_NAME, token_value)956 keyring_store_credentials(APP_NAME, token_value, callback, 1, 2, 3)
822957
823 def test_keyring_get_cred(self):958 def test_keyring_get_cred(self):
824 """The method returns the right token."""959 """The method returns the right token."""
@@ -1367,6 +1502,24 @@
1367 self.assertEqual(self.calls[0][0], 'CredentialsError')1502 self.assertEqual(self.calls[0][0], 'CredentialsError')
1368 self.assertEqual(self.calls[0][1][0], APP_NAME)1503 self.assertEqual(self.calls[0][1][0], APP_NAME)
13691504
1505 def test_credentials_are_not_stored_if_ping_failed(self):
1506 """Credentials are not stored if the ping fails."""
1507
1508 def fail(*args, **kwargs):
1509 """Raise an exception."""
1510 self.args = AssertionError((args, kwargs))
1511 # pylint: disable=E0702
1512 raise self.args
1513
1514 self.patch(self.client, '_ping_url', fail)
1515 self._patch('clear_token')
1516
1517 self.client._login_success_cb(None, APP_NAME, EMAIL)
1518
1519 self.assertEqual(len(self.calls), 1)
1520 self.assertEqual(self.calls[0][0], 'clear_token')
1521 self.assertEqual(self.calls[0][1][0], APP_NAME)
1522
13701523
1371class EnvironOverridesTestCase(TestCase):1524class EnvironOverridesTestCase(TestCase):
1372 """Some URLs can be set from the environment for testing/QA purposes."""1525 """Some URLs can be set from the environment for testing/QA purposes."""

Subscribers

People subscribed via source and target branches

to all changes: