Merge ~nteodosio/software-properties:ua-attach-tests into software-properties:ubuntu/master

Proposed by Nathan Teodosio
Status: Needs review
Proposed branch: ~nteodosio/software-properties:ua-attach-tests
Merge into: software-properties:ubuntu/master
Diff against target: 307 lines (+246/-4)
4 files modified
data/gtkbuilder/dialog-ua-attach.ui (+3/-3)
debian/control (+2/-0)
debian/rules (+1/-1)
tests/test_ua_attach.py (+240/-0)
Reviewer Review Type Date Requested Status
Sebastien Bacher Pending
Jeremy BĂ­cha debian/* Pending
Review via email: mp+438462@code.launchpad.net

Commit message

Add tests for gtk/DialogUaAttach.py.

Description of the change

Adds tests for Ubuntu Pro attachment in Software Properties.

PPA build: https://launchpad.net/~nteodosio/+archive/ubuntu/rebuilds/+build/25647866

To post a comment you must log in.
Revision history for this message
Sebastien Bacher (seb128) wrote :

not a review but just a note from a glance on the changes, if the new build-depends are only for tests you should add <!nocheck> next to those

9d3f38f... by Nathan Teodosio

Fix missing pin_label.

90017fa... by Nathan Teodosio

d/control: nocheck for the new build-depends

Unmerged commits

90017fa... by Nathan Teodosio

d/control: nocheck for the new build-depends

9d3f38f... by Nathan Teodosio

Fix missing pin_label.

7f7e9e2... by Nathan Teodosio

Add handy and ua-tools deps to d/control.

These are required in the source package, else the tests fail to import the modules and the build fails.

9d4c042... by Nathan Teodosio

Update dh_auto_test to use xvfb-run.

Needed for GTK, else:

  File "/home/nteodosio/canonical/ubuntu-advantage/software-properties/tests/test_ua_attach.py", line 40, in setUp
      self.d = DialogUaAttach(Gtk.Window(), "data", self.ua_object)

  File "/usr/lib/python3/dist-packages/gi/overrides/Gtk.py", line 519, in __init__
      raise RuntimeError(
      RuntimeError: Gtk couldn't be initialized. Use Gtk.init_check() if you want to handle this case.

20a7057... by Nathan Teodosio

Update the internet check, that didn't work

146d506... by Nathan Teodosio

Move test_gui_state_after_token_submission to WithoutInternet test suite.

e4ab9e2... by Nathan Teodosio

Linters

x

0562e9d... by Nathan Teodosio

Check with update_state(pin_validated) instead

1d1144b... by Nathan Teodosio

Test that PIN validation waits to update UI until magic radio is selected.

18ddda5... by Nathan Teodosio

Test that PIN validation waits until its radio is on.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/data/gtkbuilder/dialog-ua-attach.ui b/data/gtkbuilder/dialog-ua-attach.ui
2index 1445b82..d8a3dc2 100644
3--- a/data/gtkbuilder/dialog-ua-attach.ui
4+++ b/data/gtkbuilder/dialog-ua-attach.ui
5@@ -49,7 +49,7 @@
6 <property name="orientation">horizontal</property>
7 <child>
8 <object class="GtkLabel" id="pin_label">
9- <property name="label"> </property>
10+ <property name="label"></property>
11 <property name="visible">True</property>
12 <property name="selectable">True</property>
13 <property name="can-focus">False</property>
14@@ -85,7 +85,7 @@
15 <child>
16 <object class="GtkLabel" id="pin_status">
17 <property name="margin">3</property>
18- <property name="visible">True</property>
19+ <property name="visible">False</property>
20 <property name="valign">center</property>
21 <property name="use-markup">True</property>
22 </object>
23@@ -147,7 +147,7 @@
24 <child>
25 <object class="GtkLabel" id="token_status">
26 <property name="margin">3</property>
27- <property name="visible">True</property>
28+ <property name="visible">False</property>
29 <property name="valign">center</property>
30 <property name="use-markup">True</property>
31 </object>
32diff --git a/debian/control b/debian/control
33index fcb6123..295d1c7 100644
34--- a/debian/control
35+++ b/debian/control
36@@ -7,6 +7,7 @@ Build-Depends: dbus-x11 <!nocheck>,
37 dh-python,
38 dh-translations,
39 gir1.2-gtk-3.0 <!nocheck>,
40+ gir1.2-handy-1 <!nocheck>,
41 gpg,
42 gpg-agent,
43 intltool,
44@@ -22,6 +23,7 @@ Build-Depends: dbus-x11 <!nocheck>,
45 python3-gi <!nocheck>,
46 python3-launchpadlib,
47 python3-setuptools,
48+ ubuntu-advantage-tools (>= 27.11~) <!nocheck>,
49 xauth <!nocheck>,
50 xvfb <!nocheck>
51 Rules-Requires-Root: no
52diff --git a/debian/rules b/debian/rules
53index ccc571e..08419e2 100755
54--- a/debian/rules
55+++ b/debian/rules
56@@ -7,4 +7,4 @@ override_dh_missing:
57 dh_missing --list-missing
58
59 override_dh_auto_test:
60- dbus-run-session -- dh_auto_test
61+ dbus-run-session -- xvfb-run -a dh_auto_test
62diff --git a/tests/test_ua_attach.py b/tests/test_ua_attach.py
63new file mode 100644
64index 0000000..85b57d4
65--- /dev/null
66+++ b/tests/test_ua_attach.py
67@@ -0,0 +1,240 @@
68+#!/usr/bin/python3
69+# pylint: disable=E1101
70+"""Test cases for DialogUaAttach.py"""
71+
72+import sys
73+import unittest
74+from unittest.mock import ANY, Mock, patch
75+from uaclient.api.u.pro.attach.magic.initiate.v1 import initiate
76+from uaclient.exceptions import MagicAttachTokenError
77+from softwareproperties.gtk.DialogUaAttach import DialogUaAttach
78+from gi.repository import GLib, Gtk
79+
80+sys.path.insert(0, "..")
81+
82+
83+class InitiateResponse:
84+ """Mock a good response of the Ubuntu Pro API's initiate()"""
85+
86+ def __init__(self, pin, req_id):
87+ self.user_code = pin
88+ self.token = req_id
89+
90+
91+class IncompleteInitiateResponse:
92+ """Mock a "something has gone wrong" response of the Ubuntu Pro API's
93+ initiate()"""
94+
95+
96+class WaitResponse:
97+ """Mock a good response of the Ubuntu Pro API's wait()"""
98+
99+ contract_token = "contract_token"
100+
101+
102+class WithoutInternetAttempt(unittest.TestCase):
103+ """Tests in which net_status_changed is patched away."""
104+
105+ @patch("softwareproperties.gtk.DialogUaAttach.DialogUaAttach.net_status_changed")
106+ def setUp(self, net_mock):
107+ self.ua_object = Mock()
108+ self.d = DialogUaAttach(Gtk.Window(), "data", self.ua_object)
109+ net_mock.assert_called()
110+
111+ def test_clean_init(self):
112+ """Test the initial values of the class."""
113+
114+ self.assertEqual(self.d.pin, "")
115+ self.assertEqual(self.d.poll, None)
116+ self.assertEqual(self.d.contract_token, None)
117+ self.assertEqual(self.d.attaching, False)
118+
119+ @patch("gi.repository.GLib.Thread.new")
120+ @patch("softwareproperties.gtk.DialogUaAttach.DialogUaAttach.start_magic_attach")
121+ def test_net_status_changed(self, sma, gtn):
122+ """Test net_status_changed."""
123+
124+ def check_widgets_visibility_and_sensitiviness(net):
125+ self.assertEqual(self.d.no_connection.get_visible(), not net)
126+ self.assertEqual(self.d.radio_net_control_box.get_sensitive(), net)
127+ self.assertEqual(self.d.confirm_net_control_box.get_sensitive(), net)
128+
129+ # Widgets visibility and sensitiviness are consistent with internet
130+ # connection state
131+ net = False
132+ self.d.net_status_changed(Mock(), net, Mock())
133+ check_widgets_visibility_and_sensitiviness(net)
134+ net = True
135+ self.d.net_status_changed(Mock(), net, Mock())
136+ check_widgets_visibility_and_sensitiviness(net)
137+
138+ def net_status_changed_to(net, pin="", poll=None):
139+ sma.reset_mock()
140+ gtn.reset_mock()
141+ self.d.pin = pin
142+ self.d.poll = poll
143+ self.d.net_status_changed(Mock(), net, Mock())
144+
145+ # Internet down, no polling is started or restart
146+ net_status_changed_to(False)
147+ sma.assert_not_called()
148+ gtn.assert_not_called()
149+ self.assertIsNone(self.d.poll)
150+
151+ # Internet up but no PIN, request one and start poll
152+ net_status_changed_to(True)
153+ sma.assert_called_once()
154+ gtn.assert_not_called()
155+ self.assertIsNone(self.d.poll)
156+
157+ # Internet up but no poll, restart poll
158+ net_status_changed_to(True, "pin")
159+ sma.assert_not_called()
160+ gtn.assert_called_once()
161+ self.assertIsNotNone(self.d.poll)
162+
163+ # Internet up; Already have a PIN and is polling for it, do nothing
164+ net_status_changed_to(True, "pin", "poll")
165+ sma.assert_not_called()
166+ gtn.assert_not_called()
167+ self.assertIsNotNone(self.d.poll)
168+
169+ @patch("gi.repository.GLib.idle_add")
170+ @patch("softwareproperties.gtk.DialogUaAttach.wait")
171+ def test_poll_for_magic_token(self, wait, idle_add):
172+ """Test poll_for_magic_token."""
173+ wait.side_effect = [WaitResponse, MagicAttachTokenError, AttributeError]
174+
175+ self.d.req_id = "req_id"
176+
177+ idle_add.reset_mock()
178+ self.d.poll = 1
179+ self.d.poll_for_magic_token()
180+ idle_add.assert_called_with(ANY, "pin_validated")
181+ self.assertIsNone(self.d.poll)
182+
183+ idle_add.reset_mock()
184+ self.d.poll = 1
185+ self.d.poll_for_magic_token()
186+ idle_add.assert_called_with(ANY, "expired")
187+ self.assertIsNone(self.d.poll)
188+
189+ idle_add.reset_mock()
190+ self.d.poll = 1
191+ self.d.poll_for_magic_token()
192+ idle_add.assert_not_called()
193+ self.assertIsNone(self.d.poll)
194+
195+ @patch("softwareproperties.gtk.DialogUaAttach.initiate")
196+ def test_start_magic_attach(self, initiate):
197+ """Test start_magic_attach."""
198+
199+ def start_magic_attach_with(poll, contract_token):
200+ self.d.poll = poll
201+ self.d.contract_token = contract_token
202+ self.d.start_magic_attach()
203+
204+ # If already polling, magic attach flow is not initiated
205+ start_magic_attach_with("poll", None)
206+ initiate.assert_not_called()
207+
208+ # If already have contract_token, magic attach flow is not initiated
209+ start_magic_attach_with(None, "contract_token")
210+ initiate.assert_not_called()
211+
212+ pin = "pin"
213+ req_id = "req_id"
214+ initiate.side_effect = [
215+ IncompleteInitiateResponse,
216+ InitiateResponse(pin, req_id),
217+ ]
218+
219+ # Incomplete response from initate(): No poll started
220+ start_magic_attach_with(None, None)
221+ initiate.assert_called()
222+ self.assertIsNone(self.d.poll)
223+
224+ # Appropriate response from initate(): Poll started
225+ start_magic_attach_with(None, None)
226+ self.assertEqual(pin, self.d.pin)
227+ self.assertEqual(req_id, self.d.req_id)
228+ self.assertIsNotNone(self.d.poll)
229+
230+ @patch("gi.repository.GLib.idle_add")
231+ @patch("softwareproperties.gtk.DialogUaAttach.initiate")
232+ @patch("softwareproperties.gtk.DialogUaAttach.wait")
233+ def test_pin_validation_waits_until_magic_radio_active(
234+ self, wait, initiate, idle_add
235+ ):
236+ """PIN verification should not take over GUI if other radio is active
237+ Steps performed for verification:
238+ Click token radio button.
239+ Start polling for magic token.
240+ Verify the PIN (mocking ubuntu.com/pro/attach verification).
241+ > Assert that nothing happens just yet.
242+ Click the first radio button.
243+ > Assert that _now_ GUI is updated.
244+ """
245+ wait.return_value = WaitResponse
246+ initiate.return_value = InitiateResponse("pin", "req_id")
247+ self.d.token_radio.set_active(True)
248+ self.d.update_state = Mock(side_effect=self.d.update_state)
249+ self.d.start_magic_attach()
250+ GLib.usleep(1_500_000)
251+ self.assertEqual(WaitResponse.contract_token, self.d.contract_token)
252+ idle_add.assert_not_called()
253+ self.d.magic_radio.set_active(True)
254+ self.d.update_state.assert_called_once_with("pin_validated")
255+
256+ @patch("softwareproperties.gtk.DialogUaAttach.DialogUaAttach.finish")
257+ def test_gui_state_after_token_submission(self, finish):
258+ """Test the interface widgets after submitting an invalid token
259+ via the manual token field."""
260+ self.d.token_radio.set_active(True)
261+ self.d.token_field.set_text("bar")
262+ self.d.attach()
263+
264+ # Test checking token
265+ # Unfortunately can't test spinner, get_spinning only available in GTK4
266+ self.assertFalse(self.d.confirm.get_sensitive())
267+ self.assertFalse(self.d.token_radio.get_sensitive())
268+ self.assertFalse(self.d.magic_radio.get_sensitive())
269+
270+ _first_arg, named_args = self.ua_object.Attach.call_args_list[0]
271+
272+ # Test invalid token
273+ named_args["error_handler"]("foo")
274+ self.assertTrue(self.d.token_status.get_visible())
275+ self.assertTrue(self.d.token_status_icon.get_visible())
276+ icon_name, _icon_size = self.d.token_status_icon.get_icon_name()
277+ self.assertEqual(icon_name, "emblem-unreadable")
278+
279+ # Test valid token
280+ finish.assert_not_called()
281+ named_args["reply_handler"]()
282+ finish.assert_called_once()
283+
284+
285+class WithInternetAttempt(unittest.TestCase):
286+ """Tests in which net_status_changed is not patched away."""
287+
288+ def setUp(self):
289+ self.ua_object = Mock()
290+ self.d = DialogUaAttach(Gtk.Window(), "data", self.ua_object)
291+
292+ def test_gui_initial_state(self):
293+ """Test the interface widgets initial state."""
294+ self.assertTrue(self.d.magic_radio.get_active())
295+ self.assertFalse(self.d.token_radio.get_active())
296+ self.assertFalse(self.d.token_field.get_sensitive())
297+ self.assertFalse(self.d.confirm.get_sensitive())
298+ self.assertFalse(self.d.token_status_icon.get_visible())
299+ self.assertFalse(self.d.token_status.get_visible())
300+ self.assertFalse(self.d.pin_status_icon.get_visible())
301+ self.assertFalse(self.d.pin_status.get_visible())
302+
303+ try:
304+ initiate()
305+ self.assertTrue(len(self.d.pin_label.get_text()) == 6)
306+ except:
307+ self.assertEqual(self.d.pin_label.get_text(), "")

Subscribers

People subscribed via source and target branches