← Back to team overview

launchpad-reviewers team mailing list archive

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

 

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

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #954653 in MAAS: ""Unable to create node" is a useless error message"
  https://bugs.launchpad.net/maas/+bug/954653

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

Actually by far most of the time went into figuring out why I couldn't make the test break before starting work (it used the wrong variable in its assertion); figuring out what some of the code was meant to do exactly (so you'll see some documentation for pre-existing code show up here); and testing previously untested code (for distributing field errors among form fields).

I also factored out some tedious commonality in the tests.  They're a bit easier to follow now, as well as easier to write.  Nor did I see any tests for the HTML-escaping of the error strings, so I added one.


Jeroen
-- 
https://code.launchpad.net/~jtv/maas/bug-954653/+merge/99029
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/maas/bug-954653 into lp:maas.
=== modified file 'src/maasserver/static/js/node_add.js'
--- src/maasserver/static/js/node_add.js	2012-03-15 13:58:32 +0000
+++ src/maasserver/static/js/node_add.js	2012-03-23 14:26:19 +0000
@@ -87,6 +87,12 @@
         this.get('srcNode').all('div.field-error').remove();
     },
 
+    /* Display validation errors on their respective fields.
+     *
+     * The "errors" argument is an object.  If a field has validation errors,
+     * this object will map the field's name to a list of error strings.  Each
+     * field's errors will be shown with the label for that field.
+     */
     displayFieldErrors: function(errors) {
         this.cleanFormErrors();
         var key;
@@ -227,15 +233,21 @@
                     self.hidePanel();
                 },
                 failure: function(id, out) {
+                    Y.log("Adding a node failed.  Response object follows.")
                     Y.log(out);
                     if (out.status === 400) {
                         try {
-                            // Validation error: display the errors in the
-                            // form.
+                            /* Validation error: display the errors in the
+                             * form next to their respective fields.
+                             */
                             self.displayFieldErrors(JSON.parse(out.response));
                         }
                         catch (e) {
-                            self.displayFormError("Unable to create Node.");
+                            Y.log(
+                                "Exception while decoding error JSON: " +
+                                e.message);
+                            self.displayFormError(
+                                "Unable to create Node: " + out.responseText);
                         }
                     }
                     else if (out.status === 401) {
@@ -244,7 +256,8 @@
                     }
                     else {
                         // Unexpected error.
-                        self.displayFormError("Unable to create Node.");
+                        self.displayFormError(
+                            "Unable to create Node: " + out.responseText);
                     }
                 },
                 end: Y.bind(self.hideSpinner, self)

=== modified file 'src/maasserver/static/js/tests/test_node_add.js'
--- src/maasserver/static/js/tests/test_node_add.js	2012-03-15 13:58:32 +0000
+++ src/maasserver/static/js/tests/test_node_add.js	2012-03-23 14:26:19 +0000
@@ -50,14 +50,72 @@
 
 }));
 
+
+/* Find the add-node widget.
+ */
+function find_widget() {
+    return module._add_node_singleton.get('srcNode');
+}
+
+
+/* Find the add-node form.
+ */
+function find_form() {
+    return find_widget().one('form');
+}
+
+
+/* Find the hostname input field in the add-node form.
+ */
+function find_hostname_input() {
+    return find_widget().one('#id_hostname');
+}
+
+
+/* Find the "Add node" button at the bottom of the add-node form.
+ */
+function find_add_button() {
+    return find_widget().one('.yui3-button');
+}
+
+
+/* Find the global errors panel at the top of the add-node form.
+ */
+function find_global_errors() {
+    return find_widget().one('.form-global-errors');
+}
+
+
+/* Set up and submit the add-node form.
+ */
+function submit_add_node() {
+    module.showAddNodeWidget();
+    find_hostname_input().set('value', 'host');
+    find_add_button().simulate('click');
+}
+
+
 suite.add(new Y.maas.testing.TestCase({
     name: 'test-add-node-widget-add-node',
 
+    /* Set up a mock failure for adding a node.
+     *
+     * When the form tries to send an add-node request, the request's failure
+     * handler will be called with the fake response you provide.
+     */
+    mock_failure: function(fake_response) {
+	var mockXhr = new Y.Base();
+	mockXhr.send = function(url, cfg) {
+	    /* The 3 is a bogus transaction id. */
+	    cfg.on.failure(3, fake_response);
+	};
+	this.mockIO(mockXhr, module);
+    },
+
     testFormContainsArchitectureChoice: function() {
         // The generated form contains an 'architecture' field.
         module.showAddNodeWidget();
-        var panel = module._add_node_singleton;
-        var arch = panel.get('srcNode').one('form').one('#id_architecture');
+        var arch = find_form().one('#id_architecture');
         Y.Assert.isNotNull(arch);
         var arch_options = arch.all('option');
         Y.Assert.areEqual(2, arch_options.size());
@@ -67,18 +125,16 @@
         // The call to the API triggered by clicking on 'Add a node'
         // submits (via an API call) the panel's form.
         module.showAddNodeWidget();
-        var panel = module._add_node_singleton;
         var mockXhr = new Y.Base();
         var fired = false;
-        var form = panel.get('srcNode').one('form');
+        var form = find_form();
         mockXhr.send = function(uri, cfg) {
             fired = true;
             Y.Assert.areEqual(form, cfg.form);
         };
         this.mockIO(mockXhr, module);
-        panel.get('srcNode').one('#id_hostname').set('value', 'host');
-        var button = panel.get('srcNode').one('.yui3-button');
-        button.simulate('click');
+        find_hostname_input().set('value', 'host');
+        find_add_button().simulate('click');
         Y.Assert.isTrue(fired);
     },
 
@@ -90,10 +146,8 @@
         });
         this.mockIO(mockXhr, module);
         module.showAddNodeWidget();
-        var panel = module._add_node_singleton;
-        panel.get('srcNode').one('#id_hostname').set('value', 'host');
-        var button = panel.get('srcNode').one('.yui3-button');
-        button.simulate('click');
+        find_hostname_input().set('value', 'host');
+        find_add_button().simulate('click');
         Y.Mock.verify(mockXhr);
     },
 
