Merge lp:~rhuddie/ubuntu-push/push-autopilot-tests into lp:ubuntu-push

Proposed by Richard Huddie
Status: Superseded
Proposed branch: lp:~rhuddie/ubuntu-push/push-autopilot-tests
Merge into: lp:ubuntu-push
Diff against target: 545 lines (+506/-0)
7 files modified
tests/autopilot/push_notifications/README (+63/-0)
tests/autopilot/push_notifications/__init__.py (+8/-0)
tests/autopilot/push_notifications/config/__init__.py (+19/-0)
tests/autopilot/push_notifications/config/push.conf (+23/-0)
tests/autopilot/push_notifications/data.py (+81/-0)
tests/autopilot/push_notifications/tests/__init__.py (+239/-0)
tests/autopilot/push_notifications/tests/test_push_client.py (+73/-0)
To merge this branch: bzr merge lp:~rhuddie/ubuntu-push/push-autopilot-tests
Reviewer Review Type Date Requested Status
Leo Arias (community) Needs Fixing
VĂ­ctor R. Ruiz Pending
Allan LeSage Pending
Review via email: mp+215921@code.launchpad.net

This proposal has been superseded by a proposal from 2014-04-22.

Description of the change

A new autopilot test framework for testing push notifications.

The tests will configure the client to run against a specified push server and then send through a push notification message which is displayed on device's screen.

Currently the autopilot code for validating the notifications is not completed.

To post a comment you must log in.
Revision history for this message
Leo Arias (elopio) wrote :

Hey Richard. You need to resubmit your MP against lp:ubuntu-push/automatic instead of trunk. I'll forward you the mail pedronis sent about this.

review: Needs Fixing
Revision history for this message
Leo Arias (elopio) wrote :

53 +5) autopilot3 list push-notifications
54 +6) autopilot3 run push-notifications

You have a typo there ^. It should be push_notifications.

166 +class PushNotificationMessage:
187 +class NotificationData:

I would put these classes in a push_notifications/data.py module.

210 + if dbus_info != None:
215 + elif copy_obj != None:

You should make checks like these with 'is not' instead of !=.
That's from pep8: "Comparisons to singletons like None should always be done with is or is not, never the equality operators."

203 + def __init__(self, dbus_info=None, copy_obj=None):

I find this to be a confusing constructor. I think it would be clearer to make two @classmethods, like
def from_dbus_info(dbus_info=None)
def copy(original_object=None)

Actually, I'm not sure you would need that copy. You could use the copy module.

review: Needs Fixing
110. By Richard Huddie

update for review comments

111. By Richard Huddie

fix pep8

112. By Richard Huddie

remove dbus dependency so it can run without root

113. By Richard Huddie

unity8 dialog validation

114. By Richard Huddie

added some new tests including locked greeter

115. By Richard Huddie

re-structure tests so that client is not re-started before each test

116. By Richard Huddie

restructure to use helper classes

117. By Richard Huddie

tidy and add test for device screen off

118. By Richard Huddie

fix flake8

119. By Richard Huddie

clear notifications before running test and move stuff into base class

120. By Richard Huddie

added cert_pem_file configuration to client

121. By Richard Huddie

certificate file path update

122. By Richard Huddie

rename tests for broadcast

123. By Richard Huddie

minor change

124. By Richard Huddie

tidy up

125. By Richard Huddie

update dialog assertion

126. By Richard Huddie

tidy up and add documentation to methods

127. By Richard Huddie

remove comment

128. By Richard Huddie

Readme updates

129. By Richard Huddie

disable mocking and minor updates

130. By Richard Huddie

move display message from base class

131. By Richard Huddie

fix review comments

132. By Richard Huddie

fix review comment

133. By Richard Huddie

Copyright header updates

134. By Richard Huddie

split greeter changes

135. By Richard Huddie

tidied up and added extra test

136. By Richard Huddie

merge with trunk

137. By Richard Huddie

update comment

138. By Richard Huddie

