Merge lp:~danilo/launchpad/bug-740640 into lp:launchpad

Proposed by Данило Шеган
Status: Merged
Approved by: Данило Шеган
Approved revision: no longer in the source branch.
Merged at revision: 12691
Proposed branch: lp:~danilo/launchpad/bug-740640
Merge into: lp:launchpad
Prerequisite: lp:~danilo/launchpad/accordionoverlay
Diff against target: 395 lines (+267/-5)
5 files modified
lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py (+8/-0)
lib/lp/bugs/model/bugsubscriptionfilter.py (+17/-1)
lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py (+27/-0)
lib/lp/registry/javascript/structural-subscription.js (+119/-4)
lib/lp/registry/javascript/tests/test_structural_subscription.js (+96/-0)
To merge this branch: bzr merge lp:~danilo/launchpad/bug-740640
Reviewer Review Type Date Requested Status
Henning Eggers (community) ui,code Approve
Registry Administrators ui Pending
Review via email: mp+55123@code.launchpad.net

Commit message

[r=henninge][ui=henninge][bug=740640] Protect against XSS in structural subscription overlay and add nicer client-side validation.

Description of the change

= Bug 740640 =

At the moment, structural subscription overlay allows XSS attacks.

We should stop them at the server-side and provide nice JS-based validation to inform the users of the conditions.

Since we want similar approach for client-side tags validation, this branch implements that as well.

== Pre-implementation notes ==

I've discussed approach to solving this with Gary: allowing everything in the subscription name, while probably the best approach, would complicate implementation in that existing infrastructure just populates the data from the server-side objects directly. Thus, we decided on black-listing potential XSS-attack characters for subscription name.

== Implementation details ==

For server-side, we implement "description" property through setter and getter.

Server-side changes:
 - bugs/model/bugsubscriptionfilter.py
 - bugs/browser/tests/test_bugsubscriptionfilter.py
 - bugs/model/tests/test_bugsubscriptionfilter.py

Client-side changes:
 - All the rest

== Tests ==

  bin/test -cvvt BugSubscriptionFilter.*description
  lib/lp/registry/javascript/tests/test_structural_subscription.html

== Demo and Q/A ==

See also https://devpad.canonical.com/~danilo/screencasts/inline-validation-error.ogv

  1. Go to https://launchpad.dev/+feature-rules and add "advanced-structural-subscriptions.enabled default 10 on"
  2. Go to http://launchpad.dev/firefox and click on "Subscribe to bug mail" (on the right) and try entering "<", ">", "&" or "\"" into the subscription name: check gets triggered on the 'change' event, which means when you focus on the next element
  3. Similarly, expand "more options" and try adding a few invalid tags (valid tags are "[a-z0-9][a-z0-9+.-]*", i.e. all lowercase or digits with +, ., -)
  4. Try saving with the errors still present

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/templates/product-index.pt
  lib/lp/registry/help/structural-subscription-name.html
  lib/lp/registry/help/structural-subscription-tags.html
  lib/lp/bugs/browser/structuralsubscription.py
  lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
  lib/lp/registry/javascript/tests/test_structural_subscription.html
  lib/lp/app/javascript/lp.js
  lib/lp/registry/browser/product.py
  lib/lp/bugs/browser/tests/test_expose.py
  lib/lp/bugs/model/bugsubscriptionfilter.py
  lib/lp/registry/templates/product-portlet-license-missing.pt
  lib/lp/bugs/templates/bug-subscription-list.pt
  lib/lp/bugs/templates/bugtarget-subscription-list.pt
  lib/lp/services/inlinehelp/javascript/inlinehelp.js
  lib/lp/registry/javascript/tests/test_structural_subscription.js
  lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py
  lib/lp/registry/javascript/structural-subscription.js
  buildout-templates/bin/combine-css.in
  lib/lp/app/javascript/tests/test_accordionoverlay.html

./lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py
      82: E301 expected 1 blank line, found 0

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :
Download full text (10.3 KiB)

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Hi Danilo,
thanks for this important fix, I am sorry it took a bit longer to review it.
Also thanks for the discussion on IRC, I really enjoyed getting behind what
this code does (and I almost fully succeeded ... ;-)

With the issues solved as discussed, this is good to land code-wise. Please
consider my other two suggestions below.

 review approve code

Cheers,
Henning