@@ -105,11 +159,9 @@
         });
         this.mockIO(mockXhr, module);
         module.showAddNodeWidget();
-        var panel = module._add_node_singleton;
-        panel.get('srcNode').one('#id_hostname').set('value', 'host');
-        var form = panel.get('srcNode').one('form');
+        find_hostname_input().set('value', 'host');
         // Simulate 'Enter' being pressed.
-        form.simulate("keypress", { keyCode: 13 });
+        find_form().simulate("keypress", { keyCode: 13 });
         Y.Mock.verify(mockXhr);
     },
 
@@ -124,9 +176,8 @@
             Y.bind(
                 module._add_node_singleton.destroy,
                 module._add_node_singleton));
-        var panel = module._add_node_singleton;
-        panel.get('srcNode').one('#id_hostname').set('value', 'host');
-        var button = panel.get('srcNode').one('.yui3-button');
+        find_hostname_input().set('value', 'host');
+        var button = find_add_button();
 
         var fired = false;
         this.registerListener(
@@ -140,39 +191,48 @@
         Y.Assert.isTrue(fired);
     },
 
-    testGenericErrorMessage: function() {
-        var mockXhr = new Y.Base();
-        mockXhr.send = function(url, cfg) {
-            cfg.on.failure(3, {status: 500});
-        };
-        this.mockIO(mockXhr, module);
-        module.showAddNodeWidget();
-        var panel = module._add_node_singleton;
-        panel.get('srcNode').one('#id_hostname').set('value', 'host');
-        var button = panel.get('srcNode').one('.yui3-button');
-        button.simulate('click');
-        var error_message = panel.get(
-            'srcNode').one('.form-global-errors').get('innerHTML');
-        var message_position = error_message.search("Unable to create Node.");
-        Y.Assert.areNotEqual(-1, error_message);
+    testValidationErrorInJSONGoesToFieldsNotGlobalErrors: function() {
+        this.mock_failure({
+            status: 400,
+            response: '{"architecture": ["Xur."]}'
+        });
+        submit_add_node();
+        Y.Assert.areEqual(
+            -1, find_global_errors().get('innerHTML').search("Xur."));
+        var field_label = find_widget().one('label[for="id_architecture"]');
+        var error_node = field_label.next();
+        Y.Assert.areNotEqual(-1, error_node.get('innerHTML').search("Xur."));
+    },
+
+    test400ErrorMessageWithPlainText: function() {
+        this.mock_failure({status: 400, responseText: "Blergh."});
+        submit_add_node();
+        var error_message = find_global_errors().get('innerHTML');
+        Y.Assert.areNotEqual(-1, error_message.search("Blergh."));
     },
 
     testLoggedOffErrorMessage: function() {
-        var mockXhr = new Y.Base();
-        mockXhr.send = function(url, cfg) {
-            cfg.on.failure(3, {status: 401});
-        };
-        this.mockIO(mockXhr, module);
-        module.showAddNodeWidget();
-        var panel = module._add_node_singleton;
-        panel.get('srcNode').one('#id_hostname').set('value', 'host');
-        var button = panel.get('srcNode').one('.yui3-button');
-        button.simulate('click');
-        var error_message = panel.get(
-            'srcNode').one('.form-global-errors').get('innerHTML');
+        this.mock_failure({status: 401});
+        submit_add_node();
+        var error_message = find_global_errors().get('innerHTML');
         // The link to the login page is present in the error message.
         var link_position = error_message.search(MAAS_config.uris.login);
         Y.Assert.areNotEqual(-1, link_position);
+    },
+
+    testGenericErrorMessage: function() {
+        this.mock_failure({status: 500, responseText: "Internal error."});
+        submit_add_node();
+        var error_message = find_global_errors().get('innerHTML');
+        Y.Assert.areNotEqual(-1, error_message.search("Internal error."));
+    },
+
+    testErrorsAreEscaped: function() {
+        this.mock_failure({status: 400, responseText: "<huh>"});
+        submit_add_node();
+        var error_message = find_global_errors().get('innerHTML');
+        Y.Assert.areEqual(-1, error_message.search("<huh>"));
+        Y.Assert.areNotEqual(-1, error_message.search("&lt;huh&gt;"));
     }
 
 }));