~charmers/charms/precise/juju-gui/trunk

« back to all changes in this revision

Viewing changes to tests/20-functional.test

  • Committer: Francesco Banconi
  • Date: 2013-10-09 10:48:53 UTC
  • mfrom: (60.13.41 trunk)
  • Revision ID: francesco.banconi@canonical.com-20131009104853-6b8oxx4k1ycmv2yk
Tags: 0.11.0
MergedĀ juju-guiĀ trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
# You should have received a copy of the GNU Affero General Public License
17
17
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
18
 
 
19
from __future__ import print_function
19
20
import httplib
 
21
import itertools
20
22
import unittest
21
23
import urlparse
22
24
 
23
25
from selenium.webdriver import Firefox
24
26
from selenium.webdriver.support import ui
25
27
from xvfbwrapper import Xvfb
 
28
import yaml
26
29
 
27
30
# XXX 2013-07-30 benji bug=872264: Don't use juju_deploy directly, use
28
 
# DeployTestMixin.juju_deploy instead.  See comment in stop_services.
 
31
# DeployTestMixin.juju_deploy instead.  See comment in the method.
29
32
from deploy import juju_deploy
30
33
from helpers import (
 
34
    get_admin_secret,
31
35
    juju_destroy_service,
32
36
    juju_version,
33
 
    ssh,
 
37
    stop_services,
 
38
    WebSocketClient,
34
39
)
35
 
 
 
40
import example
36
41
 
37
42
JUJU_GUI_TEST_BRANCH = 'lp:~juju-gui/juju-gui/charm-tests-branch'
38
43
STAGING_SERVICES = ('haproxy', 'mediawiki', 'memcached', 'mysql', 'wordpress')
39
44
is_legacy_juju = juju_version().major == 0
 
45
try:
 
46
    admin_secret = get_admin_secret()
 
47
except ValueError as err:
 
48
    admin_secret = None
 
49
    print(err)
40
50
 
41
51
 
42
52
class DeployTestMixin(object):
52
62
        # Create a Selenium browser instance.
53
63
        selenium = self.selenium = Firefox()
54
64
        self.addCleanup(selenium.quit)
55
 
 
56
 
    def tearDown(self):
57
 
        juju_destroy_service(self.charm)
 
65
        super(DeployTestMixin, self).setUp()
58
66
 
59
67
    def assertEnvironmentIsConnected(self):
60
68
        """Assert the GUI environment is connected to the Juju API agent."""
88
96
        def page_ready(driver):
89
97
            driver.get(url)
90
98
            return driver.title == 'Juju Admin'
91
 
        self.wait_for(page_ready, error='Juju GUI not found.')
 
99
        self.wait_for(page_ready, error='Juju GUI not found.', timeout=60)
92
100
 
93
101
    def wait_for(self, condition, error=None, timeout=30):
