OLD | NEW |
1 #!tests/.venv/bin/python | 1 #!tests/.venv/bin/python |
2 | 2 |
3 # This file is part of the Juju GUI, which lets users view and manage Juju | 3 # This file is part of the Juju GUI, which lets users view and manage Juju |
4 # environments within a graphical interface (https://launchpad.net/juju-gui). | 4 # environments within a graphical interface (https://launchpad.net/juju-gui). |
5 # Copyright (C) 2012-2013 Canonical Ltd. | 5 # Copyright (C) 2012-2013 Canonical Ltd. |
6 # | 6 # |
7 # This program is free software: you can redistribute it and/or modify it under | 7 # This program is free software: you can redistribute it and/or modify it under |
8 # the terms of the GNU Affero General Public License version 3, as published by | 8 # the terms of the GNU Affero General Public License version 3, as published by |
9 # the Free Software Foundation. | 9 # the Free Software Foundation. |
10 # | 10 # |
11 # This program is distributed in the hope that it will be useful, but WITHOUT | 11 # This program is distributed in the hope that it will be useful, but WITHOUT |
12 # ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, | 12 # ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
13 # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | 13 # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 # Affero General Public License for more details. | 14 # Affero General Public License for more details. |
15 # | 15 # |
16 # You should have received a copy of the GNU Affero General Public License | 16 # You should have received a copy of the GNU Affero General Public License |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | 18 |
19 from __future__ import print_function | 19 from __future__ import print_function |
20 import httplib | 20 import httplib |
| 21 import json |
21 import itertools | 22 import itertools |
22 import unittest | 23 import unittest |
| 24 import urllib2 |
23 import urlparse | 25 import urlparse |
24 | 26 |
25 from selenium.webdriver import Firefox | 27 from selenium.webdriver import Firefox |
26 from selenium.webdriver.support import ui | 28 from selenium.webdriver.support import ui |
27 from xvfbwrapper import Xvfb | 29 from xvfbwrapper import Xvfb |
28 import yaml | 30 import yaml |
29 | 31 |
30 # XXX 2013-07-30 benji bug=872264: Don't use juju_deploy directly, use | 32 # XXX 2013-07-30 benji bug=872264: Don't use juju_deploy directly, use |
31 # DeployTestMixin.juju_deploy instead. See comment in the method. | 33 # DeployTestMixin.juju_deploy instead. See comment in the method. |
32 from deploy import juju_deploy | 34 from deploy import juju_deploy |
33 from helpers import ( | 35 from helpers import ( |
34 get_admin_secret, | 36 get_admin_secret, |
35 juju_destroy_service, | 37 juju_destroy_service, |
36 juju_version, | 38 juju_version, |
| 39 make_service_name, |
37 stop_services, | 40 stop_services, |
38 WebSocketClient, | 41 WebSocketClient, |
39 ) | 42 ) |
40 import example | 43 import example |
41 | 44 |
42 JUJU_GUI_TEST_BRANCH = 'lp:~juju-gui/juju-gui/charm-tests-branch' | 45 JUJU_GUI_TEST_BRANCH = 'lp:~juju-gui/juju-gui/charm-tests-branch' |
43 STAGING_SERVICES = ('haproxy', 'mediawiki', 'memcached', 'mysql', 'wordpress') | 46 STAGING_SERVICES = ('haproxy', 'mediawiki', 'memcached', 'mysql', 'wordpress') |
44 is_legacy_juju = juju_version().major == 0 | 47 is_legacy_juju = juju_version().major == 0 |
45 try: | 48 try: |
46 admin_secret = get_admin_secret() | 49 admin_secret = get_admin_secret() |
47 except ValueError as err: | 50 except ValueError as err: |
48 admin_secret = None | 51 admin_secret = None |
49 print(err) | 52 print(err) |
50 | 53 |
51 | 54 |
| 55 def juju_deploy_gui(options=None): |
| 56 """Deploy the Juju GUI charm with the given options. |
| 57 |
| 58 Deploy the charm in the bootstrap node if possible in the current Juju |
| 59 implementation. Use a random service name. |
| 60 |
| 61 Return a tuple containing the deployed service name, unit info and a list |
| 62 of services that are started in the Juju GUI unit and that must be |
| 63 manually stopped by tests. In juju-core, the services list is always empty. |
| 64 """ |
| 65 force_machine = None if is_legacy_juju else 0 |
| 66 service_name = make_service_name(prefix='juju-gui-') |
| 67 unit_info = juju_deploy( |
| 68 'juju-gui', service_name=service_name, options=options, |
| 69 force_machine=force_machine) |
| 70 # XXX 2012-11-29 frankban bug=872264: |
| 71 # Just invoking ``juju destroy-service juju-gui`` in tearDown |
| 72 # should execute the ``stop`` hook, stopping all the services |
| 73 # started by the charm in the machine. Right now this does not |
| 74 # work in pyJuju, so the desired effect is achieved by keeping |
| 75 # track of started services and manually stopping them here. |
| 76 # Once pyJuju works correctly or we drop support for it altogether, we |
| 77 # can remove this shim. |
| 78 cleanup_services = [] |
| 79 if is_legacy_juju: |
| 80 if options is None: |
| 81 options = {} |
| 82 # Either stop the builtin server or the old apache2/haproxy setup. |
| 83 if options.get('builtin-server') == 'true': |
| 84 cleanup_services.append('guiserver') |
| 85 else: |
| 86 cleanup_services.extend(['haproxy', 'apache2']) |
| 87 # Staging uses improv, otherwise the API agent is used. |
| 88 if options.get('staging') == 'true': |
| 89 cleanup_services.append('juju-api-improv') |
| 90 else: |
| 91 cleanup_services.append('juju-api-agent') |
| 92 return service_name, unit_info, cleanup_services |
| 93 |
| 94 |
52 class DeployTestMixin(object): | 95 class DeployTestMixin(object): |
53 | 96 |
54 charm = 'juju-gui' | |
55 port = '443' | 97 port = '443' |
56 | 98 |
57 def setUp(self): | 99 def setUp(self): |
58 # Perform all graphical operations in memory. | 100 # Perform all graphical operations in memory. |
59 vdisplay = Xvfb(width=1280, height=720) | 101 vdisplay = Xvfb(width=1280, height=720) |
60 vdisplay.start() | 102 vdisplay.start() |
61 self.addCleanup(vdisplay.stop) | 103 self.addCleanup(vdisplay.stop) |
62 # Create a Selenium browser instance. | 104 # Create a Selenium browser instance. |
63 selenium = self.selenium = Firefox() | 105 selenium = self.selenium = Firefox() |
64 self.addCleanup(selenium.quit) | 106 self.addCleanup(selenium.quit) |
(...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
128 condition = lambda driver: driver.execute_script(script) | 170 condition = lambda driver: driver.execute_script(script) |
129 return self.wait_for(condition, error=error, timeout=timeout) | 171 return self.wait_for(condition, error=error, timeout=timeout) |
130 | 172 |
131 def get_service_names(self): | 173 def get_service_names(self): |
132 """Return the set of services' names displayed in the current page.""" | 174 """Return the set of services' names displayed in the current page.""" |
133 def services_found(driver): | 175 def services_found(driver): |
134 return driver.find_elements_by_css_selector('.service .name') | 176 return driver.find_elements_by_css_selector('.service .name') |
135 services = self.wait_for(services_found, 'Services not displayed.') | 177 services = self.wait_for(services_found, 'Services not displayed.') |
136 return set([element.text for element in services]) | 178 return set([element.text for element in services]) |
137 | 179 |
138 def juju_deploy(self, *args, **kwargs): | 180 def deploy_gui(self, options=None): |
139 """Shim in our additional cleanup for pyJuju.""" | 181 """Shim in our additional cleanup for pyJuju.""" |
140 # XXX 2012-11-29 frankban bug=872264: | |
141 # Just invoking ``juju destroy-service juju-gui`` in tearDown | |
142 # should execute the ``stop`` hook, stopping all the services | |
143 # started by the charm in the machine. Right now this does not | |
144 # work in pyJuju, so the desired effect is achieved by keeping | |
145 # track of started services and manually stopping them here. | |
146 # Once pyJuju works correctly or we drop support for it altogether, we | 182 # Once pyJuju works correctly or we drop support for it altogether, we |
147 # can remove this shim. | 183 # can remove this shim. |
148 unit_info, self.service_name = juju_deploy(*args, **kwargs) | 184 self.service_name, unit_info, cleanup_services = juju_deploy_gui( |
| 185 options=options) |
| 186 # XXX 2013-11-27 frankban bug=872264: |
| 187 # See juju_deploy_gui above. |
149 if is_legacy_juju: | 188 if is_legacy_juju: |
150 hostname = unit_info['public-address'] | 189 hostname = unit_info['public-address'] |
151 options = kwargs.get('options', {}) | 190 self.addCleanup(stop_services, hostname, cleanup_services) |
152 # Either stop the builtin server or the old apache2/haproxy setup. | |
153 if options.get('builtin-server') == 'true': | |
154 services = ['guiserver'] | |
155 else: | |
156 services = ['haproxy', 'apache2'] | |
157 # Staging uses improv, otherwise the API agent is used. | |
158 if options.get('staging') == 'true': | |
159 services.append('juju-api-improv') | |
160 else: | |
161 services.append('juju-api-agent') | |
162 self.addCleanup(stop_services, hostname, services) | |
163 return unit_info | 191 return unit_info |
164 | 192 |
| 193 def get_builtin_server_info(self, hostname): |
| 194 """Return a dictionary of info as exposed by the builtin server.""" |
| 195 url = 'https://{}/gui-server-info'.format(hostname) |
| 196 response = urllib2.urlopen(url) |
| 197 self.assertEqual(200, response.code) |
| 198 return json.load(response) |
165 | 199 |
166 class TestDeploy(DeployTestMixin, unittest.TestCase): | 200 |
| 201 class TestDeployOptions(DeployTestMixin, unittest.TestCase): |
167 | 202 |
168 def tearDown(self): | 203 def tearDown(self): |
169 juju_destroy_service(self.service_name) | 204 juju_destroy_service(self.service_name) |
170 | 205 |
171 def test_local_release(self): | 206 def test_stable_release(self): |
172 # Ensure the Juju GUI and API agent services are correctly set up when | 207 # Ensure the stable Juju GUI release is correctly set up. |
173 # deploying the local release. | 208 unit_info = self.deploy_gui(options={'juju-gui-source': 'stable'}) |
174 unit_info = self.juju_deploy(self.charm) | |
175 hostname = unit_info['public-address'] | 209 hostname = unit_info['public-address'] |
176 self.navigate_to(hostname) | 210 self.navigate_to(hostname) |
177 self.handle_browser_warning() | 211 self.handle_browser_warning() |
178 self.assertEnvironmentIsConnected() | |
179 | |
180 def test_stable_release(self): | |
181 # Ensure the Juju GUI and API agent services are correctly set up when | |
182 # deploying the stable release. | |
183 options = {'juju-gui-source': 'stable'} | |
184 unit_info = self.juju_deploy(self.charm, options=options) | |
185 hostname = unit_info['public-address'] | |
186 self.navigate_to(hostname) | |
187 self.handle_browser_warning() | |
188 self.assertEnvironmentIsConnected() | 212 self.assertEnvironmentIsConnected() |
189 | 213 |
190 @unittest.skipUnless(is_legacy_juju, 'staging only works in pyJuju') | 214 @unittest.skipUnless(is_legacy_juju, 'staging only works in pyJuju') |
191 def test_staging(self): | 215 def test_staging(self): |
192 # Ensure the Juju GUI and improv services are correctly set up. | 216 # Ensure the Juju GUI and staging services are correctly set up. |
193 unit_info = self.juju_deploy(self.charm, options={'staging': 'true'}) | 217 unit_info = self.deploy_gui(options={'staging': 'true'}) |
194 hostname = unit_info['public-address'] | 218 hostname = unit_info['public-address'] |
195 self.navigate_to(hostname) | 219 self.navigate_to(hostname) |
196 self.handle_browser_warning() | 220 self.handle_browser_warning() |
197 self.assertEnvironmentIsConnected() | 221 self.assertEnvironmentIsConnected() |
198 # The staging environment contains five deployed services. | 222 # The staging environment contains five deployed services. |
199 self.assertSetEqual(set(STAGING_SERVICES), self.get_service_names()) | 223 self.assertSetEqual(set(STAGING_SERVICES), self.get_service_names()) |
200 | 224 |
201 def test_sandbox(self): | 225 def test_sandbox(self): |
202 # The GUI is correctly deployed and set up in sandbox mode. | 226 # The GUI is correctly deployed and set up in sandbox mode. |
203 unit_info = self.juju_deploy(self.charm, options={'sandbox': 'true'}) | 227 unit_info = self.deploy_gui(options={'sandbox': 'true'}) |
204 hostname = unit_info['public-address'] | 228 hostname = unit_info['public-address'] |
205 self.navigate_to(hostname) | 229 self.navigate_to(hostname) |
206 self.handle_browser_warning() | 230 self.handle_browser_warning() |
207 self.assertEnvironmentIsConnected() | 231 self.assertEnvironmentIsConnected() |
| 232 # Ensure the builtin server is set up to run in sandbox mode. |
| 233 server_info = self.get_builtin_server_info(hostname) |
| 234 self.assertTrue(server_info['sandbox']) |
208 | 235 |
209 def test_branch_source(self): | 236 def test_branch_source(self): |
210 # Ensure the Juju GUI is correctly deployed from a Bazaar branch. | 237 # Ensure the Juju GUI is correctly deployed from a Bazaar branch. |
211 options = {'juju-gui-source': JUJU_GUI_TEST_BRANCH} | 238 options = {'juju-gui-source': JUJU_GUI_TEST_BRANCH} |
212 unit_info = self.juju_deploy(self.charm, options=options) | 239 unit_info = self.deploy_gui(options=options) |
213 hostname = unit_info['public-address'] | 240 hostname = unit_info['public-address'] |
214 self.navigate_to(hostname) | 241 self.navigate_to(hostname) |
215 self.handle_browser_warning() | 242 self.handle_browser_warning() |
216 self.assertEnvironmentIsConnected() | 243 self.assertEnvironmentIsConnected() |
217 | 244 |
218 def test_legacy_server(self): | 245 def test_legacy_server(self): |
219 # The legacy apache + haproxy server configuration works correctly. | 246 # The legacy apache + haproxy server configuration works correctly. |
220 # Also make sure the correct cache headers are sent. | 247 # Also make sure the correct cache headers are sent. |
221 options = { | 248 unit_info = self.deploy_gui(options={'builtin-server': 'false'}) |
222 'builtin-server': False, | |
223 'juju-gui-source': JUJU_GUI_TEST_BRANCH, | |
224 } | |
225 unit_info = self.juju_deploy(self.charm, options=options) | |
226 hostname = unit_info['public-address'] | 249 hostname = unit_info['public-address'] |
| 250 self.navigate_to(hostname) |
| 251 self.handle_browser_warning() |
| 252 self.assertEnvironmentIsConnected() |
227 conn = httplib.HTTPSConnection(hostname) | 253 conn = httplib.HTTPSConnection(hostname) |
228 conn.request('HEAD', '/') | 254 conn.request('HEAD', '/') |
229 headers = conn.getresponse().getheaders() | 255 headers = conn.getresponse().getheaders() |
230 # There is only one Cache-Control header. | 256 # There is only one Cache-Control header. |
231 self.assertEqual(zip(*headers)[0].count('cache-control'), 1) | 257 self.assertEqual(zip(*headers)[0].count('cache-control'), 1) |
232 # The right cache directives are in Cache-Control. | 258 # The right cache directives are in Cache-Control. |
233 cache_control = dict(headers)['cache-control'] | 259 cache_control = dict(headers)['cache-control'] |
234 cache_directives = [s.strip() for s in cache_control.split(',')] | 260 cache_directives = [s.strip() for s in cache_control.split(',')] |
235 self.assertIn('max-age=0', cache_directives) | 261 self.assertIn('max-age=0', cache_directives) |
236 self.assertIn('public', cache_directives) | 262 self.assertIn('public', cache_directives) |
237 self.assertIn('must-revalidate', cache_directives) | 263 self.assertIn('must-revalidate', cache_directives) |
238 | 264 |
239 @unittest.skipIf(is_legacy_juju, 'force-machine only works in juju-core') | |
240 def test_force_machine(self): | |
241 # Ensure the Juju GUI is correctly set up in the Juju bootstrap node. | |
242 unit_info = self.juju_deploy(self.charm, force_machine=0) | |
243 self.assertEqual('0', unit_info['machine']) | |
244 self.navigate_to(unit_info['public-address']) | |
245 self.handle_browser_warning() | |
246 self.assertEnvironmentIsConnected() | |
247 | 265 |
248 def test_nrpe_check_available(self): | 266 class TestBuiltinServerLocalRelease(DeployTestMixin, unittest.TestCase): |
249 # Make sure the check-app-access.sh script's ADDRESS is available. | |
250 options = {'juju-gui-source': JUJU_GUI_TEST_BRANCH} | |
251 unit_info = self.juju_deploy(self.charm, options=options) | |
252 hostname = unit_info['public-address'] | |
253 conn = httplib.HTTPSConnection(hostname) | |
254 # This request matches the ADDRESS var in the script. | |
255 conn.request('GET', '/juju-ui/version.js') | |
256 message = 'ADDRESS in check-app-access.sh is not accessible.' | |
257 self.assertEqual(200, conn.getresponse().status, message) | |
258 | |
259 | |
260 class TestBuiltinServer(DeployTestMixin, unittest.TestCase): | |
261 | 267 |
262 @classmethod | 268 @classmethod |
263 def setUpClass(cls): | 269 def setUpClass(cls): |
264 # Deploy the charm. The resulting service is used by all the tests | 270 # Deploy the charm. The resulting service is used by all the tests |
265 # in this test case. | 271 # in this test case. |
266 unit_info, cls.service_name = juju_deploy(cls.charm) | 272 cls.service_name, unit_info, cls.cleanup_services = juju_deploy_gui() |
267 cls.hostname = unit_info['public-address'] | 273 cls.hostname = unit_info['public-address'] |
268 # The counter is used to produce API request identifiers. | 274 # The counter is used to produce API request identifiers. |
269 cls.counter = itertools.count() | 275 cls.counter = itertools.count() |
270 | 276 |
271 @classmethod | 277 @classmethod |
272 def tearDownClass(cls): | 278 def tearDownClass(cls): |
273 # Destroy the GUI service, and perform additional clean up in the case | 279 # Destroy the GUI service, and perform additional clean up in the case |
274 # we are in a pyJuju environment. | 280 # we are in a pyJuju environment. |
275 juju_destroy_service(cls.service_name) | 281 juju_destroy_service(cls.service_name) |
| 282 # XXX 2013-11-27 frankban bug=872264: |
| 283 # See juju_deploy_gui above. |
276 if is_legacy_juju: | 284 if is_legacy_juju: |
277 # XXX 2012-11-29 frankban bug=872264: | 285 stop_services(cls.hostname, cls.cleanup_services) |
278 # see DeployTestMixin.juju_deploy above. | |
279 stop_services(cls.hostname, ['guiserver', 'juju-api-agent']) | |
280 | 286 |
281 def make_websocket_client(self, authenticated=True): | 287 def make_websocket_client(self, authenticated=True): |
282 """Create and return a WebSocket client connected to the Juju backend. | 288 """Create and return a WebSocket client connected to the Juju backend. |
283 | 289 |
284 If authenticated is set to True, also log in to the Juju API server. | 290 If authenticated is set to True, also log in to the Juju API server. |
285 """ | 291 """ |
286 client = WebSocketClient('wss://{}:443/ws'.format(self.hostname)) | 292 client = WebSocketClient('wss://{}:443/ws'.format(self.hostname)) |
287 client.connect() | 293 client.connect() |
288 self.addCleanup(client.close) | 294 self.addCleanup(client.close) |
289 if authenticated: | 295 if authenticated: |
290 response = client.send({ | 296 response = client.send({ |
291 'RequestId': self.counter.next(), | 297 'RequestId': self.counter.next(), |
292 'Type': 'Admin', | 298 'Type': 'Admin', |
293 'Request': 'Login', | 299 'Request': 'Login', |
294 'Params': {'AuthTag': 'user-admin', 'Password': admin_secret}, | 300 'Params': {'AuthTag': 'user-admin', 'Password': admin_secret}, |
295 }) | 301 }) |
296 self.assertNotIn('Error', response) | 302 self.assertNotIn('Error', response) |
297 return client | 303 return client |
298 | 304 |
299 def test_environment_connection(self): | 305 def test_environment_connection(self): |
300 # Ensure the Juju GUI and builtin server are correctly set up. | 306 # Ensure the Juju GUI and builtin server are correctly set up using |
| 307 # the local release. |
301 self.navigate_to(self.hostname) | 308 self.navigate_to(self.hostname) |
302 self.handle_browser_warning() | 309 self.handle_browser_warning() |
303 self.assertEnvironmentIsConnected() | 310 self.assertEnvironmentIsConnected() |
| 311 # Ensure the builtin server is set up to be connected to the real env. |
| 312 server_info = self.get_builtin_server_info(self.hostname) |
| 313 self.assertFalse(server_info['sandbox']) |
304 | 314 |
305 def test_headers(self): | 315 def test_headers(self): |
306 # Ensure the Tornado headers are correctly sent. | 316 # Ensure the Tornado headers are correctly sent. |
307 conn = httplib.HTTPSConnection(self.hostname) | 317 conn = httplib.HTTPSConnection(self.hostname) |
308 conn.request('HEAD', '/') | 318 conn.request('HEAD', '/') |
309 headers = conn.getresponse().getheaders() | 319 headers = conn.getresponse().getheaders() |
310 server_header = dict(headers)['server'] | 320 server_header = dict(headers)['server'] |
311 self.assertIn('TornadoServer', server_header) | 321 self.assertIn('TornadoServer', server_header) |
312 | 322 |
313 @unittest.skipIf( | 323 @unittest.skipIf( |
(...skipping 158 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
472 'RequestId': self.counter.next(), | 482 'RequestId': self.counter.next(), |
473 'Type': 'Deployer', | 483 'Type': 'Deployer', |
474 'Request': 'Status', | 484 'Request': 'Status', |
475 }) | 485 }) |
476 self.assertIn('LastChanges', response['Response']) | 486 self.assertIn('LastChanges', response['Response']) |
477 changes = response['Response']['LastChanges'] | 487 changes = response['Response']['LastChanges'] |
478 self.assertEqual(2, len(changes)) | 488 self.assertEqual(2, len(changes)) |
479 statuses = [change['Status'] for change in changes] | 489 statuses = [change['Status'] for change in changes] |
480 self.assertEqual(['completed', 'completed'], statuses) | 490 self.assertEqual(['completed', 'completed'], statuses) |
481 | 491 |
| 492 def test_nrpe_check_available(self): |
| 493 # Make sure the check-app-access.sh script's ADDRESS is available. |
| 494 conn = httplib.HTTPSConnection(self.hostname) |
| 495 # This request matches the ADDRESS var in the script. |
| 496 conn.request('GET', '/juju-ui/version.js') |
| 497 message = 'ADDRESS in check-app-access.sh is not accessible.' |
| 498 self.assertEqual(200, conn.getresponse().status, message) |
| 499 |
482 | 500 |
483 if __name__ == '__main__': | 501 if __name__ == '__main__': |
484 unittest.main(verbosity=2) | 502 unittest.main(verbosity=2) |
OLD | NEW |