1
# Copyright 2013-2014 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Test maasserver zones views."""
6
from __future__ import (
19
from urllib import urlencode
21
from django.core.exceptions import ValidationError
22
from django.core.urlresolvers import reverse
23
from lxml.html import fromstring
24
from maasserver.models import Zone
25
from maasserver.models.zone import DEFAULT_ZONE_NAME
26
from maasserver.testing import (
31
from maasserver.testing.factory import factory
32
from maasserver.testing.testcase import MAASServerTestCase
33
from maasserver.views.zones import ZoneAdd
34
from testtools.matchers import (
43
class ZoneListingViewTest(MAASServerTestCase):
45
def test_zone_list_link_present_on_homepage(self):
47
response = self.client.get(reverse('index'))
48
zone_list_link = reverse('zone-list')
51
get_content_links(response, element='#main-nav'))
53
def test_zone_list_displays_zone_details(self):
54
# Zone listing displays the zone name and the zone description.
56
[factory.make_zone() for i in range(3)]
57
zones = Zone.objects.all()
58
response = self.client.get(reverse('zone-list'))
59
zone_names = [zone.name for zone in zones]
60
truncated_zone_descriptions = [
61
zone.description[:20] for zone in zones]
62
self.assertThat(response.content, ContainsAll(zone_names))
64
response.content, ContainsAll(truncated_zone_descriptions))
66
def test_zone_list_displays_sorted_list_of_zones(self):
67
# Zones are alphabetically sorted on the zone list page.
69
[factory.make_zone() for i in range(3)]
70
zones = Zone.objects.all()
71
sorted_zones = sorted(zones, key=lambda x: x.name.lower())
72
response = self.client.get(reverse('zone-list'))
74
reverse('zone-view', args=[zone.name])
75
for zone in sorted_zones]
78
[link for link in get_content_links(response)
79
if link.startswith('/zones/')])
81
def test_zone_list_displays_links_to_zone_node(self):
83
[factory.make_zone() for i in range(3)]
84
zones = Zone.objects.all()
85
sorted_zones = sorted(zones, key=lambda x: x.name.lower())
86
response = self.client.get(reverse('zone-list'))
88
reverse('node-list') + "?" +
89
urlencode({'query': 'zone=%s' % zone.name})
90
for zone in sorted_zones]
93
[link for link in get_content_links(response)
94
if link.startswith('/nodes/')])
97
class ZoneListingViewTestNonAdmin(MAASServerTestCase):
99
def test_zone_list_does_not_contain_edit_and_delete_links(self):
101
zones = [factory.make_zone() for i in range(3)]
102
response = self.client.get(reverse('zone-list'))
104
reverse('zone-edit', args=[zone.name]) for zone in zones]
105
zone_delete_links = [
106
reverse('zone-del', args=[zone.name]) for zone in zones]
107
all_links = get_content_links(response)
110
MatchesAll(*[Not(Equals(link)) for link in zone_edit_links]))
113
MatchesAll(*[Not(Equals(link)) for link in zone_delete_links]))
115
def test_zone_list_does_not_contain_add_link(self):
117
response = self.client.get(reverse('zone-list'))
118
add_link = reverse('zone-add')
119
self.assertNotIn(add_link, get_content_links(response))
122
class ZoneListingViewTestAdmin(MAASServerTestCase):
124
def test_zone_list_contains_edit_links(self):
125
self.client_log_in(as_admin=True)
126
zones = [factory.make_zone() for i in range(3)]
127
default_zone = Zone.objects.get_default_zone()
129
reverse('zone-edit', args=[zone.name]) for zone in zones]
130
zone_delete_links = [
131
reverse('zone-del', args=[zone.name]) for zone in zones]
132
zone_default_edit = reverse('zone-edit', args=[default_zone])
133
zone_default_delete = reverse('zone-del', args=[default_zone])
135
response = self.client.get(reverse('zone-list'))
136
all_links = get_content_links(response)
138
self.assertThat(all_links, ContainsAll(
139
zone_edit_links + zone_delete_links))
140
self.assertThat(all_links, Not(Contains(zone_default_edit)))
141
self.assertThat(all_links, Not(Contains(zone_default_delete)))
143
def test_zone_list_contains_add_link(self):
144
self.client_log_in(as_admin=True)
145
response = self.client.get(reverse('zone-list'))
146
add_link = reverse('zone-add')
147
self.assertIn(add_link, get_content_links(response))
150
class ZoneAddTestNonAdmin(MAASServerTestCase):
152
def test_cannot_add_zone(self):
154
name = factory.make_name('zone')
155
response = self.client.post(reverse('zone-add'), {'name': name})
156
# This returns an inappropriate response (302 FOUND, redirect to the
157
# login page; should be 403 FORBIDDEN) but does not actually create the
158
# zone, and that's the main thing.
159
self.assertEqual(reverse('login'), extract_redirect(response))
160
self.assertEqual([], list(Zone.objects.filter(name=name)))
163
class ZoneAddTestAdmin(MAASServerTestCase):
165
def test_adds_zone(self):
166
self.client_log_in(as_admin=True)
168
'name': factory.make_name('zone'),
169
'description': factory.getRandomString(),
171
response = self.client.post(reverse('zone-add'), definition)
172
self.assertEqual(httplib.FOUND, response.status_code)
173
zone = Zone.objects.get(name=definition['name'])
174
self.assertEqual(definition['description'], zone.description)
175
self.assertEqual(reverse('zone-list'), extract_redirect(response))
177
def test_description_is_optional(self):
178
self.client_log_in(as_admin=True)
179
name = factory.make_name('zone')
180
response = self.client.post(reverse('zone-add'), {'name': name})
181
self.assertEqual(httplib.FOUND, response.status_code)
182
zone = Zone.objects.get(name=name)
183
self.assertEqual('', zone.description)
185
def test_get_success_url_returns_valid_url(self):
186
self.client_log_in(as_admin=True)
187
url = ZoneAdd().get_success_url()
188
self.assertIn("/zones", url)
191
class ZoneDetailViewTest(MAASServerTestCase):
193
def test_zone_detail_displays_zone_detail(self):
194
# The Zone detail view displays the zone name and the zone
197
zone = factory.make_zone()
198
response = self.client.get(reverse('zone-view', args=[zone.name]))
199
self.assertThat(response.content, Contains(zone.name))
201
response.content, Contains(zone.description))
203
def test_zone_detail_displays_node_count(self):
205
zone = factory.make_zone()
206
node = factory.make_node()
208
response = self.client.get(reverse('zone-view', args=[zone.name]))
209
document = fromstring(response.content)
210
count_text = document.get_element_by_id("#nodecount").text_content()
212
count_text, Contains(unicode(zone.node_set.count())))
214
def test_zone_detail_links_to_node_list(self):
216
zone = factory.make_zone()
217
node = factory.make_node()
219
response = self.client.get(reverse('zone-view', args=[zone.name]))
221
reverse('node-list') + "?" +
222
urlencode({'query': 'zone=%s' % zone.name}))
223
all_links = get_content_links(response)
224
self.assertIn(zone_node_link, all_links)
227
class ZoneDetailViewNonAdmin(MAASServerTestCase):
229
def test_zone_detail_does_not_contain_edit_link(self):
231
zone = factory.make_zone()
232
response = self.client.get(reverse('zone-view', args=[zone.name]))
233
zone_edit_link = reverse('zone-edit', args=[zone.name])
234
self.assertNotIn(zone_edit_link, get_content_links(response))
236
def test_zone_detail_does_not_contain_delete_link(self):
238
zone = factory.make_zone()
239
response = self.client.get(reverse('zone-view', args=[zone.name]))
240
zone_delete_link = reverse('zone-del', args=[zone.name])
241
self.assertNotIn(zone_delete_link, get_content_links(response))
244
class ZoneDetailViewAdmin(MAASServerTestCase):
246
def test_zone_detail_contains_edit_link(self):
247
self.client_log_in(as_admin=True)
248
zone = factory.make_zone()
249
response = self.client.get(reverse('zone-view', args=[zone.name]))
250
zone_edit_link = reverse('zone-edit', args=[zone.name])
251
self.assertIn(zone_edit_link, get_content_links(response))
253
def test_zone_detail_contains_delete_link(self):
254
self.client_log_in(as_admin=True)
255
zone = factory.make_zone()
256
response = self.client.get(reverse('zone-view', args=[zone.name]))
257
zone_delete_link = reverse('zone-del', args=[zone.name])
258
self.assertIn(zone_delete_link, get_content_links(response))
260
def test_zone_detail_for_default_zone_does_not_contain_delete_link(self):
261
self.client_log_in(as_admin=True)
262
response = self.client.get(
263
reverse('zone-view', args=[DEFAULT_ZONE_NAME]))
264
zone_delete_link = reverse('zone-del', args=[DEFAULT_ZONE_NAME])
265
self.assertNotIn(zone_delete_link, get_content_links(response))
268
class ZoneEditNonAdminTest(MAASServerTestCase):
270
def test_cannot_access_zone_edit(self):
272
zone = factory.make_zone()
273
response = self.client.post(reverse('zone-edit', args=[zone.name]))
274
self.assertEqual(reverse('login'), extract_redirect(response))
277
class ZoneEditAdminTest(MAASServerTestCase):
279
def test_zone_edit(self):
280
self.client_log_in(as_admin=True)
281
zone = factory.make_zone()
282
new_name = factory.make_name('name')
283
new_description = factory.make_name('description')
284
response = self.client.post(
285
reverse('zone-edit', args=[zone.name]),
288
'description': new_description,
291
reverse('zone-list'), extract_redirect(response),
293
zone = reload_object(zone)
295
(new_name, new_description),
296
(zone.name, zone.description),
300
class ZoneDeleteNonAdminTest(MAASServerTestCase):
302
def test_cannot_delete(self):
304
zone = factory.make_zone()
305
response = self.client.post(reverse('zone-del', args=[zone.name]))
306
self.assertEqual(reverse('login'), extract_redirect(response))
307
self.assertIsNotNone(reload_object(zone))
310
class ZoneDeleteAdminTest(MAASServerTestCase):
312
def test_deletes_zone(self):
313
self.client_log_in(as_admin=True)
314
zone = factory.make_zone()
315
response = self.client.post(
316
reverse('zone-del', args=[zone.name]),
318
self.assertEqual(httplib.FOUND, response.status_code)
319
self.assertIsNone(reload_object(zone))
321
def test_rejects_deletion_of_default_zone(self):
322
self.client_log_in(as_admin=True)
325
reverse('zone-del', args=[DEFAULT_ZONE_NAME]),
327
except ValidationError:
328
# Right now, this generates an error because the deletion
329
# is prevented in the model code and not at the form level.
330
# This is not so bad because we make sure that the deletion link
331
# for the default zone isn't shown anywhere.
332
# If we move validation to the form level, this exception goes
333
# away and we'll have to check the HTTP response for a validation
337
# The default zone is still there.
338
self.assertIsNotNone(Zone.objects.get_default_zone())
340
def test_redirects_to_listing(self):
341
self.client_log_in(as_admin=True)
342
zone = factory.make_zone()
343
response = self.client.post(
344
reverse('zone-del', args=[zone.name]),
346
self.assertEqual(reverse('zone-list'), extract_redirect(response))
348
def test_does_not_delete_nodes(self):
349
self.client_log_in(as_admin=True)
350
zone = factory.make_zone()
351
node = factory.make_node(zone=zone)
352
response = self.client.post(
353
reverse('zone-del', args=[zone.name]),
355
self.assertEqual(httplib.FOUND, response.status_code)
356
self.assertIsNone(reload_object(zone))
357
node = reload_object(node)
358
self.assertIsNotNone(node)
359
self.assertEqual(Zone.objects.get_default_zone(), node.zone)