94
102
        """Wait for condition to be True.
127
135
        services = self.wait_for(services_found, 'Services not displayed.')
128
136
        return set([element.text for element in services])
129
137
 
130
 
    def stop_services(self, hostname, services):
 
138
    def juju_deploy(self, *args, **kwargs):
 
139
        """Shim in our additional cleanup for pyJuju."""
131
140
        # XXX 2012-11-29 frankban bug=872264:
132
141
            # Just invoking ``juju destroy-service juju-gui`` in tearDown
133
142
            # should execute the ``stop`` hook, stopping all the services
134
143
            # started by the charm in the machine. Right now this does not
135
144
            # work in pyJuju, so the desired effect is achieved by keeping
136
145
            # track of started services and manually stopping them here.
137
 
        target = 'ubuntu@{}'.format(hostname)
138
 
        for service in services:
139
 
            ssh(target, 'sudo', 'service', service, 'stop')
140
 
 
141
 
    def juju_deploy(self, *args, **kws):
142
 
        """Shim in our additional cleanup for pyJuju."""
143
 
        # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.
144
146
        # Once pyJuju works correctly or we drop support for it altogether, we
145
147
        # can remove this shim.
146
 
        unit_info = juju_deploy(*args, **kws)
 
148
        unit_info = juju_deploy(*args, **kwargs)
147
149
        if is_legacy_juju:
148
150
            hostname = unit_info['public-address']
149
 
            services = ['haproxy', 'apache2']
 
151
            options = kwargs.get('options', {})
 
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']
150
157
            # Staging uses improv, otherwise the API agent is used.
151
 
            if kws.get('options').get('staging') == 'true':
 
158
            if options.get('staging') == 'true':
152
159
                services.append('juju-api-improv')
153
160
            else:
154
161
                services.append('juju-api-agent')
155
 
            self.addCleanup(self.stop_services, hostname, services)
 
162
            self.addCleanup(stop_services, hostname, services)
156
163
        return unit_info
157
164
 
158
165
 
159
 
class DeployTest(DeployTestMixin, unittest.TestCase):
160
 
 
161
 
    def test_api_agent(self):
162
 
        # Ensure the Juju GUI and API agent services are correctly set up.
 
166
class TestDeploy(DeployTestMixin, unittest.TestCase):
 
167
 
 
168
    def tearDown(self):
 
169
        juju_destroy_service(self.charm)
 
170
 
 
171
    def test_local_release(self):
 
172
        # Ensure the Juju GUI and API agent services are correctly set up when
 
173
        # deploying the local release.
163
174
        unit_info = self.juju_deploy(self.charm)
164
175
        hostname = unit_info['public-address']
165
176
        self.navigate_to(hostname)
166
177
        self.handle_browser_warning()
167
178
        self.assertEnvironmentIsConnected()
168
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()
 
189
 
169
190
    @unittest.skipUnless(is_legacy_juju, 'staging only works in pyJuju')
170
191
    def test_staging(self):
171
192
        # Ensure the Juju GUI and improv services are correctly set up.
179
200
 
180
201
    def test_sandbox(self):
181
202
        # The GUI is correctly deployed and set up in sandbox mode.
182
 
        unit_info = self.juju_deploy(
183
 
            self.charm, options={'builtin-server': 'true', 'sandbox': 'true'})
184
 
        hostname = unit_info['public-address']
185
 
        self.navigate_to(hostname)
186
 
        self.handle_browser_warning()
187
 
        self.assertEnvironmentIsConnected()
188
 
 
189
 
    def test_builtin_server(self):
190
 
        # Ensure the Juju GUI and builtin server are correctly set up.
191
 
        unit_info = self.juju_deploy(
192
 
            self.charm, options={'builtin-server': 'true'})
193
 
        hostname = unit_info['public-address']
194
 
        self.navigate_to(hostname)
195
 
        self.handle_browser_warning()
196
 
        self.assertEnvironmentIsConnected()
197
 
        conn = httplib.HTTPSConnection(hostname)
198
 
        conn.request('HEAD', '/')
199
 
        headers = conn.getresponse().getheaders()
200
 
        server_header = dict(headers)['server']
201
 
        self.assertIn('TornadoServer', server_header)
 
203
        unit_info = self.juju_deploy(self.charm, options={'sandbox': 'true'})
 
204
        hostname = unit_info['public-address']
 
205
        self.navigate_to(hostname)
 
206
        self.handle_browser_warning()
 
207
        self.assertEnvironmentIsConnected()
202
208
 
203
209
    def test_branch_source(self):
204
210
        # Ensure the Juju GUI is correctly deployed from a Bazaar branch.
205
 
        unit_info = self.juju_deploy(
206
 
            self.charm, options={'juju-gui-source': JUJU_GUI_TEST_BRANCH})
 
211
        options = {'juju-gui-source': JUJU_GUI_TEST_BRANCH}
 
212
        unit_info = self.juju_deploy(self.charm, options=options)
207
213
        hostname = unit_info['public-address']
208
214
        self.navigate_to(hostname)
209
215
        self.handle_browser_warning()
210
216
        self.assertEnvironmentIsConnected()
211
217
 
212
 
    def test_cache_headers(self):
213
 
        # Make sure the correct cache headers are sent.
214
 
        unit_info = self.juju_deploy(
215
 
            self.charm, options={'juju-gui-source': JUJU_GUI_TEST_BRANCH})
 
218
    def test_legacy_server(self):
 
219
        # The legacy apache + haproxy server configuration works correctly.
 
220
        # Also make sure the correct cache headers are sent.
 
221
        options = {
 
222
            'builtin-server': False,
 
223
            'juju-gui-source': JUJU_GUI_TEST_BRANCH,
 
224
        }
 
225
        unit_info = self.juju_deploy(self.charm, options=options)
216
226
        hostname = unit_info['public-address']
217
227
        conn = httplib.HTTPSConnection(hostname)
218
228
        conn.request('HEAD', '/')
237
247
 
238
248
    def test_nrpe_check_available(self):
239
249
        # Make sure the check-app-access.sh script's ADDRESS is available.
240
 
        unit_info = self.juju_deploy(
241
 
            self.charm, options={'juju-gui-source': JUJU_GUI_TEST_BRANCH})
 
250
        options = {'juju-gui-source': JUJU_GUI_TEST_BRANCH}
 
251
        unit_info = self.juju_deploy(self.charm, options=options)
242
252
        hostname = unit_info['public-address']
243
253
        conn = httplib.HTTPSConnection(hostname)
244
254
        # This request matches the ADDRESS var in the script.
247
257
        self.assertEqual(200, conn.getresponse().status, message)
248
258
 
249
259
 
 
260
class TestBuiltinServer(DeployTestMixin, unittest.TestCase):
 
261
 
 
262
    @classmethod
 
263
    def setUpClass(cls):
 
264
        # Deploy the charm. The resulting service is used by all the tests
 
265
        # in this test case.
 
266
        unit_info = juju_deploy(cls.charm)
 
267
        cls.hostname = unit_info['public-address']
 
268
        # The counter is used to produce API request identifiers.
 
269
        cls.counter = itertools.count()
 
270
 
 
271
    @classmethod
 
272
    def tearDownClass(cls):
 
273
        # Destroy the GUI service, and perform additional clean up in the case
 
274
        # we are in a pyJuju environment.
 
275
        juju_destroy_service(cls.charm)
 
276
        if is_legacy_juju:
 
277
            # XXX 2012-11-29 frankban bug=872264:
 
278
            # see DeployTestMixin.juju_deploy above.
 
279
            stop_services(cls.hostname, ['guiserver', 'juju-api-agent'])
 
280
 
 
281
    def make_websocket_client(self, authenticated=True):
 
282
        """Create and return a WebSocket client connected to the Juju backend.
 
