← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/maas/bug-989384 into lp:maas

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/bug-989384 into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #989384 in MAAS: "Test helper: extract path from redirect response"
  https://bugs.launchpad.net/maas/+bug/989384

For more details, see:
https://code.launchpad.net/~jtv/maas/bug-989384/+merge/103866

This will save us some typing (and cargo-culting, not to mention reading time) from tests that need to check out redirects (HttpResponses with status code httplib.FOUND) and see where they redirect to.

Note how the way I put that asserts that the redirect is really a response.  If that's what it's supposed to be, just use this function with confidence.  But if it's not a redirect, you'll get a reasonably helpful message about what was in the response.  No more need to check its status code in your test, let alone fire up a debugger to get at the response body!


-- 
https://code.launchpad.net/~jtv/maas/bug-989384/+merge/103866
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/maas/bug-989384 into lp:maas.
=== modified file 'src/maasserver/testing/__init__.py'
--- src/maasserver/testing/__init__.py	2012-04-23 11:34:19 +0000
+++ src/maasserver/testing/__init__.py	2012-04-27 12:45:26 +0000
@@ -11,6 +11,7 @@
 
 __metaclass__ = type
 __all__ = [
+    "extract_redirect",
     "get_content_links",
     "get_data",
     "get_fake_provisioning_api_proxy",
@@ -19,7 +20,9 @@
     "reload_objects",
     ]
 
+import httplib
 import os
+from urlparse import urlparse
 from uuid import uuid1
 
 from lxml.html import fromstring
@@ -59,6 +62,29 @@
     fake_provisioning_proxy = None
 
 
