~lutostag/ubuntu/utopic/maas/1.5.2+packagefix

« back to all changes in this revision

Viewing changes to src/maasserver/tests/test_views.py

  • Committer: Package Import Robot
  • Author(s): Andres Rodriguez, Graham Binns, Andres Rodriguez, Julian Edwards, Seth Arnold
  • Date: 2014-02-15 12:08:23 UTC
  • mfrom: (1.2.22)
  • Revision ID: package-import@ubuntu.com-20140215120823-kew2lqrw5qgww721
Tags: 1.5+bzr1948-0ubuntu1
* New upstream release.

[ Graham Binns ]
* debian/control: Depends on python-jsonschema.

[ Andres Rodriguez ]
* debian/maas-region-controller-min.posinst: Make txlongpoll.yaml only
  readable by the app and not world readeable.
* debian/patches/02-pserv-config.patch: Refreshed.

[ Julian Edwards ]
* debian/extras/maas-cli renamed to debian/extras/maas, and introduce
  a deprecation warning in favour of using maas over maas-cli.
* debian/extras/maas renamed to debian/extras/maas-region-admin
* debian/maas-cli.install: install debian/extras/maas
* debian/maas-dns.postinst: Invoke maas-region-admin instead of maas
* debian/maas-region-controller-min.install: install maas-region-admin
  instead of maas
* debian/maas-region-controller.postinst: Invoke maas-region-admin instead
  of maas
* debian/maas-cli.links: Link from maas to maas-cli for backward compat.