Am 28.03.2011 13:22, schrieb Данило Шеган:
> === modified file 'lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py'
> --- lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2011-02-18 18:16:34 +0000
> +++ lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2011-03-28 11:22:31 +0000
> @@ -183,6 +183,15 @@
> self.assertEqual(
> u"It's late.", self.subscription_filter.description)
>
> + def test_modify_description_xss_safeguard(self):
> + # The description can be modified.
> +
> + # Modify, save, and start a new transaction.

Oops, something wrong with the comments here ...

> + self.ws_subscription_filter.description = u"&gt; <>"
> + error = self.assertRaises(
> + BadRequest, self.ws_subscription_filter.lp_save)
> + self.assertEqual(400, error.response.status)
> +
> def test_modify_statuses(self):
> # The statuses field can be modified.
> self.assertEqual(set(), self.subscription_filter.statuses)
>
> === modified file 'lib/lp/bugs/model/bugsubscriptionfilter.py'
> --- lib/lp/bugs/model/bugsubscriptionfilter.py 2011-03-22 14:53:26 +0000
> +++ lib/lp/bugs/model/bugsubscriptionfilter.py 2011-03-28 11:22:31 +0000
> @@ -22,6 +22,7 @@
> from canonical.database.sqlbase import sqlvalues
> from canonical.launchpad import searchbuilder
> from canonical.launchpad.interfaces.lpstorm import IStore
> +from lazr.restful.error import expose
> from lp.bugs.enum import BugNotificationLevel
> from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter
> from lp.bugs.interfaces.bugtask import (
> @@ -62,7 +63,20 @@
>
> other_parameters = Unicode()
>
> - description = Unicode()
> + _description = Unicode('description')
> +
> + def _get_description(self):
> + return self._description
> +
> + def _set_description(self, description):
> + if ('<' in description or '>' in description or
> + '"' in description or '&' in description):

I don't like multi-line conditions. Can you please use an intermediate
variable? Ore would a regular expression be too expensive here?
re.find('[<>"&]', description), I think.

> + raise expose(ValueError(
> + 'BugSubscriptionFilter description cannot contain '
> + 'any of <, >, " or &.'))
> + self._description = description
> +
> + description = property(_get_description, _set_description)
>
> def _get_statuses(self):
> """Return a frozenset of statuses to filter on."""
>
> === modified file 'lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py'
> --- lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py 2011-03-21 18:20:09 +0000
> +++ lib/lp/bugs/model/tests/test_bugsubscriptionf...

review: Approve (code)
Revision history for this message
Henning Eggers (henninge) wrote :

The only UI issue I see is the question if the error text for the tags is correct. Should it not mention that tags can only begin with an alphanumeric character? Otherwise we might have users wondering what they are doing wrong by entering ".foo".

review: Approve (ui,code)
Revision history for this message
Данило Шеган (danilo) wrote :
Download full text (6.5 KiB)

У пон, 28. 03 2011. у 15:24 +0000, Henning Eggers пише:
> Review: Approve code
> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> Hi Danilo,
> thanks for this important fix, I am sorry it took a bit longer to review it.
> Also thanks for the discussion on IRC, I really enjoyed getting behind what
> this code does (and I almost fully succeeded ... ;-)
>
> With the issues solved as discussed, this is good to land code-wise. Please
> consider my other two suggestions below.
>
> review approve code
>
> Cheers,
> Henning
>
>
> Am 28.03.2011 13:22, schrieb Данило Шеган:
> > === modified file 'lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py'
> > --- lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2011-02-18 18:16:34 +0000
> > +++ lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2011-03-28 11:22:31 +0000
> > @@ -183,6 +183,15 @@
> > self.assertEqual(
> > u"It's late.", self.subscription_filter.description)
> >
> > + def test_modify_description_xss_safeguard(self):
> > + # The description can be modified.
> > +
> > + # Modify, save, and start a new transaction.
>
> Oops, something wrong with the comments here ...

Fixed :)

> > === modified file 'lib/lp/bugs/model/bugsubscriptionfilter.py'
> > --- lib/lp/bugs/model/bugsubscriptionfilter.py 2011-03-22 14:53:26 +0000
> > +++ lib/lp/bugs/model/bugsubscriptionfilter.py 2011-03-28 11:22:31 +0000

> > @@ -62,7 +63,20 @@
> >
> > other_parameters = Unicode()
> >
> > - description = Unicode()
> > + _description = Unicode('description')
> > +
> > + def _get_description(self):
> > + return self._description
> > +
> > + def _set_description(self, description):
> > + if ('<' in description or '>' in description or
> > + '"' in description or '&' in description):
>
> I don't like multi-line conditions. Can you please use an intermediate
> variable? Ore would a regular expression be too expensive here?
> re.find('[<>"&]', description), I think.

