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
=== added directory 'tests'
=== added directory 'tests/autopilot'
=== added directory 'tests/autopilot/push_notifications'
=== added file 'tests/autopilot/push_notifications/README'
--- tests/autopilot/push_notifications/README 1970-01-01 00:00:00 +0000
+++ tests/autopilot/push_notifications/README 2014-04-22 14:02:46 +0000
@@ -0,0 +1,63 @@
1==================
2README
3==================
4
5To 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.
6
7----------------------------------
8To configure and build the server:
9----------------------------------
10
111) export GOPATH=${PWD}/push
122) mkdir -p push/src/launchpad.net
133) cd push/src/launchpad.net
144) bzr branch lp:ubuntu-push
155) Edit ubuntu-push/sampleconfigs/dev.json:
16 "addr": "192.168.1.2:9090",
17 The ip should be changed to match your current environment
18 - use 127.0.0.1 if the client is also going to be running on the same machine as the server
19 - use real ip address if the client is going to be running on a different machine on the same network
205) cd ubuntu-push
216) make bootstrap
227) make run-server-dev
23 Following output should be observed:
24 INFO listening for http on 192.168.1.2:8080
25 INFO listening for devices on 192.168.1.2:9090
26
27------------------------
28To configure the client:
29------------------------
30
31Note: Tests must be run as root user
32
331) sudo apt-get install ubuntu-push-client
342) bzr branch lp:ubuntu-push
353) Edit ubuntu-push/tests/autopilot/push_notifications/config/push.conf:
36 [default]
37 environment = remote
38 [remote]
39 addr = 192.168.1.2
40 listener_port = 8080
41 device_port = 9090
42
43 The environment can be toggled using the 'environment' setting under [default] section. The addr and port settings for the chosen environment shold match required.
44
454) cd ubuntu-push/tests/autopilot
465) autopilot3 list push_notifications
476) autopilot3 run push_notifications
48
49----------------
50Troubleshooting:
51----------------
52
531) Ping from client to server to ensure connectivity is correct
542) Delete .local/share/ubuntu-push-client/levels.db if no notifications are being displayed
553) To send a notification manually:
56 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
57 Response should be:
58 {"ok":true}
59 Note that:
60 - The channel and device names must match the client.
61 - The build number must be greater than current installed version in order to trigger an update message.
62 - The expiration time must be in the future.
63
064
=== added file 'tests/autopilot/push_notifications/__init__.py'
--- tests/autopilot/push_notifications/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/autopilot/push_notifications/__init__.py 2014-04-22 14:02:46 +0000
@@ -0,0 +1,8 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2014 Canonical
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7
8"""push-notifications autopilot tests."""
09
=== added directory 'tests/autopilot/push_notifications/config'
=== added file 'tests/autopilot/push_notifications/config/__init__.py'
--- tests/autopilot/push_notifications/config/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/autopilot/push_notifications/config/__init__.py 2014-04-22 14:02:46 +0000
@@ -0,0 +1,19 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2014 Canonical
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7
8
9import os
10
11CONFIG_FILE = 'push.conf'
12
13
14def get_config_file():
15 """
16 Return the path for the config file
17 """
18 config_dir = os.path.dirname(__file__)
19 return os.path.join(config_dir, CONFIG_FILE)
020
=== added file 'tests/autopilot/push_notifications/config/push.conf'
--- tests/autopilot/push_notifications/config/push.conf 1970-01-01 00:00:00 +0000
+++ tests/autopilot/push_notifications/config/push.conf 2014-04-22 14:02:46 +0000
@@ -0,0 +1,23 @@
1[default]
2environment = remote
3
4[local]
5addr = 127.0.0.1
6listener_port = 8080
7device_port = 9090
8
9[remote]
10addr = 192.168.1.2
11listener_port = 8080
12device_port = 9090
13
14[staging]
15addr = staging-url.com
16listener_port = 8080
17device_port = 9090
18
19[production]
20addr = production-url.com
21listener_port = 8080
22device_port = 9090
23
024
=== added file 'tests/autopilot/push_notifications/data.py'
--- tests/autopilot/push_notifications/data.py 1970-01-01 00:00:00 +0000
+++ tests/autopilot/push_notifications/data.py 2014-04-22 14:02:46 +0000
@@ -0,0 +1,81 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2014 Canonical
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7
8"""Push-Notifications autopilot data structure classes"""
9
10
11class PushNotificationMessage:
12 """
13 Class to hold all the details required for a
14 push notification message
15 """
16 channel = None
17 expire_after = None
18 data = None
19
20 def __init__(self, channel='system', data='', expire_after=''):
21 self.channel = channel
22 self.data = data
23 self.expire_after = expire_after
24
25 def json(self):
26 """
27 Return json representation of message
28 """
29 json_str = '{{"channel":"{0}", "data":{{{1}}}, "expire_on":"{2}"}}'
30 return json_str.format(self.channel, self.data, self.expire_after)
31
32
33class NotificationData:
34 """
35 Class to represent notification data including
36 Device software channel
37 Device build number
38 Device model
39 Device last update
40 Data for the notification
41 """
42 channel = None
43 build_number = None
44 device = None
45 last_update = None
46 version = None
47 data = None
48
49 @classmethod
50 def from_dbus_info(cls, dbus_info=None):
51 """
52 Create a new object based on dbus_info if provided
53 """
54 nd = NotificationData()
55 if dbus_info is not None:
56 nd.device = dbus_info[1]
57 nd.channel = dbus_info[2]
58 nd.last_update = dbus_info[3]
59 nd.build_number = dbus_info[4]['version']
60 return nd
61
62 def inc_build_number(self):
63 """
64 Increment build number
65 """
66 self.build_number = str(int(self.build_number) + 1)
67
68 def dec_build_number(self):
69 """
70 Decrement build number
71 """
72 self.build_number = str(int(self.build_number) - 1)
73
74 def json(self):
75 """
76 Return json representation of info based:
77 "IMAGE-CHANNEL/DEVICE-MODEL": [BUILD-NUMBER, CHANNEL-ALIAS]"
78 """
79 json_str = '"{0}/{1}": [{2}, "{3}"]'
80 return json_str.format(self.channel, self.device, self.build_number,
81 self.data)
082
=== added directory 'tests/autopilot/push_notifications/tests'
=== added file 'tests/autopilot/push_notifications/tests/__init__.py'
--- tests/autopilot/push_notifications/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ tests/autopilot/push_notifications/tests/__init__.py 2014-04-22 14:02:46 +0000
@@ -0,0 +1,239 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2014 Canonical
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7
8"""push-notifications autopilot tests."""
9
10
11import configparser
12import http.client as http
13import json
14import os
15import datetime
16import subprocess
17import dbus
18import copy
19
20from autopilot.testcase import AutopilotTestCase
21from push_notifications.data import PushNotificationMessage
22from push_notifications.data import NotificationData
23from push_notifications import config
24
25
26class PushNotificationTestBase(AutopilotTestCase):
27 """
28 Base class for push notification test cases
29 """
30
31 PUSH_CLIENT_DEFAULT_CONFIG_FILE = '/etc/xdg/ubuntu-push-client/config.json'
32 PUSH_CLIENT_CONFIG_FILE = '~/.config/ubuntu-push-client/config.json'
33 PUSH_SERVER_BROADCAST_URL = '/broadcast'
34 DEFAULT_DISPLAY_MESSAGE = 'There\'s an updated system image.'
35 PUSH_MIME_TYPE = 'application/json'
36 SECTION_DEFAULT = 'default'
37 KEY_ENVIRONMENT = 'environment'
38 KEY_ADDR = 'addr'
39 KEY_LISTENER_PORT = 'listener_port'
40 KEY_DEVICE_PORT = 'device_port'
41
42 def setUp(self):
43 """
44 Start the client running with the correct server config
45 """
46 # setup
47 super(PushNotificationTestBase, self).setUp()
48 # Read the config data
49 self.read_config_file()
50 # write server device address to the client config
51 self.write_client_test_config()
52 # restart the push client
53 self.restart_push_client()
54 # validate that the initialisation push message is displayed
55 self.validate_push_message(self.DEFAULT_DISPLAY_MESSAGE)
56 # dbus
57 self.get_device_info()
58
59 def create_notification_data_copy(self):
60 """
61 Return a copy of the device's notification data
62 """
63 return copy.deepcopy(self.notification_data)
64
65 def get_device_info(self):
66 """
67 Discover the device's model and build info
68 """
69 system_bus = dbus.SystemBus()
70 info_service = system_bus.get_object(
71 'com.canonical.SystemImage', '/Service')
72 info = info_service.Info()
73 # Create a NotificationData object based on the dbus info
74 self.notification_data = NotificationData.from_dbus_info(
75 dbus_info=info)
76
77 def read_config_file(self):
78 """
79 Read data from config file
80 """
81 config_file = config.get_config_file()
82 self.config = configparser.ConfigParser()
83 self.config.read(config_file)
84 # read the name of the environment to use (local/remote)
85 self.env = self.config[self.SECTION_DEFAULT][self.KEY_ENVIRONMENT]
86 # format the server device and listener address
87 addr_fmt = '{0}:{1}'
88 self.server_listener_addr = addr_fmt.format(
89 self.get_server_addr(), self.get_listener_port())
90 self.server_device_addr = addr_fmt.format(
91 self.get_server_addr(), self.get_device_port())
92
93 def get_server_addr(self):
94 """
95 Return the server address from config file
96 """
97 return self.config[self.env][self.KEY_ADDR]
98
99 def get_listener_port(self):
100 """
101 Return the server listener port from config file
102 """
103 return self.config[self.env][self.KEY_LISTENER_PORT]
104
105 def get_device_port(self):
106 """
107 Return the server listener port from config file
108 """
109 return self.config[self.env][self.KEY_DEVICE_PORT]
110
111 def _control_client(self, command):
112 """
113 start/stop/restart the ubuntu-push-client using initctl
114 """
115 subprocess.call(['initctl', command, 'ubuntu-push-client'])
116
117 def stop_push_client(self):
118 """
119 Stop the push client
120 """
121 self._control_client('stop')
122
123 def start_push_client(self):
124 """
125 Start the push client
126 """
127 self._control_client('start')
128
129 def restart_push_client(self):
130 """
131 Restart the push client
132 """
133 self.stop_push_client()
134 self.start_push_client()
135
136 def write_client_test_config(self):
137 """
138 Write the test server address to client config file
139 """
140 # read the original config file
141 with open(self.PUSH_CLIENT_DEFAULT_CONFIG_FILE) as config_file:
142 config = json.load(config_file)
143 # change server address
144 config['addr'] = self.server_device_addr
145 # write the config json out to the ~.local address
146 abs_config_file = os.path.expanduser(self.PUSH_CLIENT_CONFIG_FILE)
147 config_dir = os.path.dirname(abs_config_file)
148 if not os.path.exists(config_dir):
149 os.makedirs(config_dir)
150 with open(abs_config_file, 'w+') as outfile:
151 json.dump(config, outfile, indent=4)
152 outfile.close()
153
154 def send_push_broadcast_notification(self, msg_json):
155 """
156 Send the specified push message to the server broadcast url
157 using an HTTP POST command
158 """
159 headers = {'Content-type': self.PUSH_MIME_TYPE}
160 conn = http.HTTPConnection(self.server_listener_addr)
161 conn.request(
162 'POST',
163 self.PUSH_SERVER_BROADCAST_URL,
164 headers=headers,
165 body=msg_json)
166 return conn.getresponse()
167
168 def create_push_message(self, channel='system', data='', expire_after=''):
169 """
170 Return a new push msg
171 If no expiry time is given, a future date will be assigned
172 """
173 if expire_after == '':
174 expire_after = self.get_future_iso_time()
175 return PushNotificationMessage(
176 channel=channel,
177 data=data,
178 expire_after=expire_after)
179
180 def validate_push_message(self, display_message, timeout=10):
181 """
182 Validate that a notification message is displayed on screen
183 """
184
185 def get_past_iso_time(self):
186 """
187 Return time 1 year in past in ISO format
188 """
189 return self.get_iso_time(year_offset=-1)
190
191 def get_near_past_iso_time(self):
192 """
193 Return time 1 minute in past in ISO format
194 """
195 return self.get_iso_time(min_offset=-1)
196
197 def get_near_future_iso_time(self):
198 """
199 Return time 1 minute in future in ISO format
200 """
201 return self.get_iso_time(min_offset=1)
202
203 def get_future_iso_time(self):
204 """
205 Return time 1 year in future in ISO format
206 """
207 return self.get_iso_time(year_offset=1)
208
209 def get_current_iso_time(self):
210 """
211 Return current time in ISO format
212 """
213 return self.get_iso_time()
214
215 def get_iso_time(self, year_offset=0, month_offset=0, day_offset=0,
216 hour_offset=0, min_offset=0, sec_offset=0,
217 tz_hour_offset=0, tz_min_offset=0):
218 """
219 Return an ISO8601 format date-time string, including time-zone
220 offset: YYYY-MM-DDTHH:MM:SS-HH:MM
221 """
222 # calulate target time based on current time and format it
223 now = datetime.datetime.now()
224 target_time = datetime.datetime(
225 year=now.year + year_offset,
226 month=now.month + month_offset,
227 day=now.day + day_offset,
228 hour=now.hour + hour_offset,
229 minute=now.minute + min_offset,
230 second=now.second + sec_offset)
231 target_time_fmt = target_time.strftime('%Y-%m-%dT%H:%M:%S')
232 # format time zone offset
233 tz = datetime.time(
234 hour=tz_hour_offset,
235 minute=tz_min_offset)
236 tz_fmt = tz.strftime('%H:%M')
237 # combine target time and time zone offset
238 iso_time = '{0}-{1}'.format(target_time_fmt, tz_fmt)
239 return iso_time
0240
=== added file 'tests/autopilot/push_notifications/tests/test_push_client.py'
--- tests/autopilot/push_notifications/tests/test_push_client.py 1970-01-01 00:00:00 +0000
+++ tests/autopilot/push_notifications/tests/test_push_client.py 2014-04-22 14:02:46 +0000
@@ -0,0 +1,73 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2# Copyright 2014 Canonical
3#
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7
8"""Tests for Push Notifications client"""
9
10from __future__ import absolute_import
11
12from testtools.matchers import Equals
13from push_notifications.tests import PushNotificationTestBase
14
15
16class TestPushClient(PushNotificationTestBase):
17 """ Tests a Push notification can be sent and received """
18
19 def _validate_response(self, response, expected_status_code=200):
20 """
21 Validate the received response status code against expected code
22 """
23 self.assertThat(response.status, Equals(expected_status_code))
24
25 def test_broadcast_push_notification(self):
26 """
27 Positive test case to send a valid broadcast push notification
28 to the client and validate that a notification message is displayed
29 """
30 # create a copy of the device's build info
31 msg_data = self.create_notification_data_copy()
32 # increment the build number to trigger an update
33 msg_data.inc_build_number()
34 # create message based on the data
35 msg = self.create_push_message(data=msg_data.json())
36 # send the notification message to the server and check response
37 response = self.send_push_broadcast_notification(msg.json())
38 self._validate_response(response)
39
40 # TODO validate that message is received on client
41
42 def test_expired_broadcast_push_notification(self):
43 """
44 Send an expired broadcast notification message to server
45 """
46 msg = self.create_push_message(expire_after=self.get_past_iso_time())
47 response = self.send_push_broadcast_notification(msg.json())
48 # 400 status is received for an expired message
49 self._validate_response(response, expected_status_code='400')
50
51 # TODO validate that message is not received on client
52
53 def test_near_expiry_broadcast_push_notification(self):
54 """
55 Send a broadcast message with a short validity time
56 """
57 msg = self.create_push_message(
58 expire_after=self.get_near_future_iso_time())
59 response = self.send_push_broadcast_notification(msg.json())
60 self._validate_response(response)
61
62 # TODO validate that message is received on client
63
64 def test_just_expired_broadcast_push_notification(self):
65 """
66 Send a broadcast message which has just expired
67 """
68 msg = self.create_push_message(
69 expire_after=self.get_near_past_iso_time())
70 response = self.send_push_broadcast_notification(msg.json())
71 self._validate_response(response)
72
73 # TODO validate that message is not received on client

Subscribers

People subscribed via source and target branches