1
# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Test maasserver API."""
6
from __future__ import (
18
from random import randint
19
from xmlrpclib import Fault
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 (
37
from maasserver.views.nodes import NodeEdit
38
from testtools.matchers import ContainsAll
41
class Test404500(MAASServerTestCase):
42
"""Test pages displayed when an error 404 or an error 500 occur."""
46
response = self.client.get('/no-found-page/')
47
doc = fromstring(response.content)
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 '
54
[elem.text.strip() for elem in
59
from maasserver.urls import urlpatterns
60
urlpatterns += patterns(
62
(r'^500/$', 'django.views.defaults.server_error'),
64
response = self.client.get('/500/')
65
doc = fromstring(response.content)
67
"Internal server error",
68
doc.cssselect('title')[0].text)
69
self.assertSequenceEqual(
70
['Internal server error.'],
71
[elem.text.strip() for elem in
75
class TestSnippets(MAASServerTestCase):
77
def _assertTemplateExistsAndContains(self, content, template_selector,
78
contains_selector, reverse=False):
79
doc = fromstring(content)
80
snippets = doc.cssselect(template_selector)
84
"The snippet '%s' does not exist." % template_selector)
85
# It contains the required element.
86
selects = fromstring(snippets[0].text).cssselect(contains_selector)
90
"The element '%s' does exist." % contains_selector)
94
"The element '%s' does not exist." % contains_selector,)
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'.
102
self._assertTemplateExistsAndContains(
103
content, template_selector, contains_selector)
105
def assertTemplateExistsAndDoesNotContain(self, content, template_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'.
111
self._assertTemplateExistsAndContains(
112
content, template_selector, contains_selector, reverse=True)
114
def test_architecture_snippet(self):
116
response = self.client.get('/')
117
self.assertTemplateExistsAndContains(
118
response.content, '#add-node', 'select#id_architecture')
120
def test_hostname(self):
122
response = self.client.get('/')
123
self.assertTemplateExistsAndContains(
124
response.content, '#add-node', 'input#id_hostname')
126
def test_after_commissioning_action_snippet(self):
128
response = self.client.get('/')
129
self.assertTemplateExistsAndContains(
130
response.content, '#add-node',
131
'select#id_after_commissioning_action')
133
def test_power_type_does_not_exist_if_not_admin(self):
135
response = self.client.get('/')
136
self.assertTemplateExistsAndDoesNotContain(
137
response.content, '#add-node',
138
'select#id_power_type')
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')
147
def test_zone_does_not_exist_if_not_admin(self):
149
response = self.client.get('/')
150
self.assertTemplateExistsAndDoesNotContain(
151
response.content, '#add-node',
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',
162
class FakeDeletableModel:
163
"""A fake model class, with a delete method."""
166
app_label = 'maasserver'
168
verbose_name = "fake object"
177
class FakeDeleteView(HelpfulDeleteView):
178
"""A fake `HelpfulDeleteView` instance. Goes through most of the motions.
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.
185
model = FakeDeletableModel
187
template_name = 'not-a-real-template'
189
def __init__(self, obj=None, next_url=None, request=None):
191
self.next_url = next_url
192
self.request = request
195
def get_object(self):
201
def get_next_url(self):
204
def raise_permission_denied(self):
205
"""Helper to substitute for get_object."""
206
raise PermissionDenied()
208
def show_notice(self, notice):
209
self.notices.append(notice)
212
class HelpfulDeleteViewTest(MAASServerTestCase):
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())
219
self.assertTrue(obj.deleted)
220
self.assertEqual([view.compose_feedback_deleted(obj)], view.notices)
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)
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)
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)
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)
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()
259
"Not deleting: %s not found." % class_name,
260
view.compose_feedback_nonexistent())
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
267
"%s deleted." % object_name.capitalize(),
268
view.compose_feedback_deleted(view.obj))
271
class SimpleFakeModel:
272
"""Pretend model object for testing"""
274
def __init__(self, counter):
278
class SimpleListView(PaginatedListView):
279
"""Simple paginated view for testing"""
284
def __init__(self, query_results):
285
self.query_results = list(query_results)
287
def get_queryset(self):
288
"""Return precanned list of objects
290
Really this should return a QuerySet object, but for basic usage a
291
list is close enough.
293
return self.query_results
296
class PaginatedListViewTests(MAASServerTestCase):
297
"""Check PaginatedListView page links inserted into context are correct"""
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"])
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"])
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"])
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"])
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"])
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"])
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"])
378
class MAASExceptionHandledInView(MAASServerTestCase):
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.
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))
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.
398
error_message = factory.getRandomString()
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, {})
411
[message.message for message in response.context['messages']])
414
class PermanentErrorDisplayTest(MAASServerTestCase):
416
def test_permanent_error_displayed(self):
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)
430
register_persistent_error(component, error_message)
433
reverse('node-list'),
437
response = self.client.get(link)
441
[escape(error.faultString) for error in errors]))