[ Seth Arnold ]
* debian/maas-region-controller-min.postinst: Make sure txlongpoll.yaml
  gets correct permissions on upgrade (LP: #1254034)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2012-2014 Canonical Ltd.  This software is licensed under the
2
 
# GNU Affero General Public License version 3 (see the file LICENSE).
3
 
 
4
 
"""Test maasserver API."""
5
 
 
6
 
from __future__ import (
7
 
    absolute_import,
8
 
    print_function,
9
 
    unicode_literals,
10
 
    )
11
 
 
12
 
str = None
13
 
 
14
 
__metaclass__ = type
15
 
__all__ = []
16
 
 
17
 
import httplib
18
 
from random import randint
19
 
from xmlrpclib import Fault
20
 
 
21
 
from django.conf.urls import patterns
22
 
from django.core.exceptions import PermissionDenied
23
 
from django.core.urlresolvers import reverse
24
 
from django.http import Http404
25
 
from django.test.client import RequestFactory
26
 
from django.utils.html import escape
27
 
from lxml.html import fromstring
28
 
from maasserver.components import register_persistent_error
29
 
from maasserver.exceptions import ExternalComponentException
30
 
from maasserver.testing import extract_redirect
31
 
from maasserver.testing.factory import factory
32
 
from maasserver.testing.testcase import MAASServerTestCase
33
 
from maasserver.views import (
34
 
    HelpfulDeleteView,
35
 
    PaginatedListView,
36
 
    )
37
 
from maasserver.views.nodes import NodeEdit
38
 
from testtools.matchers import ContainsAll
39
 
 
40
 
 
41
 
class Test404500(MAASServerTestCase):
42
 
    """Test pages displayed when an error 404 or an error 500 occur."""
43
 
 
44
 
    def test_404(self):
45
 
        self.client_log_in()
46
 
        response = self.client.get('/no-found-page/')
47
 
        doc = fromstring(response.content)
48
 
        self.assertIn(
49
 
            "Error: Page not found",
50
 
            doc.cssselect('title')[0].text)
51
 
        self.assertSequenceEqual(
52
 
            ['The requested URL /no-found-page/ was not found on this '
53
 
             'server.'],
54
 
            [elem.text.strip() for elem in
55
 
                doc.cssselect('h2')])
56
 
 
57
 
    def test_500(self):
58
 
        self.client_log_in()
59
 
        from maasserver.urls import urlpatterns
60
 
        urlpatterns += patterns(
61
 
            '',
62
 
            (r'^500/$', 'django.views.defaults.server_error'),
63
 
        )
64
 
        response = self.client.get('/500/')
65
 
        doc = fromstring(response.content)
66
 
        self.assertIn(
67
 
            "Internal server error",
68
 
            doc.cssselect('title')[0].text)
69
 
        self.assertSequenceEqual(
70
 
            ['Internal server error.'],
71
 
            [elem.text.strip() for elem in
72
 
                doc.cssselect('h2')])
73
 
 
74
 
 
75
 
class TestSnippets(MAASServerTestCase):
76
 
 
77
 
    def _assertTemplateExistsAndContains(self, content, template_selector,
78
 
                                         contains_selector, reverse=False):
79
 
        doc = fromstring(content)
80
 
        snippets = doc.cssselect(template_selector)
81
 
        # The snippet exists.
82
 
        self.assertEqual(
83
 
            1, len(snippets),
84
 
            "The snippet '%s' does not exist." % template_selector)
85
 
        # It contains the required element.
86
 
        selects = fromstring(snippets[0].text).cssselect(contains_selector)
87
 
        if reverse:
88
 
            self.assertEqual(
89
 
                0, len(selects),
90
 
                "The element '%s' does exist." % contains_selector)
91
 
        else:
92
 
            self.assertEqual(
93
 
                1, len(selects),
94
 
                "The element '%s' does not exist." % contains_selector,)
95
 
 
96
 
    def assertTemplateExistsAndContains(self, content, template_selector,
97
 
                                        contains_selector, reverse=False):
98
 
        """Assert that the provided html 'content' contains a snippet as
99
 
        selected by 'template_selector' which in turn contains an element
100
 
        selected by 'contains_selector'.
101
 
        """
102
 
        self._assertTemplateExistsAndContains(
103
 
            content, template_selector, contains_selector)
104
 
 
105
 
    def assertTemplateExistsAndDoesNotContain(self, content, template_selector,
106
 
                                              contains_selector):
107
 
        """Assert that the provided html 'content' contains a snippet as
108
 
        selected by 'template_selector' which does not contains an element
109
 
        selected by 'contains_selector'.
110
 
        """
111
 
        self._assertTemplateExistsAndContains(
112
 
            content, template_selector, contains_selector, reverse=True)
113
 
 
114
 
    def test_architecture_snippet(self):
115
 
        self.client_log_in()
116
 
        response = self.client.get('/')
117
 
        self.assertTemplateExistsAndContains(
118
 
            response.content, '#add-node', 'select#id_architecture')
119
 
 
120
 
    def test_hostname(self):
121
 
        self.client_log_in()
122
 
        response = self.client.get('/')
123
 
        self.assertTemplateExistsAndContains(
124
 
            response.content, '#add-node', 'input#id_hostname')
125
 
 
126
 
    def test_after_commissioning_action_snippet(self):
127
 
        self.client_log_in()
128
 
        response = self.client.get('/')
129
 
        self.assertTemplateExistsAndContains(
130
 
            response.content, '#add-node',
131
 
            'select#id_after_commissioning_action')
132
 
 
133
 
    def test_power_type_does_not_exist_if_not_admin(self):
134
 
        self.client_log_in()
135
 
        response = self.client.get('/')
136
 
        self.assertTemplateExistsAndDoesNotContain(
137
 
            response.content, '#add-node',
138
 
            'select#id_power_type')
139
 
 
140
 
    def test_power_type_exists_if_admin(self):
141
 
        self.client_log_in(as_admin=True)
142
 
        response = self.client.get('/')
143
 
        self.assertTemplateExistsAndContains(
144
 
            response.content, '#add-node',
145
 
            'select#id_power_type')
146
 
 
147
 
    def test_zone_does_not_exist_if_not_admin(self):
148
 
        self.client_log_in()
149
 
        response = self.client.get('/')
150
 
        self.assertTemplateExistsAndDoesNotContain(
151
 
            response.content, '#add-node',
152
 
            'select#id_zone')
153
 
 
154
 
    def test_zone_exists_if_admin(self):
155
 
        self.client_log_in(as_admin=True)
156
 
        response = self.client.get('/')
157
 
        self.assertTemplateExistsAndContains(
158
 
            response.content, '#add-node',
159
 
            'select#id_zone')
160
 
 
161
 
 
162
 
class FakeDeletableModel:
163
 
    """A fake model class, with a delete method."""
164
 
 
165
 
    class Meta:
166
 
        app_label = 'maasserver'
167
 
        object_name = 'fake'
168
 
        verbose_name = "fake object"
169
 
 
170
 
    _meta = Meta
171
 
    deleted = False
172
 
 
173
 
    def delete(self):
174
 
        self.deleted = True
175
 
 
176
 
 
177
 
class FakeDeleteView(HelpfulDeleteView):
178
 
    """A fake `HelpfulDeleteView` instance.  Goes through most of the motions.
179
 
 
180
 
    There are a few special features to help testing along:
181
 
     - If there's no object, get_object() raises Http404.
182
 
     - Info messages are captured in self.notices.
183
 
    """
184
 
 
185
 
    model = FakeDeletableModel
186
 
 
187
 
    template_name = 'not-a-real-template'
188
 
 
189
 
    def __init__(self, obj=None, next_url=None, request=None):
190
 
        self.obj = obj
191
 
        self.next_url = next_url
192
 
        self.request = request
193
 
        self.notices = []
194
 
 
195
 
    def get_object(self):
196
 
        if self.obj is None:
197
 
            raise Http404()
198
 
        else:
199
 
            return self.obj
200
 
 
201
 
    def get_next_url(self):
202
 
        return self.next_url
203
 
 
204
 
    def raise_permission_denied(self):
205
 
        """Helper to substitute for get_object."""
206
 
        raise PermissionDenied()
207
 
 
208
 
    def show_notice(self, notice):
209
 
        self.notices.append(notice)
210
 
 
211
 
 
212
 
class HelpfulDeleteViewTest(MAASServerTestCase):
213
 
 
214
 
    def test_delete_deletes_object(self):
215
 
        obj = FakeDeletableModel()
216
 
        # HttpResponseRedirect does not allow next_url to be None.
217
 
        view = FakeDeleteView(obj, next_url=factory.getRandomString())
218
 
        view.delete()
219
 
        self.assertTrue(obj.deleted)
220
 
        self.assertEqual([view.compose_feedback_deleted(obj)], view.notices)
221
 
 
222
 
    def test_delete_is_gentle_with_missing_objects(self):
223
 
        # Deleting a nonexistent object is basically treated as successful.
224
 
        # HttpResponseRedirect does not allow next_url to be None.
225
 
        view = FakeDeleteView(next_url=factory.getRandomString())
226
 
        response = view.delete()
227
 
        self.assertEqual(httplib.FOUND, response.status_code)
228
 
        self.assertEqual([view.compose_feedback_nonexistent()], view.notices)
229
 
 
230
 
    def test_delete_is_not_gentle_with_permission_violations(self):
231
 
        view = FakeDeleteView()
232
 
        view.get_object = view.raise_permission_denied
233
 
        self.assertRaises(PermissionDenied, view.delete)
234
 
 
235
 
    def test_get_asks_for_confirmation_and_does_nothing_yet(self):
236
 
        obj = FakeDeletableModel()
237
 
        next_url = factory.getRandomString()
238
 
        request = RequestFactory().get('/foo')
239
 
        view = FakeDeleteView(obj, request=request, next_url=next_url)
240
 
        response = view.get(request)
241
 
        self.assertEqual(httplib.OK, response.status_code)
242
 
        self.assertNotIn(next_url, response.get('Location', ''))
243
 
        self.assertFalse(obj.deleted)
244
 
        self.assertEqual([], view.notices)
245
 
 
246
 
    def test_get_skips_confirmation_for_missing_objects(self):
247
 
        next_url = factory.getRandomString()
248
 
        request = RequestFactory().get('/foo')
249
 
        view = FakeDeleteView(next_url=next_url, request=request)
250
 
        response = view.get(request)
251
 
        self.assertEqual(next_url, extract_redirect(response))
252
 
        self.assertEqual([view.compose_feedback_nonexistent()], view.notices)
253
 
 
254
 
    def test_compose_feedback_nonexistent_names_class(self):
255
 
        class_name = factory.getRandomString()
256
 
        self.patch(FakeDeletableModel.Meta, 'verbose_name', class_name)
257
 
        view = FakeDeleteView()
258
 
        self.assertEqual(
259
 
            "Not deleting: %s not found." % class_name,
260
 
            view.compose_feedback_nonexistent())
261
 
 
262
 
    def test_compose_feedback_deleted_uses_name_object(self):
263
 
        object_name = factory.getRandomString()
264
 
        view = FakeDeleteView(FakeDeletableModel())
265
 
        view.name_object = lambda _obj: object_name
266
 
        self.assertEqual(
267
 
            "%s deleted." % object_name.capitalize(),
268
 
            view.compose_feedback_deleted(view.obj))
269
 
 
270
 
 
271
 
class SimpleFakeModel:
272
 
    """Pretend model object for testing"""
273
 
 
274
 
    def __init__(self, counter):
275
 
        self.id = counter
276
 
 
277
 
 
278
 
class SimpleListView(PaginatedListView):
279
 
    """Simple paginated view for testing"""
280
 
 
281
 
    paginate_by = 2
282
 
    query_results = None
283
 
 
284
 
    def __init__(self, query_results):
285
 
        self.query_results = list(query_results)
286
 
 
287
 
    def get_queryset(self):
288
 
        """Return precanned list of objects
289
 
 
290
 
        Really this should return a QuerySet object, but for basic usage a
291
 
        list is close enough.
292
 
        """
293
 
        return self.query_results
294
 
 
295
 
 
296
 
class PaginatedListViewTests(MAASServerTestCase):
297
 
    """Check PaginatedListView page links inserted into context are correct"""
298
 
 
299
 
    def test_single_page(self):
300
 
        view = SimpleListView.as_view(query_results=[SimpleFakeModel(1)])
301
 
        request = RequestFactory().get('/index')
302
 
        response = view(request)
303
 
        context = response.context_data
304
 
        self.assertEqual("", context["first_page_link"])
305
 
        self.assertEqual("", context["previous_page_link"])
306
 
        self.assertEqual("", context["next_page_link"])
307
 
        self.assertEqual("", context["last_page_link"])
308
 
 
309
 
    def test_on_first_page(self):
310
 
        view = SimpleListView.as_view(
311
 
            query_results=[SimpleFakeModel(i) for i in range(5)])
312
 
        request = RequestFactory().get('/index')
313
 
        response = view(request)
314
 
        context = response.context_data
315
 
        self.assertEqual("", context["first_page_link"])
316
 
        self.assertEqual("", context["previous_page_link"])
317
 
        self.assertEqual("?page=2", context["next_page_link"])
318
 
        self.assertEqual("?page=3", context["last_page_link"])
319
 
 
320
 
    def test_on_second_page(self):
321
 
        view = SimpleListView.as_view(
322
 
            query_results=[SimpleFakeModel(i) for i in range(7)])
323
 
        request = RequestFactory().get('/index?page=2')
324
 
        response = view(request)
325
 
        context = response.context_data
326
 
        self.assertEqual("index", context["first_page_link"])
327
 
        self.assertEqual("index", context["previous_page_link"])
328
 
        self.assertEqual("?page=3", context["next_page_link"])
329
 
        self.assertEqual("?page=4", context["last_page_link"])
330
 
 
331
 
    def test_on_final_page(self):
332
 
        view = SimpleListView.as_view(
333
 
            query_results=[SimpleFakeModel(i) for i in range(5)])
334
 
        request = RequestFactory().get('/index?page=3')
335
 
        response = view(request)
336
 
        context = response.context_data
337
 
        self.assertEqual("index", context["first_page_link"])
338
 
        self.assertEqual("?page=2", context["previous_page_link"])
339
 
        self.assertEqual("", context["next_page_link"])
340
 
        self.assertEqual("", context["last_page_link"])
341
 
 
342
 
    def test_relative_to_directory(self):
343
 
        view = SimpleListView.as_view(
344
 
            query_results=[SimpleFakeModel(i) for i in range(6)])
345
 
        request = RequestFactory().get('/index/?page=2')
346
 
        response = view(request)
347
 
        context = response.context_data
348
 
        self.assertEqual(".", context["first_page_link"])
349
 
        self.assertEqual(".", context["previous_page_link"])
350
 
        self.assertEqual("?page=3", context["next_page_link"])
351
 
        self.assertEqual("?page=3", context["last_page_link"])
352
 
 
353
 
    def test_preserves_query_string(self):
354
 
        view = SimpleListView.as_view(
355
 
            query_results=[SimpleFakeModel(i) for i in range(6)])
356
 
        request = RequestFactory().get('/index?lookup=value')
357
 
        response = view(request)
358
 
        context = response.context_data
359
 
        self.assertEqual("", context["first_page_link"])
360
 
        self.assertEqual("", context["previous_page_link"])
361
 
        # Does this depend on dict hash values for order or does django sort?
362
 
        self.assertEqual("?lookup=value&page=2", context["next_page_link"])
363
 
        self.assertEqual("?lookup=value&page=3", context["last_page_link"])
364
 
 
365
 
    def test_preserves_query_string_with_page(self):
366
 
        view = SimpleListView.as_view(
367
 
            query_results=[SimpleFakeModel(i) for i in range(8)])
368
 
        request = RequestFactory().get('/index?page=3&lookup=value')
369
 
        response = view(request)
370
 
        context = response.context_data
371
 
        self.assertEqual("?lookup=value", context["first_page_link"])
372
 
        # Does this depend on dict hash values for order or does django sort?
373
 
        self.assertEqual("?lookup=value&page=2", context["previous_page_link"])
374
 
        self.assertEqual("?lookup=value&page=4", context["next_page_link"])
375
 
        self.assertEqual("?lookup=value&page=4", context["last_page_link"])
376
 
 
377
 
 
378
 
class MAASExceptionHandledInView(MAASServerTestCase):
379
 
 
380
 
    def test_raised_MAASException_redirects(self):
381
 
        # When a ExternalComponentException is raised in a POST request, the
382
 
        # response is a redirect to the same page.
383
 
        self.client_log_in()
384
 
 
385
 
        # Patch NodeEdit to error on post.
386
 
        def post(self, request, *args, **kwargs):
387
 
            raise ExternalComponentException()
388
 
        self.patch(NodeEdit, 'post', post)
389
 
        node = factory.make_node(owner=self.logged_in_user)
390
 
        node_edit_link = reverse('node-edit', args=[node.system_id])
391
 
        response = self.client.post(node_edit_link, {})
392
 
        self.assertEqual(node_edit_link, extract_redirect(response))
393
 
 
394
 
    def test_raised_ExternalComponentException_publishes_message(self):
395
 
        # When a ExternalComponentException is raised in a POST request, a
396
 
        # message is published with the error message.
397
 
        self.client_log_in()
398
 
        error_message = factory.getRandomString()
399
 
 
400
 
        # Patch NodeEdit to error on post.
401
 
        def post(self, request, *args, **kwargs):
402
 
            raise ExternalComponentException(error_message)
403
 
        self.patch(NodeEdit, 'post', post)
404
 
        node = factory.make_node(owner=self.logged_in_user)
405
 
        node_edit_link = reverse('node-edit', args=[node.system_id])
406
 
        self.client.post(node_edit_link, {})
407
 
        # Manually perform the redirect: i.e. get the same page.
408
 
        response = self.client.get(node_edit_link, {})
409
 
        self.assertEqual(
410
 
            [error_message],
411
 
            [message.message for message in response.context['messages']])
412
 
 
413
 
 
414
 
class PermanentErrorDisplayTest(MAASServerTestCase):
415
 
 
416
 
    def test_permanent_error_displayed(self):
417
 
        self.client_log_in()
418
 
        fault_codes = [
419
 
            randint(1, 100),
420
 
            randint(101, 200),
421
 
            ]
422
 
        errors = []
423
 
        for fault in fault_codes:
424
 
            # Create component with getRandomString to be sure
425
 
            # to display all the errors.
426
 
            component = factory.make_name('component')
427
 
            error_message = factory.make_name('error')
428
 
            error = Fault(fault, error_message)
429
 
            errors.append(error)
430
 
            register_persistent_error(component, error_message)
431
 
        links = [
432
 
            reverse('index'),
433
 
            reverse('node-list'),
434
 
            reverse('prefs'),
435
 
        ]
436
 
        for link in links:
437
 
            response = self.client.get(link)
438
 
            self.assertThat(
439
 
                response.content,
440
 
                ContainsAll(
441
 
                    [escape(error.faultString) for error in errors]))