1
# This file is part of the Juju GUI, which lets users view and manage Juju
2
# environments within a graphical interface (https://launchpad.net/juju-gui).
3
# Copyright (C) 2013 Canonical Ltd.
5
# This program is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU Affero General Public License version 3, as published by
7
# the Free Software Foundation.
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
# Affero General Public License for more details.
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
"""Tests for the bundle deployment views."""
20
from tornado import concurrent
21
from tornado.testing import(
28
from guiserver.bundles import views
29
from guiserver.tests import helpers
32
class ViewsTestMixin(object):
33
"""Base helpers and common tests for all the view tests.
35
Subclasses must define a get_view() method returning the view function to
36
be tested. Subclasses can also override the invalid_params and
37
invalid_params_error attributes, used to test the view in the case the
38
passed parameters are not valid.
41
invalid_params = {'No-such': 'parameter'}
42
invalid_params_error = 'invalid request: invalid data parameters'
45
super(ViewsTestMixin, self).setUp()
46
self.view = self.get_view()
47
self.deployer = mock.Mock()
49
def make_future(self, result):
50
"""Create and return a Future containing the given result."""
51
future = concurrent.Future()
52
future.set_result(result)
56
def test_not_authenticated(self):
57
# An error response is returned if the user is not authenticated.
58
request = self.make_view_request(is_authenticated=False)
59
expected_log = 'deployer: unauthorized access: no user logged in'
60
with ExpectLog('', expected_log, required=True):
61
response = yield self.view(request, self.deployer)
64
'Error': 'unauthorized access: no user logged in',
66
self.assertEqual(expected_response, response)
67
# The Deployer methods have not been called.
68
self.assertEqual(0, len(self.deployer.mock_calls))
71
def test_invalid_parameters(self):
72
# An error response is returned if the parameters in the request are
74
request = self.make_view_request(params=self.invalid_params)
75
expected_log = 'deployer: {}'.format(self.invalid_params_error)
76
with ExpectLog('', expected_log, required=True):
77
response = yield self.view(request, self.deployer)
80
'Error': self.invalid_params_error,
82
self.assertEqual(expected_response, response)
83
# The Deployer methods have not been called.
84
self.assertEqual(0, len(self.deployer.mock_calls))
87
class TestImportBundle(
88
ViewsTestMixin, helpers.BundlesTestMixin, LogTrapTestCase,
92
return views.import_bundle
95
def test_invalid_yaml(self):
96
# An error response is returned if an invalid YAML encoded string is
98
params = {'Name': 'bundle-name', 'YAML': 42}
99
request = self.make_view_request(params=params)
100
response = yield self.view(request, self.deployer)
101
expected_response = {
103
'Error': 'invalid request: invalid YAML contents: '
104
"'int' object has no attribute 'read'",
106
self.assertEqual(expected_response, response)
107
# The Deployer methods have not been called.
108
self.assertEqual(0, len(self.deployer.mock_calls))
111
def test_no_name_failure(self):
112
# An error response is returned if the requested bundle name is not
113
# provided and the YAML contents include multiple bundles
114
params = {'YAML': 'bundle1: contents1\nbundle2: contents2'}
115
request = self.make_view_request(params=params)
116
response = yield self.view(request, self.deployer)
117
expected_response = {
119
'Error': 'invalid request: invalid data parameters: '
120
'no bundle name provided',
122
self.assertEqual(expected_response, response)
123
# The Deployer methods have not been called.
124
self.assertEqual(0, len(self.deployer.mock_calls))
127
def test_bundle_not_found(self):
128
# An error response is returned if the requested bundle name is not
129
# found in the bundle YAML contents.
130
params = {'Name': 'no-such-bundle', 'YAML': 'mybundle: mycontents'}
131
request = self.make_view_request(params=params)
132
response = yield self.view(request, self.deployer)
133
expected_response = {
135
'Error': 'invalid request: bundle no-such-bundle not found',
137
self.assertEqual(expected_response, response)
138
# The Deployer methods have not been called.
139
self.assertEqual(0, len(self.deployer.mock_calls))
142
def test_invalid_bundle(self):
143
# An error response is returned if the bundle is not well formed.
144
params = {'Name': 'mybundle', 'YAML': 'mybundle: not valid'}
145
request = self.make_view_request(params=params)
146
response = yield self.view(request, self.deployer)
147
expected_response = {
149
'Error': 'invalid request: invalid bundle mybundle: '
150
'the bundle data is not well formed',
152
self.assertEqual(expected_response, response)
153
# The Deployer methods have not been called.
154
self.assertEqual(0, len(self.deployer.mock_calls))
157
def test_invalid_bundle_constraints(self):
158
# An error response is returned if the bundle includes services with
159
# unsupported constraints.
162
'YAML': 'mybundle: {services: {django: {constraints: invalid=1}}}',
164
request = self.make_view_request(params=params)
165
response = yield self.view(request, self.deployer)
166
expected_response = {
168
'Error': 'invalid request: invalid bundle mybundle: '
169
'unsupported constraints: invalid',
171
self.assertEqual(expected_response, response)
172
# The Deployer methods have not been called.
173
self.assertEqual(0, len(self.deployer.mock_calls))
176
def test_undeployable_bundle(self):
177
# An error response is returned if the bundle cannot be imported in the
178
# current Juju environment.
179
params = {'Name': 'mybundle', 'YAML': 'mybundle: {services: {}}'}
180
request = self.make_view_request(params=params)
181
# Simulate an error returned by the Deployer validate method.
182
self.deployer.validate.return_value = self.make_future('an error')
184
response = yield self.view(request, self.deployer)
185
expected_response = {
187
'Error': 'invalid request: an error',
189
self.assertEqual(expected_response, response)
190
# The Deployer validate method has been called.
191
self.deployer.validate.assert_called_once_with(
192
request.user, 'mybundle', {'services': {}})
195
def test_success(self):
196
# The response includes the deployment identifier.
197
params = {'Name': 'mybundle', 'YAML': 'mybundle: {services: {}}'}
198
request = self.make_view_request(params=params)
199
# Set up the Deployer mock.
200
self.deployer.validate.return_value = self.make_future(None)
201
self.deployer.import_bundle.return_value = 42
203
response = yield self.view(request, self.deployer)
204
expected_response = {'Response': {'DeploymentId': 42}}
205
self.assertEqual(expected_response, response)
206
# Ensure the Deployer methods have been correctly called.
207
args = (request.user, 'mybundle', {'services': {}})
208
self.deployer.validate.assert_called_once_with(*args)
209
args = (request.user, 'mybundle', {'services': {}}, None)
210
self.deployer.import_bundle.assert_called_once_with(*args)
213
def test_logging(self):
214
# The beginning of the bundle import process is properly logged.
215
params = {'Name': 'mybundle', 'YAML': 'mybundle: {services: {}}'}
216
request = self.make_view_request(params=params)
217
# Set up the Deployer mock.
218
self.deployer.validate.return_value = self.make_future(None)
219
self.deployer.import_bundle.return_value = 42
221
expected_log = "import_bundle: scheduling 'mybundle' deployment"
222
with ExpectLog('', expected_log, required=True):
223
yield self.view(request, self.deployer)
225
# The following tests exercise views._validate_import_params directly.
226
def test_no_name_success(self):
227
# The process succeeds if the bundle name is not provided but the
228
# YAML contents include just one bundle.
229
params = {'YAML': 'mybundle: {services: {}}'}
230
results = views._validate_import_params(params)
231
expected = ('mybundle', {'services': {}}, None)
232
self.assertEqual(expected, results)
234
def test_id_provided(self):
235
params = {'YAML': 'mybundle: {services: {}}',
236
'BundleID': '~jorge/wiki/3/smallwiki'}
237
results = views._validate_import_params(params)
238
expected = ('mybundle', {'services': {}}, '~jorge/wiki/3/smallwiki')
239
self.assertEqual(expected, results)
241
def test_id_and_name_provided(self):
242
params = {'YAML': 'mybundle: {services: {}}',
244
'BundleID': '~jorge/wiki/3/smallwiki'}
245
results = views._validate_import_params(params)
246
expected = ('mybundle', {'services': {}}, '~jorge/wiki/3/smallwiki')
247
self.assertEqual(expected, results)
250
def test_id_passed_to_deployer(self):
251
params = {'YAML': 'mybundle: {services: {}}',
253
'BundleID': '~jorge/wiki/3/smallwiki'}
254
request = self.make_view_request(params=params)
255
# Set up the Deployer mock.
256
self.deployer.validate.return_value = self.make_future(None)
257
self.deployer.import_bundle.return_value = 42
259
yield self.view(request, self.deployer)
260
# Ensure the Deployer methods have been correctly called.
261
args = (request.user, 'mybundle', {'services': {}})
262
self.deployer.validate.assert_called_once_with(*args)
263
args = (request.user, 'mybundle', {'services': {}},
264
'~jorge/wiki/3/smallwiki')
265
self.deployer.import_bundle.assert_called_once_with(*args)
269
ViewsTestMixin, helpers.BundlesTestMixin, LogTrapTestCase,
276
def test_deployment_not_found(self):
277
# An error response is returned if the deployment identifier is not
279
request = self.make_view_request(params={'DeploymentId': 42})
280
# Set up the Deployer mock.
281
self.deployer.watch.return_value = None
283
response = yield self.view(request, self.deployer)
284
expected_response = {
286
'Error': 'invalid request: deployment not found',
288
self.assertEqual(expected_response, response)
289
# Ensure the Deployer methods have been correctly called.
290
self.deployer.watch.assert_called_once_with(42)
293
def test_success(self):
294
# The response includes the watcher identifier.
295
request = self.make_view_request(params={'DeploymentId': 42})
296
# Set up the Deployer mock.
297
self.deployer.watch.return_value = 47
299
response = yield self.view(request, self.deployer)
300
expected_response = {'Response': {'WatcherId': 47}}
301
self.assertEqual(expected_response, response)
302
# Ensure the Deployer methods have been correctly called.
303
self.deployer.watch.assert_called_once_with(42)
306
def test_logging(self):
307
# The beginning of the bundle watch process is properly logged.
308
request = self.make_view_request(params={'DeploymentId': 42})
309
# Set up the Deployer mock.
310
self.deployer.watch.return_value = 47
312
expected_log = 'watch: deployment 42 being observed by watcher 47'
313
with ExpectLog('', expected_log, required=True):
314
yield self.view(request, self.deployer)
318
ViewsTestMixin, helpers.BundlesTestMixin, LogTrapTestCase,
325
def test_invalid_watcher_identifier(self):
326
# An error response is returned if the watcher identifier is not valid.
327
request = self.make_view_request(params={'WatcherId': 42})
328
# Set up the Deployer mock.
329
self.deployer.next.return_value = self.make_future(None)
331
response = yield self.view(request, self.deployer)
332
expected_response = {
334
'Error': 'invalid request: invalid watcher identifier',
336
self.assertEqual(expected_response, response)
337
# Ensure the Deployer methods have been correctly called.
338
self.deployer.next.assert_called_once_with(42)
341
def test_success(self):
342
# The response includes the deployment changes.
343
request = self.make_view_request(params={'WatcherId': 42})
344
# Set up the Deployer mock.
345
changes = ['change1', 'change2']
346
self.deployer.next.return_value = self.make_future(changes)
348
response = yield self.view(request, self.deployer)
349
expected_response = {'Response': {'Changes': changes}}
350
self.assertEqual(expected_response, response)
351
# Ensure the Deployer methods have been correctly called.
352
self.deployer.next.assert_called_once_with(42)
355
def test_logging(self):
356
# The watcher next request is properly logged.
357
request = self.make_view_request(params={'WatcherId': 42})
358
# Set up the Deployer mock.
359
changes = ['change1', 'change2']
360
self.deployer.next.return_value = self.make_future(changes)
362
expected_request_log = 'next: requested changes for watcher 42'
363
expected_response_log = 'next: returning changes for watcher 42'
364
with ExpectLog('', expected_request_log, required=True):
365
with ExpectLog('', expected_response_log, required=True):
366
yield self.view(request, self.deployer)
370
ViewsTestMixin, helpers.BundlesTestMixin, LogTrapTestCase,
377
def test_invalid_deployment(self):
378
# An error response is returned if the deployment identifier is not
380
request = self.make_view_request(params={'DeploymentId': 42})
381
# Set up the Deployer mock.
382
self.deployer.cancel.return_value = 'bad wolf'
384
response = yield self.view(request, self.deployer)
385
expected_response = {
387
'Error': 'invalid request: bad wolf',
389
self.assertEqual(expected_response, response)
390
# Ensure the Deployer methods have been correctly called.
391
self.deployer.cancel.assert_called_once_with(42)
394
def test_success(self):
395
# An empty response is returned if everything is ok.
396
request = self.make_view_request(params={'DeploymentId': 42})
397
# Set up the Deployer mock.
398
self.deployer.cancel.return_value = None
400
response = yield self.view(request, self.deployer)
401
self.assertEqual({'Response': {}}, response)
402
# Ensure the Deployer methods have been correctly called.
403
self.deployer.cancel.assert_called_once_with(42)
406
def test_logging(self):
407
# The bundle cancellation is properly logged.
408
request = self.make_view_request(params={'DeploymentId': 42})
409
# Set up the Deployer mock.
410
self.deployer.cancel.return_value = None
412
expected_log = 'cancel: deployment 42 cancelled'
413
with ExpectLog('', expected_log, required=True):
414
yield self.view(request, self.deployer)
418
ViewsTestMixin, helpers.BundlesTestMixin, LogTrapTestCase,
421
invalid_params_error = 'invalid request: invalid data parameters: No-such'
427
def test_success(self):
428
# The response includes the watcher identifier.
429
request = self.make_view_request()
430
# Set up the Deployer mock.
431
last_changes = ['change1', 'change2']
432
self.deployer.status.return_value = last_changes
434
response = yield self.view(request, self.deployer)
435
expected_response = {'Response': {'LastChanges': last_changes}}
436
self.assertEqual(expected_response, response)
437
# Ensure the Deployer methods have been correctly called.
438
self.deployer.status.assert_called_once_with()
441
def test_logging(self):
442
# The status request is properly logged.
443
request = self.make_view_request()
444
# Set up the Deployer mock.
445
self.deployer.status.return_value = []
447
expected_log = 'status: returning last changes'
448
with ExpectLog('', expected_log, required=True):
449
yield self.view(request, self.deployer)