283
 
 
284
        If authenticated is set to True, also log in to the Juju API server.
 
285
        """
 
286
        client = WebSocketClient('wss://{}:443/ws'.format(self.hostname))
 
287
        client.connect()
 
288
        self.addCleanup(client.close)
 
289
        if authenticated:
 
290
            response = client.send({
 
291
                'RequestId': self.counter.next(),
 
292
                'Type': 'Admin',
 
293
                'Request': 'Login',
 
294
                'Params': {'AuthTag': 'user-admin', 'Password': admin_secret},
 
295
            })
 
296
            self.assertNotIn('Error', response)
 
297
        return client
 
298
 
 
299
    def test_environment_connection(self):
 
300
        # Ensure the Juju GUI and builtin server are correctly set up.
 
301
        self.navigate_to(self.hostname)
 
302
        self.handle_browser_warning()
 
303
        self.assertEnvironmentIsConnected()
 
304
 
 
305
    def test_headers(self):
 
306
        # Ensure the Tornado headers are correctly sent.
 
307
        conn = httplib.HTTPSConnection(self.hostname)
 
308
        conn.request('HEAD', '/')
 
309
        headers = conn.getresponse().getheaders()
 
310
        server_header = dict(headers)['server']
 
311
        self.assertIn('TornadoServer', server_header)
 
312
 
 
313
    @unittest.skipIf(
 
314
        is_legacy_juju, 'bundle deployments are only supported in juju-core')
 
315
    def test_deployer_not_authenticated(self):
 
316
        # An error is returned trying to start a bundle deployment without
 
317
        # being authenticated.
 
318
        client = self.make_websocket_client(authenticated=False)
 
319
        response = client.send({
 
320
            'RequestId': self.counter.next(),
 
321
            'Type': 'Deployer',
 
322
            'Request': 'Import',
 
323
            'Params': {'Name': 'bundle-name', 'YAML': 'foo: bar'},
 
324
        })
 
325
        self.assertIn('Error', response)
 
326
        self.assertEqual(
 
327
            'unauthorized access: no user logged in', response['Error'])
 
328
 
 
329
    @unittest.skipUnless(admin_secret, 'admin secret was not found')
 
330
    @unittest.skipIf(
 
331
        is_legacy_juju, 'bundle deployments are only supported in juju-core')
 
332
    def test_deployer_invalid_bundle_name(self):
 
333
        # An error is returned trying to deploy a bundle with an invalid name.
 
334
        client = self.make_websocket_client()
 
335
        response = client.send({
 
336
            'RequestId': self.counter.next(),
 
337
            'Type': 'Deployer',
 
338
            'Request': 'Import',
 
339
            'Params': {'Name': 'no-such', 'YAML': example.BUNDLE1},
 
340
        })
 
341
        self.assertIn('Error', response)
 
342
        self.assertEqual(
 
343
            'invalid request: bundle no-such not found', response['Error'])
 
344
 
 
345
    @unittest.skipUnless(admin_secret, 'admin secret was not found')
 
346
    @unittest.skipIf(
 
347
        is_legacy_juju, 'bundle deployments are only supported in juju-core')
 
348
    def test_deployer_invalid_bundle_yaml(self):
 
349
        # An error is returned trying to deploy an invalid bundle YAML.
 
350
        client = self.make_websocket_client()
 
351
        response = client.send({
 
352
            'RequestId': self.counter.next(),
 
353
            'Type': 'Deployer',
 
354
            'Request': 'Import',
 
355
            'Params': {'Name': 'bundle-name', 'YAML': 42},
 
356
        })
 
357
        self.assertIn('Error', response)
 
358
        self.assertIn(
 
359
            'invalid request: invalid YAML contents', response['Error'])
 
360
 
 
361
    @unittest.skipUnless(admin_secret, 'admin secret was not found')
 
362
    @unittest.skipIf(
 
363
        is_legacy_juju, 'bundle deployments are only supported in juju-core')
 
364
    def test_deployer_watch_unknown_deployment(self):
 
365
        # An error is returned trying to watch an unknown deployment.
 
366
        client = self.make_websocket_client()
 
367
        response = client.send({
 
368
            'RequestId': self.counter.next(),
 
369
            'Type': 'Deployer',
 
370
            'Request': 'Watch',
 
371
            'Params': {'DeploymentId': 424242},
 
372
        })
 
373
        self.assertIn('Error', response)
 
374
        self.assertEqual(
 
375
            'invalid request: deployment not found', response['Error'])
 
376
 
 
377
    @unittest.skipUnless(admin_secret, 'admin secret was not found')
 
378
    @unittest.skipIf(
 
379
        is_legacy_juju, 'bundle deployments are only supported in juju-core')
 
380
    def test_deployer(self):
 
381
        # The builtin server supports deploying bundles using juju-deployer.
 
382
        client = self.make_websocket_client()
 
383
 
 
384
        # Start a first bundle deployment.
 
385
        response = client.send({
 
386
            'RequestId': self.counter.next(),
 
387
            'Type': 'Deployer',
 
388
            'Request': 'Import',
 
389
            'Params': {'Name': 'bundle1', 'YAML': example.BUNDLE1},
 
390
        })
 
391
        self.assertNotIn('Error', response)
 
392
        self.assertIn('DeploymentId', response['Response'])
 
393
        # Schedule the removal of the services deployed processing the bundle.
 
394
        bundle_data = yaml.safe_load(example.BUNDLE1)
 
395
        services = bundle_data['bundle1']['services'].keys()
 
396
        for service in services:
 
397
            self.addCleanup(juju_destroy_service, service)
 
398
 
 
399
        # Start a second bundle deployment: the bundle name can be omitted if
 
400
        # the YAML contains only one bundle.
 
401
        response = client.send({
 
402
            'RequestId': self.counter.next(),
 
403
            'Type': 'Deployer',
 
404
            'Request': 'Import',
 
405
            'Params': {'YAML': example.BUNDLE2},
 
406
        })
 
407
        self.assertNotIn('Error', response)
 
408
        self.assertIn('DeploymentId', response['Response'])
 
409
        # Store the deployment id to be used later.
 
410
        deployment_id = response['Response']['DeploymentId']
 
411
        # Schedule the removal of the services deployed processing the bundle.
 
412
        bundle_data = yaml.safe_load(example.BUNDLE2)
 
413
        services = bundle_data['bundle2']['services'].keys()
 
414
        for service in services:
 
415
            self.addCleanup(juju_destroy_service, service)
 
416
 
 
417
        # Check the bundle deployments status.
 
418
        response = client.send({
 
419
            'RequestId': self.counter.next(),
 
420
            'Type': 'Deployer',
 
421
            'Request': 'Status',
 
422
        })
 
423
        self.assertIn('LastChanges', response['Response'])
 
424
        changes = response['Response']['LastChanges']
 
425
        self.assertEqual(2, len(changes))
 
426
        change1, change2 = changes
 
427
        self.assertEqual(0, change1['Queue'])
 
428
        self.assertEqual('started', change1['Status'])
 
429
        self.assertEqual(1, change2['Queue'])
 
430
        self.assertEqual('scheduled', change2['Status'])
 
431
 
 
432
        # Start watching the second deployment.
 
433
        response = client.send({
 
434
            'RequestId': self.counter.next(),
 
435
            'Type': 'Deployer',
 
436
            'Request': 'Watch',
 
437
            'Params': {'DeploymentId': deployment_id},
 
438
        })
 
439
        self.assertNotIn('Error', response)
 
440
        self.assertIn('WatcherId', response['Response'])
 
441
        watcher_id = response['Response']['WatcherId']
 
442
 
 
443
        # Observe three changes on the second deployment.
 
444
        for status in ('scheduled', 'started', 'completed'):
 
445
            response = client.send({
 
446
                'RequestId': self.counter.next(),
 
447
                'Type': 'Deployer',
 
448
                'Request': 'Next',
 
449
                'Params': {'WatcherId': watcher_id},
 
450
            })
 
451
            self.assertNotIn('Error', response)
 
452
            self.assertIn('Changes', response['Response'])
 
453
            changes = response['Response']['Changes']
 
454
            self.assertEqual(1, len(changes))
 
455
            self.assertEqual(status, changes[0]['Status'])
 
456
 
 
457
        # An error is returned trying to re-deploy a bundle.
 
458
        response = client.send({
 
459
            'RequestId': self.counter.next(),
 
460
            'Type': 'Deployer',
 
461
            'Request': 'Import',
 
462
            'Params': {'YAML': example.BUNDLE1},
 
463
        })
 
464
        self.assertIn('Error', response)
 
465
        self.assertEqual(
 
466
            'invalid request: service(s) already in the environment: '
 
467
            'wordpress, mysql',
 
468
            response['Error'])
 
469
 
 
470
        # Check the final bundle deployment status.
 
471
        response = client.send({
 
472
            'RequestId': self.counter.next(),
 
473
            'Type': 'Deployer',
 
474
            'Request': 'Status',
 
475
        })
 
476
        self.assertIn('LastChanges', response['Response'])
 
477
        changes = response['Response']['LastChanges']
 
478
        self.assertEqual(2, len(changes))
 
479
        statuses = [change['Status'] for change in changes]
 
480
        self.assertEqual(['completed', 'completed'], statuses)
 
481
 
 
482
 
250
483
if __name__ == '__main__':
251
484
    unittest.main(verbosity=2)