revert split greeter changes

139. By Richard Huddie

use /sbin/initctl

140. By Richard Huddie

update readme including emulator info

141. By Richard Huddie

wait for notification to dismiss automatically

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'tests'
2=== added directory 'tests/autopilot'
3=== added directory 'tests/autopilot/push_notifications'
4=== added file 'tests/autopilot/push_notifications/README'
5--- tests/autopilot/push_notifications/README 1970-01-01 00:00:00 +0000
6+++ tests/autopilot/push_notifications/README 2014-04-22 14:02:46 +0000
7@@ -0,0 +1,63 @@
8+==================
9+README
10+==================
11+
12+To run ubuntu-push autopilot tests you need to have a push server available. This can be running locally using loopback (127.0.0.1) or remotely on the same network.
13+
14+----------------------------------
15+To configure and build the server:
16+----------------------------------
17+
18+1) export GOPATH=${PWD}/push
19+2) mkdir -p push/src/launchpad.net
20+3) cd push/src/launchpad.net
21+4) bzr branch lp:ubuntu-push
22+5) Edit ubuntu-push/sampleconfigs/dev.json:
23+ "addr": "192.168.1.2:9090",
24+ The ip should be changed to match your current environment
25+ - use 127.0.0.1 if the client is also going to be running on the same machine as the server
26+ - use real ip address if the client is going to be running on a different machine on the same network
27+5) cd ubuntu-push
28+6) make bootstrap
29+7) make run-server-dev
30+ Following output should be observed:
31+ INFO listening for http on 192.168.1.2:8080
32+ INFO listening for devices on 192.168.1.2:9090
33+
34+------------------------
35+To configure the client:
36+------------------------
37+
38+Note: Tests must be run as root user
39+
40+1) sudo apt-get install ubuntu-push-client
41+2) bzr branch lp:ubuntu-push
42+3) Edit ubuntu-push/tests/autopilot/push_notifications/config/push.conf:
43+ [default]
44+ environment = remote
45+ [remote]
46+ addr = 192.168.1.2
47+ listener_port = 8080
48+ device_port = 9090
49+
50+ The environment can be toggled using the 'environment' setting under [default] section. The addr and port settings for the chosen environment shold match required.
51+
52+4) cd ubuntu-push/tests/autopilot
53+5) autopilot3 list push_notifications
54+6) autopilot3 run push_notifications
55+
56+----------------
57+Troubleshooting:
58+----------------
59+
60+1) Ping from client to server to ensure connectivity is correct
61+2) Delete .local/share/ubuntu-push-client/levels.db if no notifications are being displayed
62+3) To send a notification manually:
63+ echo '{"channel":"system", "data": {"ubuntu-touch/trusty-proposed/mako": [297, ""]}, "expire_on": "2015-12-19T16:39:57-08:00"}' | POST -c application/json http://192.168.1.2:8080/broadcast
64+ Response should be:
65+ {"ok":true}
66+ Note that:
67+ - The channel and device names must match the client.
68+ - The build number must be greater than current installed version in order to trigger an update message.
69+ - The expiration time must be in the future.
70+
71
72=== added file 'tests/autopilot/push_notifications/__init__.py'
73--- tests/autopilot/push_notifications/__init__.py 1970-01-01 00:00:00 +0000
74+++ tests/autopilot/push_notifications/__init__.py 2014-04-22 14:02:46 +0000
75@@ -0,0 +1,8 @@
76+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
77+# Copyright 2014 Canonical
78+#
79+# This program is free software: you can redistribute it and/or modify it
80+# under the terms of the GNU General Public License version 3, as published
81+# by the Free Software Foundation.
82+
83+"""push-notifications autopilot tests."""
84
85=== added directory 'tests/autopilot/push_notifications/config'
86=== added file 'tests/autopilot/push_notifications/config/__init__.py'
87--- tests/autopilot/push_notifications/config/__init__.py 1970-01-01 00:00:00 +0000
88+++ tests/autopilot/push_notifications/config/__init__.py 2014-04-22 14:02:46 +0000
89@@ -0,0 +1,19 @@
90+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
91+# Copyright 2014 Canonical
92+#
93+# This program is free software: you can redistribute it and/or modify it
94+# under the terms of the GNU General Public License version 3, as published
95+# by the Free Software Foundation.
96+
97+
98+import os
99+
100+CONFIG_FILE = 'push.conf'
101+
102+
103+def get_config_file():
104+ """
105+ Return the path for the config file
106+ """
107+ config_dir = os.path.dirname(__file__)
108+ return os.path.join(config_dir, CONFIG_FILE)
109
110=== added file 'tests/autopilot/push_notifications/config/push.conf'
111--- tests/autopilot/push_notifications/config/push.conf 1970-01-01 00:00:00 +0000
112+++ tests/autopilot/push_notifications/config/push.conf 2014-04-22 14:02:46 +0000
113@@ -0,0 +1,23 @@
114+[default]
115+environment = remote
116+
117+[local]
118+addr = 127.0.0.1
119+listener_port = 8080
120+device_port = 9090
121+
122+[remote]
123+addr = 192.168.1.2
124+listener_port = 8080
125+device_port = 9090
126+
127+[staging]
128+addr = staging-url.com
129+listener_port = 8080
130+device_port = 9090
131+
132+[production]
133+addr = production-url.com
134+listener_port = 8080
135+device_port = 9090
136+
137
138=== added file 'tests/autopilot/push_notifications/data.py'
139--- tests/autopilot/push_notifications/data.py 1970-01-01 00:00:00 +0000
140+++ tests/autopilot/push_notifications/data.py 2014-04-22 14:02:46 +0000
141@@ -0,0 +1,81 @@
142+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
143+# Copyright 2014 Canonical
144+#
145+# This program is free software: you can redistribute it and/or modify it
146+# under the terms of the GNU General Public License version 3, as published
147+# by the Free Software Foundation.
148+
149+"""Push-Notifications autopilot data structure classes"""
150+
151+
152+class PushNotificationMessage:
153+ """
154+ Class to hold all the details required for a
155+ push notification message
156+ """
157+ channel = None
158+ expire_after = None
159+ data = None
160+
161+ def __init__(self, channel='system', data='', expire_after=''):
162+ self.channel = channel
163+ self.data = data
164+ self.expire_after = expire_after
165+
166+ def json(self):
167+ """
168+ Return json representation of message
169+ """
170+ json_str = '{{"channel":"{0}", "data":{{{1}}}, "expire_on":"{2}"}}'
171+ return json_str.format(self.channel, self.data, self.expire_after)
172+
173+
174+class NotificationData:
175+ """
176+ Class to represent notification data including
177+ Device software channel
178+ Device build number
179+ Device model
180+ Device last update
181+ Data for the notification
182+ """
183+ channel = None
184+ build_number = None
185+ device = None
186+ last_update = None
187+ version = None
188+ data = None
189+
190+ @classmethod
191+ def from_dbus_info(cls, dbus_info=None):
192+ """
193+ Create a new object based on dbus_info if provided
194+ """
195+ nd = NotificationData()
196+ if dbus_info is not None:
197+ nd.device = dbus_info[1]
198+ nd.channel = dbus_info[2]
199+ nd.last_update = dbus_info[3]
200+ nd.build_number = dbus_info[4]['version']
201+ return nd
202+
203+ def inc_build_number(self):
204+ """
205+ Increment build number
206+ """
207+ self.build_number = str(int(self.build_number) + 1)
208+
209+ def dec_build_number(self):
210+ """
211+ Decrement build number
212+ """
213+ self.build_number = str(int(self.build_number) - 1)
214+
215+ def json(self):
216+ """
217+ Return json representation of info based:
218+ "IMAGE-CHANNEL/DEVICE-MODEL": [BUILD-NUMBER, CHANNEL-ALIAS]"
219+ """
220+ json_str = '"{0}/{1}": [{2}, "{3}"]'
221+ return json_str.format(self.channel, self.device, self.build_number,
222+ self.data)
223
224=== added directory 'tests/autopilot/push_notifications/tests'
225=== added file 'tests/autopilot/push_notifications/tests/__init__.py'
226--- tests/autopilot/push_notifications/tests/__init__.py 1970-01-01 00:00:00 +0000
227+++ tests/autopilot/push_notifications/tests/__init__.py 2014-04-22 14:02:46 +0000
228@@ -0,0 +1,239 @@
229+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
230+# Copyright 2014 Canonical
231+#
232+# This program is free software: you can redistribute it and/or modify it
233+# under the terms of the GNU General Public License version 3, as published
234+# by the Free Software Foundation.
235+
236+"""push-notifications autopilot tests."""
237+
238+
239+import configparser
240+import http.client as http
241+import json
242+import os
243+import datetime
244+import subprocess
245+import dbus
246+import copy
247+
248+from autopilot.testcase import AutopilotTestCase
249+from push_notifications.data import PushNotificationMessage
250+from push_notifications.data import NotificationData
251+from push_notifications import config
252+
253+
254+class PushNotificationTestBase(AutopilotTestCase):
255+ """
256+ Base class for push notification test cases
257+ """
258+
259+ PUSH_CLIENT_DEFAULT_CONFIG_FILE = '/etc/xdg/ubuntu-push-client/config.json'
260+ PUSH_CLIENT_CONFIG_FILE = '~/.config/ubuntu-push-client/config.json'
261+ PUSH_SERVER_BROADCAST_URL = '/broadcast'
262+ DEFAULT_DISPLAY_MESSAGE = 'There\'s an updated system image.'
263+ PUSH_MIME_TYPE = 'application/json'
264+ SECTION_DEFAULT = 'default'
265+ KEY_ENVIRONMENT = 'environment'
266+ KEY_ADDR = 'addr'
267+ KEY_LISTENER_PORT = 'listener_port'
268+ KEY_DEVICE_PORT = 'device_port'
269+
270+ def setUp(self):
271+ """
272+ Start the client running with the correct server config
273+ """
274+ # setup
275+ super(PushNotificationTestBase, self).setUp()
276+ # Read the config data
277+ self.read_config_file()
278+ # write server device address to the client config
279+ self.write_client_test_config()
280+ # restart the push client
281+ self.restart_push_client()
282+ # validate that the initialisation push message is displayed
283+ self.validate_push_message(self.DEFAULT_DISPLAY_MESSAGE)
284+ # dbus
285+ self.get_device_info()
286+
287+ def create_notification_data_copy(self):
288+ """
289+ Return a copy of the device's notification data
290+ """
291+ return copy.deepcopy(self.notification_data)
292+
293+ def get_device_info(self):
294+ """
295+ Discover the device's model and build info
296+ """
297+ system_bus = dbus.SystemBus()
298+ info_service = system_bus.get_object(
299+ 'com.canonical.SystemImage', '/Service')
300+ info = info_service.Info()
301+ # Create a NotificationData object based on the dbus info
302+ self.notification_data = NotificationData.from_dbus_info(
303+ dbus_info=info)
304+
305+ def read_config_file(self):
306+ """
307+ Read data from config file
308+ """
309+ config_file = config.get_config_file()
310+ self.config = configparser.ConfigParser()
311+ self.config.read(config_file)
312+ # read the name of the environment to use (local/remote)
313+ self.env = self.config[self.SECTION_DEFAULT][self.KEY_ENVIRONMENT]
314+ # format the server device and listener address
315+ addr_fmt = '{0}:{1}'
316+ self.server_listener_addr = addr_fmt.format(
317+ self.get_server_addr(), self.get_listener_port())
318+ self.server_device_addr = addr_fmt.format(
319+ self.get_server_addr(), self.get_device_port())
320+
321+ def get_server_addr(self):
322+ """
323+ Return the server address from config file
324+ """
325+ return self.config[self.env][self.KEY_ADDR]
326+
327+ def get_listener_port(self):
328+ """
329+ Return the server listener port from config file
330+ """
331+ return self.config[self.env][self.KEY_LISTENER_PORT]
332+
333+ def get_device_port(self):
334+ """
335+ Return the server listener port from config file
336+ """
337+ return self.config[self.env][self.KEY_DEVICE_PORT]
338+
339+ def _control_client(self, command):
340+ """
341+ start/stop/restart the ubuntu-push-client using initctl
342+ """
343+ subprocess.call(['initctl', command, 'ubuntu-push-client'])
344+
345+ def stop_push_client(self):
346+ """
347+ Stop the push client
348+ """
349+ self._control_client('stop')
350+
351+ def start_push_client(self):
352+ """
353+ Start the push client
354+ """
355+ self._control_client('start')
356+
357+ def restart_push_client(self):
358+ """
359+ Restart the push client
360+ """
361+ self.stop_push_client()
362+ self.start_push_client()
363+
364+ def write_client_test_config(self):
365+ """
366+ Write the test server address to client config file
367+ """
368+ # read the original config file
369+ with open(self.PUSH_CLIENT_DEFAULT_CONFIG_FILE) as config_file:
370+ config = json.load(config_file)
371+ # change server address
372+ config['addr'] = self.server_device_addr
373+ # write the config json out to the ~.local address
374+ abs_config_file = os.path.expanduser(self.PUSH_CLIENT_CONFIG_FILE)
375+ config_dir = os.path.dirname(abs_config_file)
376+ if not os.path.exists(config_dir):
377+ os.makedirs(config_dir)
378+ with open(abs_config_file, 'w+') as outfile:
379+ json.dump(config, outfile, indent=4)
380+ outfile.close()
381+
382+ def send_push_broadcast_notification(self, msg_json):
383+ """
384+ Send the specified push message to the server broadcast url
385+ using an HTTP POST command
386+ """
387+ headers = {'Content-type': self.PUSH_MIME_TYPE}
388+ conn = http.HTTPConnection(self.server_listener_addr)
389+ conn.request(
390+ 'POST',
391+ self.PUSH_SERVER_BROADCAST_URL,
392+ headers=headers,
393+ body=msg_json)
394+ return conn.getresponse()
395+
396+ def create_push_message(self, channel='system', data='', expire_after=''):
397+ """
398+ Return a new push msg
399+ If no expiry time is given, a future date will be assigned
400+ """
401+ if expire_after == '':
402+ expire_after = self.get_future_iso_time()
403+ return PushNotificationMessage(
404+ channel=channel,
405+ data=data,
406+ expire_after=expire_after)
407+
408+ def validate_push_message(self, display_message, timeout=10):
409+ """
410+ Validate that a notification message is displayed on screen
411+ """
412+
413+ def get_past_iso_time(self):
414+ """
415+ Return time 1 year in past in ISO format
416+ """
417+ return self.get_iso_time(year_offset=-1)
418+
419+ def get_near_past_iso_time(self):
420+ """
421+ Return time 1 minute in past in ISO format
422+ """
423+ return self.get_iso_time(min_offset=-1)
424+
425+ def get_near_future_iso_time(self):
426+ """
427+ Return time 1 minute in future in ISO format
428+ """
429+ return self.get_iso_time(min_offset=1)
430+
431+ def get_future_iso_time(self):
432+ """
433+ Return time 1 year in future in ISO format
434+ """
435+ return self.get_iso_time(year_offset=1)
436+
437+ def get_current_iso_time(self):
438+ """
439+ Return current time in ISO format
440+ """
441+ return self.get_iso_time()
442+
443+ def get_iso_time(self, year_offset=0, month_offset=0, day_offset=0,
444+ hour_offset=0, min_offset=0, sec_offset=0,
445+ tz_hour_offset=0, tz_min_offset=0):
446+ """
447+ Return an ISO8601 format date-time string, including time-zone
448+ offset: YYYY-MM-DDTHH:MM:SS-HH:MM
449+ """
450+ # calulate target time based on current time and format it
451+ now = datetime.datetime.now()
452+ target_time = datetime.datetime(
453+ year=now.year + year_offset,
454+ month=now.month + month_offset,
455+ day=now.day + day_offset,
456+ hour=now.hour + hour_offset,
457+ minute=now.minute + min_offset,
458+ second=now.second + sec_offset)
459+ target_time_fmt = target_time.strftime('%Y-%m-%dT%H:%M:%S')
460+ # format time zone offset
461+ tz = datetime.time(
462+ hour=tz_hour_offset,
463+ minute=tz_min_offset)
464+ tz_fmt = tz.strftime('%H:%M')
465+ # combine target time and time zone offset
466+ iso_time = '{0}-{1}'.format(target_time_fmt, tz_fmt)
467+ return iso_time
468
469=== added file 'tests/autopilot/push_notifications/tests/test_push_client.py'
470--- tests/autopilot/push_notifications/tests/test_push_client.py 1970-01-01 00:00:00 +0000
471+++ tests/autopilot/push_notifications/tests/test_push_client.py 2014-04-22 14:02:46 +0000
472@@ -0,0 +1,73 @@
473+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
474+# Copyright 2014 Canonical
475+#
476+# This program is free software: you can redistribute it and/or modify it
477+# under the terms of the GNU General Public License version 3, as published
478+# by the Free Software Foundation.
479+
480+"""Tests for Push Notifications client"""
481+
482+from __future__ import absolute_import
483+
484+from testtools.matchers import Equals
485+from push_notifications.tests import PushNotificationTestBase
486+
487+
488+class TestPushClient(PushNotificationTestBase):
489+ """ Tests a Push notification can be sent and received """
490+
491+ def _validate_response(self, response, expected_status_code=200):
492+ """
493+ Validate the received response status code against expected code
494+ """
495+ self.assertThat(response.status, Equals(expected_status_code))
496+
497+ def test_broadcast_push_notification(self):
498+ """
499+ Positive test case to send a valid broadcast push notification
500+ to the client and validate that a notification message is displayed
501+ """
502+ # create a copy of the device's build info
503+ msg_data = self.create_notification_data_copy()
504+ # increment the build number to trigger an update
505+ msg_data.inc_build_number()
506+ # create message based on the data
507+ msg = self.create_push_message(data=msg_data.json())
508+ # send the notification message to the server and check response
509+ response = self.send_push_broadcast_notification(msg.json())
510+ self._validate_response(response)
511+
512+ # TODO validate that message is received on client
513+
514+ def test_expired_broadcast_push_notification(self):
515+ """
516+ Send an expired broadcast notification message to server
517+ """
518+ msg = self.create_push_message(expire_after=self.get_past_iso_time())
519+ response = self.send_push_broadcast_notification(msg.json())
520+ # 400 status is received for an expired message
521+ self._validate_response(response, expected_status_code='400')
522+
523+ # TODO validate that message is not received on client
524+
525+ def test_near_expiry_broadcast_push_notification(self):
526+ """
527+ Send a broadcast message with a short validity time
528+ """
529+ msg = self.create_push_message(
530+ expire_after=self.get_near_future_iso_time())
531+ response = self.send_push_broadcast_notification(msg.json())
532+ self._validate_response(response)
533+
534+ # TODO validate that message is received on client
535+
536+ def test_just_expired_broadcast_push_notification(self):
537+ """
538+ Send a broadcast message which has just expired
539+ """
540+ msg = self.create_push_message(
541+ expire_after=self.get_near_past_iso_time())
542+ response = self.send_push_broadcast_notification(msg.json())
543+ self._validate_response(response)
544+
545+ # TODO validate that message is not received on client

Subscribers

People subscribed via source and target branches