I did consider using the re module (but for a very short while). It's
not about multi-line for me though, but mostly about performance: this
would do four passes over the string, and I suspect re would be even
faster for long strings. However, with short strings (like descriptions
will be), burden of loading the re module and compiling the expression
is probably more than that.

I'm happy to put it into an intermediate variable though, so I did that.

> > === modified file 'lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py'
> > --- lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py 2011-03-21 18:20:09 +0000
> > +++ lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py 2011-03-28 11:22:31 +0000
> > @@ -70,6 +70,21 @@
> > self.assertEqual(u"foo", bug_subscription_filter.other_parameters)
> > self.assertEqual(u"bar", bug_subscription_filter.description)
> >
> > + def test_description(self):
> > + """Test the description property."""
> > + bug_subscription_filter = BugSubscriptionFilter()
> > + bug_subscription_filter.description = u"foo"
> > + self.assertEqual(u"foo", bug_subscription_filter.descrip...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py'
--- lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2011-02-18 18:16:34 +0000
+++ lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2011-03-29 13:11:26 +0000
@@ -183,6 +183,14 @@
183 self.assertEqual(183 self.assertEqual(
184 u"It's late.", self.subscription_filter.description)184 u"It's late.", self.subscription_filter.description)
185185
186 def test_modify_description_xss_safeguard(self):
187 # The description can be modified but raises an
188 # exception if you try potential XSS attack characters.
189 self.ws_subscription_filter.description = u"&gt; <>"
190 error = self.assertRaises(
191 BadRequest, self.ws_subscription_filter.lp_save)
192 self.assertEqual(400, error.response.status)
193
186 def test_modify_statuses(self):194 def test_modify_statuses(self):
187 # The statuses field can be modified.195 # The statuses field can be modified.
188 self.assertEqual(set(), self.subscription_filter.statuses)196 self.assertEqual(set(), self.subscription_filter.statuses)
189197
=== modified file 'lib/lp/bugs/model/bugsubscriptionfilter.py'
--- lib/lp/bugs/model/bugsubscriptionfilter.py 2011-03-23 15:55:44 +0000
+++ lib/lp/bugs/model/bugsubscriptionfilter.py 2011-03-29 13:11:26 +0000
@@ -22,6 +22,7 @@
22from canonical.database.sqlbase import sqlvalues22from canonical.database.sqlbase import sqlvalues
23from canonical.launchpad import searchbuilder23from canonical.launchpad import searchbuilder
24from canonical.launchpad.interfaces.lpstorm import IStore24from canonical.launchpad.interfaces.lpstorm import IStore
25from lazr.restful.error import expose
25from lp.bugs.enum import BugNotificationLevel26from lp.bugs.enum import BugNotificationLevel
26from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter27from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter
27from lp.bugs.interfaces.bugtask import (28from lp.bugs.interfaces.bugtask import (
@@ -62,7 +63,22 @@
6263
63 other_parameters = Unicode()64 other_parameters = Unicode()
6465
65 description = Unicode()66 _description = Unicode('description')
67
68 def _get_description(self):
69 return self._description
70
71 def _set_description(self, description):
72 has_html_chars = (
73 '<' in description or '>' in description or
74 '"' in description or '&' in description)
75 if has_html_chars:
76 raise expose(ValueError(
77 'BugSubscriptionFilter description cannot contain '
78 'any of <, >, " or &.'))
79 self._description = description
80
81 description = property(_get_description, _set_description)
6682
67 def _get_statuses(self):83 def _get_statuses(self):
68 """Return a frozenset of statuses to filter on."""84 """Return a frozenset of statuses to filter on."""
6985
=== modified file 'lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py'
--- lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py 2011-03-21 18:23:31 +0000
+++ lib/lp/bugs/model/tests/test_bugsubscriptionfilter.py 2011-03-29 13:11:26 +0000
@@ -70,6 +70,33 @@
70 self.assertEqual(u"foo", bug_subscription_filter.other_parameters)70 self.assertEqual(u"foo", bug_subscription_filter.other_parameters)
71 self.assertEqual(u"bar", bug_subscription_filter.description)71 self.assertEqual(u"bar", bug_subscription_filter.description)
7272
73 def test_description(self):
74 """Test the description property."""
75 bug_subscription_filter = BugSubscriptionFilter()
76 bug_subscription_filter.description = u"foo"
77 self.assertEqual(u"foo", bug_subscription_filter.description)
78
79 def test_description_xss_safeguard_tags(self):
80 """Test that description property disallows a HTML tags."""
81 bug_subscription_filter = BugSubscriptionFilter()
82 self.assertRaises(ValueError,
83 setattr, bug_subscription_filter, 'description',
84 u'<script>')
85
86 def test_description_xss_safeguard_ampersand(self):
87 """description property disallows a few HTML characters."""
88 bug_subscription_filter = BugSubscriptionFilter()
89 self.assertRaises(ValueError,
90 setattr, bug_subscription_filter, 'description',
91 u'test & blow up')
92
93 def test_description_xss_safeguard_quote(self):
94 """description property disallows a few HTML characters."""
95 bug_subscription_filter = BugSubscriptionFilter()
96 self.assertRaises(ValueError,
97 setattr, bug_subscription_filter, 'description',
98 u'test "double" quotes')
99
73 def test_defaults(self):100 def test_defaults(self):
74 """Test the default values of `BugSubscriptionFilter` objects."""101 """Test the default values of `BugSubscriptionFilter` objects."""
75 # Create.102 # Create.
76103
=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
--- lib/lp/registry/javascript/structural-subscription.js 2011-03-28 19:24:42 +0000
+++ lib/lp/registry/javascript/structural-subscription.js 2011-03-29 13:11:26 +0000
@@ -208,6 +208,9 @@
208208
209function save_subscription(form_data) {209function save_subscription(form_data) {
210 var who;210 var who;
211 var has_errors = check_for_errors_in_overlay(add_subscription_overlay);
212 if (has_errors)
213 return false;
211 if (form_data.recipient[0] === 'user') {214 if (form_data.recipient[0] === 'user') {
212 who = LP.links.me;215 who = LP.links.me;
213 } else {216 } else {
@@ -218,10 +221,36 @@
218}221}
219namespace.save_subscription = save_subscription;222namespace.save_subscription = save_subscription;
220223
224function check_for_errors_in_overlay(overlay) {
225 var has_errors = false;
226 var errors = new Array();
227 for (var field in overlay.field_errors) {
228 if (overlay.field_errors[field]) {
229 has_errors = true;
230 errors.push(field);
231 }
232 };
233 if (has_errors) {
234 var error_text = errors.pop()
235 if (errors.length > 0) {
236 error_text = errors.join(', ') + ' and ' + error_text;
237 }
238
239 overlay.showError(
240 'Value for ' + error_text + ' is invalid.');
241 return true;
242 } else {
243 return false;
244 }
245};
246
221/**247/**
222 * Handle the activation of the edit subscription link.248 * Handle the activation of the edit subscription link.
223 */249 */
224function edit_subscription_handler(context, form_data) {250function edit_subscription_handler(context, form_data) {
251 var has_errors = check_for_errors_in_overlay(add_subscription_overlay);
252 if (has_errors)
253 return false;
225 var on = {success: function (new_data) {254 var on = {success: function (new_data) {
226 var filter = new_data.getAttrs();255 var filter = new_data.getAttrs();
227 var description_node = Y.one(256 var description_node = Y.one(
@@ -237,6 +266,18 @@
237 patch_bug_filter(context.filter_info.filter, form_data, on);266 patch_bug_filter(context.filter_info.filter, form_data, on);
238}267}
239268
269function setup_overlay_validators(overlay, overlay_id) {
270 overlay.field_validators = {
271 name: get_error_for_subscription_name,
272 tags: get_error_for_tags_list
273 };
274 overlay.field_errors = {};
275 for (var field_name in overlay.field_validators) {
276 add_input_validator(overlay, overlay_id, field_name,
277 overlay.field_validators[field_name]);
278 };
279};
280
240/**281/**
241 * Populate the overlay element with the contents of the add/edit form.282 * Populate the overlay element with the contents of the add/edit form.
242 */283 */
@@ -252,9 +293,15 @@
252 visible: false,293 visible: false,
253 form_submit_button: submit_button,294 form_submit_button: submit_button,
254 form_cancel_button: Y.Node.create(cancel_button_html),295 form_cancel_button: Y.Node.create(cancel_button_html),
255 form_submit_callback: submit_callback296 form_submit_callback: function(formdata) {
297 // Do not clean up if saving was not successful.
298 var save_succeeded = submit_callback(formdata);
299 if (save_succeeded !== false)
300 clean_up();
301 }
256 });302 });
257 add_subscription_overlay.render(content_box_id);303 add_subscription_overlay.render(content_box_id);
304 setup_overlay_validators(add_subscription_overlay, overlay_id);
258 // Prevent cruft from hanging around upon closing.305 // Prevent cruft from hanging around upon closing.
259 function clean_up(e) {306 function clean_up(e) {
260 var filter_wrapper = Y.one('#' + FILTER_WRAPPER);307 var filter_wrapper = Y.one('#' + FILTER_WRAPPER);
@@ -263,8 +310,6 @@
263 }310 }
264 add_subscription_overlay.get('form_cancel_button').on(311 add_subscription_overlay.get('form_cancel_button').on(
265 'click', clean_up);312 'click', clean_up);
266 add_subscription_overlay.get('form_submit_button').on(
267 'click', clean_up);
268 add_subscription_overlay.on('cancel', clean_up);313 add_subscription_overlay.on('cancel', clean_up);
269}314}
270315
@@ -396,6 +441,11 @@
396 };441 };
397}442}
398443
444/* We want to call 'resize' directly on tags container
445 * when we add a validation failed error message.
446 */
447var tags_container;
448
399/**449/**
400 * Create the accordion.450 * Create the accordion.
401 *451 *
@@ -426,6 +476,8 @@
426 contentHeight: {method: "auto"}476 contentHeight: {method: "auto"}
427 } );477 } );
428478
479 tags_container = tags_ai;
480
429 tags_ai.set("bodyContent",481 tags_ai.set("bodyContent",
430 '<div>\n' +482 '<div>\n' +
431 '<div>\n' +483 '<div>\n' +
@@ -774,6 +826,70 @@
774 }, window);826 }, window);
775};827};
776828
829/*
830 * Set up a validator function for a form input field.
831 * @method add_input_validator
832 * @param {Object} overlay Overlay object.
833 * @param {String} overlay_id Element ID of the containing overlay.
834 * @param {String} field_name Form <input> 'name' to set up a validator for.
835 * @param {String} validator Function which returns 'null' if there is
836 no error in the field value, and an error message otherwise.
837 */
838function add_input_validator(overlay, overlay_id, field_name, validator) {
839 var input = Y.one(overlay_id + ' input[name="'+field_name+'"]');
840 var field_container = input.get('parentNode');
841 var error_container = Y.Node.create('<div class="inline-warning"></div>');
842 field_container.appendChild(error_container);
843
844 input.on('change', function(e) {
845 var error_text = validator(input.get('value'));
846 if (error_text !== null) {
847 Y.lazr.anim.red_flash({node: input}).run();
848 error_container.setContent(error_text);
849 overlay.field_errors[field_name] = true;
850 // Accordion sets fixed height for the accordion item,
851 // so we have to resize the tags container.
852 if (field_name == 'tags')
853 tags_container.resize();
854 // Firefox prohibits focus from inside the 'focus lost' event
855 // handler (probably to stop loops), so we need to run
856 // it from a different context (which we do with setTimeout).
857 setTimeout(function() { input.focus(); input.select(); }, 1);
858 } else {
859 error_container.setContent('');
860 overlay.field_errors[field_name] = false;
861 }
862 });
863};
864
865function get_error_for_subscription_name(value) {
866 if (value.search(/[\<\>\"\&]/) == -1) {
867 return null;
868 } else {
869 return ('Subscription name cannot contain any of the following ' +
870 'characters: &lt; &gt; &quot; &amp;');
871 }
872};
873// Export for testing
874namespace._get_error_for_subscription_name = get_error_for_subscription_name;
875
876function get_error_for_tags_list(value) {
877 // See database/schema/trusted.sql valid_name() function
878 // which is used to validate a single tag.
879 // As an extension, we also allow "-" (hyphen) in front of
880 // any tag to indicate exclusion of a tag, and we accept
881 // a space-separated list.
882 if (value.match(/^(\-?[a-z0-9][a-z0-9\+\.\-]*[ ]*)*$/) !== null) {
883 return null;
884 } else {
885 return ('Tags can only contain lowercase ASCII letters, ' +
886 'digits 0-9 and symbols "+", "-" or ".", and they ' +
887 'must start with a lowercase letter or a digit.');
888 }
889};
890// Export for testing
891namespace._get_error_for_tags_list = get_error_for_tags_list;
892
777function get_input_by_value(node, value) {893function get_input_by_value(node, value) {
778 // XXX broken: this should also care about input name because some values894 // XXX broken: this should also care about input name because some values
779 // repeat in other areas of the form895 // repeat in other areas of the form
@@ -1180,7 +1296,6 @@
1180 // initialized except for the ones we added, so setupHelpTrigger1296 // initialized except for the ones we added, so setupHelpTrigger
1181 // is idempotent. Notice that this is old MochiKit code.1297 // is idempotent. Notice that this is old MochiKit code.
1182 forEach(findHelpLinks(), setupHelpTrigger);1298 forEach(findHelpLinks(), setupHelpTrigger);
1183
1184}; // setup1299}; // setup
11851300
1186}, '0.1', {requires: [1301}, '0.1', {requires: [
11871302
=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-03-28 19:24:42 +0000
+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-03-29 13:11:26 +0000
@@ -235,6 +235,102 @@
235 suite.add(test_case);235 suite.add(test_case);
236236
237 test_case = new Y.Test.Case({237 test_case = new Y.Test.Case({
238 name: 'Structural Subscription validation tests',
239
240 _should: {
241 error: {
242 }
243 },
244
245 setUp: function() {
246 // Monkeypatch LP to avoid network traffic and to allow
247 // insertion of test data.
248 window.LP = {
249 links: {},
250 cache: {}
251 };
252
253 },
254
255 test_get_error_for_subscription_name_valid: function() {
256 // Valid name makes get_error_for_subscription_name return null.
257 var name = 'Test + name!';
258 Assert.isNull(module._get_error_for_subscription_name(name));
259 },
260
261 assertHasErrorInSubscriptionName: function(subscription_name) {
262 var error_text = module._get_error_for_subscription_name(
263 subscription_name);
264 Assert.isNotNull(error_text);
265 Assert.areEqual(
266 'Subscription name cannot contain any of the following ' +
267 'characters: &lt; &gt; &quot; &amp;',
268 error_text);
269 },
270
271 test_get_error_for_subscription_name_left_angle_bracket: function() {
272 // For invalid names get_error_for_subscription_name returns
273 // an error message instead.
274 this.assertHasErrorInSubscriptionName('<Test');
275 },
276
277 test_get_error_for_subscription_name_right_angle_bracket: function() {
278 // For invalid names get_error_for_subscription_name returns
279 // an error message instead.
280 this.assertHasErrorInSubscriptionName('Test>');
281 },
282
283 test_get_error_for_subscription_name_ampersand: function() {
284 // For invalid names get_error_for_subscription_name returns
285 // an error message instead.
286 this.assertHasErrorInSubscriptionName('Test & fail');
287 },
288
289 test_get_error_for_subscription_name_quote: function() {
290 // For invalid names get_error_for_subscription_name returns
291 // an error message instead.
292 this.assertHasErrorInSubscriptionName('Test "failure"');
293 },
294
295 test_get_error_for_tags_list_valid: function() {
296 // Valid tags list is a space-separated list of tags
297 // consisting of all lowercase and digits and potentially
298 // '+', '-', '.' in non-initial characters.
299 var tags = 'tag1 tag+2 tag.3 tag-4 5tag';
300 Assert.isNull(module._get_error_for_tags_list(tags));
301 },
302
303 assertHasErrorInTagsList: function(tags) {
304 var error_text = module._get_error_for_tags_list(tags);
305 Assert.isNotNull(error_text);
306 Assert.areEqual(
307 'Tags can only contain lowercase ASCII letters, ' +
308 'digits 0-9 and symbols "+", "-" or ".", and they ' +
309 'must start with a lowercase letter or a digit.',
310 error_text);
311 },
312
313
314 test_get_error_for_tags_list_uppercase: function() {
315 // Uppercase is not allowed in tags.
316 this.assertHasErrorInTagsList('Tag');
317 },
318
319 test_get_error_for_tags_list_invalid_characters: function() {
320 // Anything other than lowercase, digits or '+', '-' and '.'
321 // is invalid in tags.
322 this.assertHasErrorInTagsList('tag#!');
323 },
324
325 test_get_error_for_tags_list_special_characters: function() {
326 // Even if '+', '-' or '.' are allowed in tags,
327 // they must not be at the beginning of a tag.
328 this.assertHasErrorInTagsList('tag1 +tag2 -tag3 .tag4');
329 },
330 });
331 suite.add(test_case);
332
333 test_case = new Y.Test.Case({
238 name: 'Structural Subscription interaction tests',334 name: 'Structural Subscription interaction tests',
239335
240 _should: {336 _should: {