+def extract_redirect(http_response):
+    """Extract redirect target from an http response object.
+
+    Only the http path part of the redirect is ignored; protocol and host
+    name, if present, are not included in the result.
+
+    If the response is not a redirect, this raises :class:`ValueError` with
+    a descriptive error message.
+
+    :param http_response: A response returned from an http request.
+    :type http_response: :class:`HttpResponse`
+    :return: The "path" part of the target that `http_response` redirects to.
+    :raises: ValueError
+    """
+    if http_response.status_code != httplib.FOUND:
+        raise ValueError(
+            "Not a redirect: http status %d.  Content: %s"
+            % (http_response.status_code, http_response.content[:80]))
+    target_url = http_response['Location']
+    parsed_url = urlparse(target_url)
+    return parsed_url.path
+
+
 def reload_object(model_object):
     """Reload `obj` from the database.
 

=== modified file 'src/maasserver/testing/tests/test_module.py'
--- src/maasserver/testing/tests/test_module.py	2012-04-16 10:00:51 +0000
+++ src/maasserver/testing/tests/test_module.py	2012-04-27 12:45:26 +0000
@@ -12,11 +12,19 @@
 __metaclass__ = type
 __all__ = []
 
+import httplib
+
+from django.http import (
+    HttpResponse,
+    HttpResponseRedirect,
+    )
 from maasserver import provisioning
 from maasserver.testing import (
+    extract_redirect,
     reload_object,
     reload_objects,
     )
+from maasserver.testing.factory import factory
 from maasserver.testing.models import TestModel
 from maasserver.testing.testcase import (
     TestCase,
@@ -76,6 +84,29 @@
 
     app = 'maasserver.testing'
 
+    def test_extract_redirect_extracts_redirect_location(self):
+        url = factory.getRandomString()
+        self.assertEqual(
+            url, extract_redirect(HttpResponseRedirect(url)))
+
+    def test_extract_redirect_only_returns_target_path(self):
+        url_path = factory.getRandomString()
+        self.assertEqual(
+            "/%s" % url_path,
+            extract_redirect(
+                HttpResponseRedirect("http://example.com/%s"; % url_path)))
+
+    def test_extract_redirect_errors_out_helpfully_if_not_a_redirect(self):
+        content = factory.getRandomString(10)
+        other_response = HttpResponse(status=httplib.OK, content=content)
+        try:
+            extract_redirect(other_response)
+        except ValueError as e:
+            pass
+
+        self.assertIn(unicode(httplib.OK), unicode(e))
+        self.assertIn(content, unicode(e))
+
     def test_reload_object_reloads_object(self):
         test_obj = TestModel(text="old text")
         test_obj.save()

=== modified file 'src/maasserver/tests/test_exceptions.py'
--- src/maasserver/tests/test_exceptions.py	2012-04-25 16:19:07 +0000
+++ src/maasserver/tests/test_exceptions.py	2012-04-27 12:45:26 +0000
@@ -18,6 +18,7 @@
     MAASAPIBadRequest,
     Redirect,
     )
+from maasserver.testing import extract_redirect
 from maastesting.factory import factory
 from maastesting.testcase import TestCase
 
@@ -36,5 +37,4 @@
         target = factory.getRandomString()
         exception = Redirect(target)
         response = exception.make_http_response()
-        self.assertEqual(httplib.FOUND, response.status_code)
-        self.assertEqual(target, response['Location'])
+        self.assertEqual(target, extract_redirect(response))

=== modified file 'src/maasserver/tests/test_forms.py'
--- src/maasserver/tests/test_forms.py	2012-04-27 10:56:37 +0000
+++ src/maasserver/tests/test_forms.py	2012-04-27 12:45:26 +0000
@@ -42,10 +42,15 @@
     DEFAULT_CONFIG,
     MACAddress,
     )
+<<<<<<< TREE
 from maasserver.node_action import (
     AcceptAndCommission,
     Delete,
     )
+=======
+from maasserver.provisioning import get_provisioning_api_proxy
+from maasserver.testing import extract_redirect
+>>>>>>> MERGE-SOURCE
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import TestCase
 from provisioningserver.enum import POWER_TYPE_CHOICES
@@ -243,6 +248,104 @@
 
 class TestNodeActionForm(TestCase):
 
+<<<<<<< TREE
+=======
+    def test_inhibit_delete_allows_deletion_of_unowned_node(self):
+        admin = factory.make_admin()
+        node = factory.make_node(status=NODE_STATUS.DECLARED)
+        self.assertIsNone(inhibit_delete(node, admin))
+
+    def test_inhibit_delete_inhibits_deletion_of_owned_node(self):
+        admin = factory.make_admin()
+        node = factory.make_node(
+            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
+        self.assertEqual(
+            "You cannot delete this node because it's in use.",
+            inhibit_delete(node, admin))
+
+    def test_delete_node_redirects_to_confirmation_page(self):
+        admin = factory.make_admin()
+        node = factory.make_node(status=NODE_STATUS.DECLARED)
+        try:
+            delete_node(node, admin)
+        except MAASAPIException as e:
+            pass
+        else:
+            self.fail("delete_node should have raised a Redirect.")
+        response = e.make_http_response()
+        self.assertEqual(
+            reverse('node-delete', args=[node.system_id]),
+            extract_redirect(response))
+
+    def have_permission_returns_False_if_action_is_None(self):
+        admin = factory.make_admin()
+        node = factory.make_node(status=NODE_STATUS.DECLARED)
+        self.assertFalse(get_action_form(admin)(node).have_permission(None))
+
+    def have_permission_returns_False_if_lacking_permission(self):
+        user = factory.make_user()
+        node = factory.make_node(
+            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
+        action = {
+            'permission': NODE_PERMISSION.EDIT,
+        }
+        self.assertFalse(get_action_form(user)(node).have_permission(action))
+
+    def have_permission_returns_True_given_permission(self):
+        node = factory.make_node(
+            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
+        action = {
+            'permission': NODE_PERMISSION.EDIT,
+        }
+        form = get_action_form(node.owner)(node)
+        self.assertTrue(form.have_permission(action))
+
+    def test_compile_action_records_inhibition(self):
+        node = factory.make_node()
+        user = factory.make_user()
+        form = get_action_form(user)(node)
+        inhibition = factory.getRandomString()
+
+        def inhibit(*args, **kwargs):
+            return inhibition
+
+        compiled_action = form.compile_action({'inhibit': inhibit})
+        self.assertEqual(inhibition, compiled_action['inhibition'])
+
+    def test_compile_action_records_None_if_no_inhibition(self):
+        node = factory.make_node()
+        user = factory.make_user()
+        form = get_action_form(user)(node)
+
+        def inhibit(*args, **kwargs):
+            return None
+
+        compiled_action = form.compile_action({'inhibit': inhibit})
+        self.assertIsNone(compiled_action['inhibition'])
+
+    def test_compile_actions_for_declared_node_admin(self):
+        # Check which transitions are available for an admin on a
+        # 'Declared' node.
+        admin = factory.make_admin()
+        node = factory.make_node(status=NODE_STATUS.DECLARED)
+        form = get_action_form(admin)(node)
+        actions = form.compile_actions()
+        self.assertItemsEqual(
+            ["Accept & commission", "Delete node"],
+            [action['display'] for action in actions])
+        # All permissions should be ADMIN.
+        self.assertEqual(
+            [NODE_PERMISSION.ADMIN] * len(actions),
+            [action['permission'] for actions in actions])
+
+    def test_compile_actions_for_declared_node_simple_user(self):
+        # A simple user sees no actions for a 'Declared' node.
+        user = factory.make_user()
+        node = factory.make_node(status=NODE_STATUS.DECLARED)
+        form = get_action_form(user)(node)
+        self.assertItemsEqual([], form.compile_actions())
+
+>>>>>>> MERGE-SOURCE
     def test_get_action_form_creates_form_class_with_attributes(self):
         user = factory.make_admin()
         form_class = get_action_form(user)

=== modified file 'src/maasserver/tests/test_middleware.py'
--- src/maasserver/tests/test_middleware.py	2012-04-20 12:54:26 +0000
+++ src/maasserver/tests/test_middleware.py	2012-04-27 12:45:26 +0000
@@ -44,6 +44,7 @@
     ExternalComponentsMiddleware,
     PROFILES_CHECK_DONE_KEY,
     )
+from maasserver.testing import extract_redirect
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import (
     LoggedInTestCase,
@@ -301,9 +302,7 @@
         middleware = ErrorsMiddleware()
         response = middleware.process_exception(request, exception)
         # The response is a redirect.
-        self.assertEqual(
-            (httplib.FOUND, response['Location']),
-            (response.status_code, url))
+        self.assertEqual(url, extract_redirect(response))
         # An error message has been published.
         self.assertEqual(
             [(constants.ERROR, error_message, '')], request._messages.messages)

=== modified file 'src/maasserver/tests/test_views.py'
--- src/maasserver/tests/test_views.py	2012-04-23 14:02:51 +0000
+++ src/maasserver/tests/test_views.py	2012-04-27 12:45:26 +0000
@@ -13,7 +13,6 @@
 __all__ = []
 
 import httplib
-from urlparse import urlparse
 from xmlrpclib import Fault
 
 from django.conf.urls.defaults import patterns
@@ -26,6 +25,7 @@
 from maasserver import components
 from maasserver.components import register_persistent_error
 from maasserver.exceptions import ExternalComponentException
+from maasserver.testing import extract_redirect
 from maasserver.testing.enum import map_enum
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import (
@@ -189,9 +189,7 @@
         request = RequestFactory().get('/foo')
         view = FakeDeleteView(next_url=next_url, request=request)
         response = view.get(request)
-        self.assertEqual(
-            (httplib.FOUND, next_url),
-            (response.status_code, response['Location']))
+        self.assertEqual(next_url, extract_redirect(response))
         self.assertEqual([view.compose_feedback_nonexistent()], view.notices)
 
     def test_compose_feedback_nonexistent_names_class(self):
@@ -224,10 +222,7 @@
         node = factory.make_node(owner=self.logged_in_user)
         node_edit_link = reverse('node-edit', args=[node.system_id])
         response = self.client.post(node_edit_link, {})
-        redirect_url = urlparse(response['Location']).path
-        self.assertEqual(
-            (httplib.FOUND, redirect_url),
-            (response.status_code, node_edit_link))
+        self.assertEqual(node_edit_link, extract_redirect(response))
 
     def test_raised_ExternalComponentException_publishes_message(self):
         # When a ExternalComponentException is raised in a POST request, a

=== modified file 'src/maasserver/tests/test_views_nodes.py'
--- src/maasserver/tests/test_views_nodes.py	2012-04-27 10:56:37 +0000
+++ src/maasserver/tests/test_views_nodes.py	2012-04-27 12:45:26 +0000
@@ -13,7 +13,6 @@
 __all__ = []
 
 import httplib
-from urlparse import urlparse
 
 from django.conf import settings
 from django.core.urlresolvers import reverse
@@ -32,6 +31,7 @@
     Node,
     )
 from maasserver.testing import (
+    extract_redirect,
     get_content_links,
     reload_object,
     reload_objects,
@@ -300,20 +300,70 @@
         factory.make_sshkey(self.logged_in_user)
         self.set_up_oauth_token()
         node = factory.make_node(status=NODE_STATUS.READY)
+<<<<<<< TREE
         node_link = reverse('node-view', args=[node.system_id])
         response = self.client.post(
             node_link, data={NodeActionForm.input_name: StartNode.display})
         self.assertEqual(httplib.FOUND, response.status_code)
         self.assertEqual(NODE_STATUS.ALLOCATED, reload_object(node).status)
+=======
+        response = self.client.post(
+            reverse('node-view', args=[node.system_id]),
+            data={NodeActionForm.input_name: "Delete node"})
+        self.assertEqual(
+            reverse('node-delete', args=[node.system_id]),
+            extract_redirect(response))
+
+    def test_view_node_POST_admin_cannot_delete_used_node(self):
+        self.become_admin()
+        node = factory.make_node(
+            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
+        response = self.client.post(
+            reverse('node-view', args=[node.system_id]),
+            data={NodeActionForm.input_name: "Delete node"})
+        self.assertEqual(httplib.FORBIDDEN, response.status_code)
+
+    def test_view_node_POST_admin_can_start_commissioning_node(self):
+        self.become_admin()
+        node = factory.make_node(status=NODE_STATUS.DECLARED)
+        node_link = reverse('node-view', args=[node.system_id])
+        response = self.client.post(
+            node_link,
+            data={
+                NodeActionForm.input_name: "Accept & commission",
+            })
+        self.assertEqual(httplib.FOUND, response.status_code)
+        self.assertEqual(
+            NODE_STATUS.COMMISSIONING, reload_object(node).status)
+
+    def test_view_node_POST_admin_can_retry_failed_commissioning(self):
+        self.become_admin()
+        node = factory.make_node(status=NODE_STATUS.FAILED_TESTS)
+        node_link = reverse('node-view', args=[node.system_id])
+        response = self.client.post(
+            node_link,
+            data={NodeActionForm.input_name: "Retry commissioning"})
+        self.assertEqual(httplib.FOUND, response.status_code)
+        self.assertEqual(
+            NODE_STATUS.COMMISSIONING, reload_object(node).status)
+>>>>>>> MERGE-SOURCE
 
     def perform_action_and_get_node_page(self, node, action_name):
         """POST to perform a node action, then load the resulting page."""
         node_link = reverse('node-view', args=[node.system_id])
         response = self.client.post(
+<<<<<<< TREE
             node_link, data={NodeActionForm.input_name: action_name})
         if response.status_code != httplib.FOUND:
             self.fail("%d: '%s'" % (response.status_code, response.content))
         redirect = urlparse(response['Location']).path
+=======
+            node_link,
+            data={
+                NodeActionForm.input_name: action_name,
+            })
+        redirect = extract_redirect(response)
+>>>>>>> MERGE-SOURCE
         if redirect != node_link:
             self.fail("Odd: %s redirected to %s." % (node_link, redirect))
         return self.client.get(redirect)
@@ -355,10 +405,9 @@
         mac = factory.getRandomMACAddress()
         mac_delete_link = reverse('mac-delete', args=[node.system_id, mac])
         response = self.client.get(mac_delete_link)
-        next_url = reverse('node-edit', args=[node.system_id])
         self.assertEqual(
-            (httplib.FOUND, next_url),
-            (response.status_code, urlparse(response['Location']).path))
+            reverse('node-edit', args=[node.system_id]),
+            extract_redirect(response))
 
     def test_node_delete_access_denied_if_user_cannot_edit_node(self):
         node = factory.make_node(owner=factory.make_user())
@@ -382,10 +431,9 @@
         mac = factory.make_mac_address(node=node)
         mac_delete_link = reverse('mac-delete', args=[node.system_id, mac])
         response = self.client.post(mac_delete_link, {'post': 'yes'})
-        next_url = reverse('node-edit', args=[node.system_id])
         self.assertEqual(
-            (httplib.FOUND, next_url),
-            (response.status_code, urlparse(response['Location']).path))
+            reverse('node-edit', args=[node.system_id]),
+            extract_redirect(response))
         self.assertFalse(MACAddress.objects.filter(id=mac.id).exists())
 
     def test_node_delete_mac_POST_displays_message(self):
@@ -393,7 +441,8 @@
         mac = factory.make_mac_address(node=node)
         mac_delete_link = reverse('mac-delete', args=[node.system_id, mac])
         response = self.client.post(mac_delete_link, {'post': 'yes'})
-        response = self.client.get(urlparse(response['Location']).path)
+        redirect = extract_redirect(response)
+        response = self.client.get(redirect)
         self.assertEqual(
             ["Mac address %s deleted." % mac.mac_address],
             [message.message for message in response.context['messages']])
@@ -413,10 +462,9 @@
         mac_add_link = reverse('mac-add', args=[node.system_id])
         mac = factory.getRandomMACAddress()
         response = self.client.post(mac_add_link, {'mac_address': mac})
-        next_url = reverse('node-edit', args=[node.system_id])
         self.assertEqual(
-            (httplib.FOUND, next_url),
-            (response.status_code, urlparse(response['Location']).path))
+            reverse('node-edit', args=[node.system_id]),
+            extract_redirect(response))
         self.assertTrue(
             MACAddress.objects.filter(node=node, mac_address=mac).exists())
 
@@ -425,7 +473,8 @@
         mac_add_link = reverse('mac-add', args=[node.system_id])
         mac = factory.getRandomMACAddress()
         response = self.client.post(mac_add_link, {'mac_address': mac})
-        response = self.client.get(urlparse(response['Location']).path)
+        redirect = extract_redirect(response)
+        response = self.client.get(redirect)
         self.assertEqual(
             ["MAC address added."],
             [message.message for message in response.context['messages']])

=== modified file 'src/maasserver/tests/test_views_prefs.py'
--- src/maasserver/tests/test_views_prefs.py	2012-04-23 11:34:19 +0000
+++ src/maasserver/tests/test_views_prefs.py	2012-04-27 12:45:26 +0000
@@ -14,13 +14,13 @@
 
 
 import httplib
-from urlparse import urlparse
 
 from django.contrib.auth.models import User
 from django.core.urlresolvers import reverse
 from lxml.html import fromstring
 from maasserver.models import SSHKey
 from maasserver.testing import (
+    extract_redirect,
     get_content_links,
     get_data,
     get_prefixed_form_data,
@@ -196,9 +196,7 @@
         del_link = reverse('prefs-delete-sshkey', args=[key.id])
         key.delete()
         response = self.client.get(del_link)
-        self.assertEqual(
-            (httplib.FOUND, '/account/prefs/'),
-            (response.status_code, urlparse(response['Location']).path))
+        self.assertEqual('/account/prefs/', extract_redirect(response))
 
     def test_delete_key_POST(self):
         # A POST request deletes the key, and redirects to the prefs.
@@ -206,9 +204,7 @@
         del_link = reverse('prefs-delete-sshkey', args=[key.id])
         response = self.client.post(del_link, {'post': 'yes'})
 
-        self.assertEqual(
-            (httplib.FOUND, '/account/prefs/'),
-            (response.status_code, urlparse(response['Location']).path))
+        self.assertEqual('/account/prefs/', extract_redirect(response))
         self.assertFalse(SSHKey.objects.filter(id=key.id).exists())
 
     def test_delete_key_POST_ignores_nonexistent_key(self):
@@ -218,6 +214,4 @@
         del_link = reverse('prefs-delete-sshkey', args=[key.id])
         key.delete()
         response = self.client.post(del_link, {'post': 'yes'})
-        self.assertEqual(
-            (httplib.FOUND, '/account/prefs/'),
-            (response.status_code, urlparse(response['Location']).path))
+        self.assertEqual('/account/prefs/', extract_redirect(response))